Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion packages/vue-router/src/viewStacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,29 @@ export const createViewStacks = (router: Router) => {
* and will not run route guards that
* are written in the component.
*/
const instance = viewItem.vueComponentRef.value;
viewItem.matchedRoute.instances = {
default: viewItem.vueComponentRef.value,
default: instance,
};

/**
* Run any callbacks passed to `next()` inside a
* `beforeRouteEnter` guard now that the component
* instance is available. Vue Router's official
* `<router-view>` does this in a post-flush watcher;
* because IonRouterOutlet manages rendering itself,
* the wrapper must invoke them explicitly.
*/
const enterCallbacks = viewItem.matchedRoute.enterCallbacks?.default;
if (instance && enterCallbacks && enterCallbacks.length > 0) {
/**
* Clear before invoking so any synchronous navigation
* triggered by a callback can push fresh callbacks onto
* the array without being wiped by a post-iteration reset.
*/
viewItem.matchedRoute.enterCallbacks.default = [];
enterCallbacks.forEach((cb) => cb(instance));
}
};

const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/vue/test/base/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/routing/child',
component: () => import('@/views/RoutingChild.vue')
},
{
path: '/routing/guards',
component: () => import('@/views/RoutingGuards.vue')
},
{
path: '/routing/:id',
component: () => import('@/views/RoutingParameter.vue'),
Expand Down
4 changes: 4 additions & 0 deletions packages/vue/test/base/src/views/Routing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
<ion-label>Go to Child Page</ion-label>
</ion-item>

<ion-item router-link="/routing/guards" id="guards">
<ion-label>Go to Route Guards Page</ion-label>
</ion-item>

<ion-item router-link="/routing/abc" id="parameter-abc">
<ion-label>Go to Parameter Page ABC</ion-label>
</ion-item>
Expand Down
69 changes: 69 additions & 0 deletions packages/vue/test/base/src/views/RoutingGuards.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<ion-page data-pageid="routing-guards">
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/routing"></ion-back-button>
</ion-buttons>
<ion-title>Routing Guards</ion-title>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<h2>beforeRouteEnter next() callback</h2>
<p>
This page's <code>beforeRouteEnter</code> guard calls
<code>next((vm) =&gt; { vm.member = 5 })</code>.
</p>
<p>
Expected <code>member</code> value: <strong>5</strong>.
</p>
<p>
Actual <code>member</code> value:
<strong data-test="member-value">{{ member }}</strong>
</p>
<p
data-test="result"
:data-status="member === 5 ? 'pass' : 'fail'"
>
Result:
<strong v-if="member === 5">PASS</strong>
<strong v-else>FAIL (callback never ran)</strong>
</p>
</ion-content>
</ion-page>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import {
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
} from '@ionic/vue';

export default defineComponent({
name: 'RoutingGuards',
components: {
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
},
data() {
return { member: 0 };
},
beforeRouteEnter(_to, _from, next) {
next((vm: any) => {
vm.member = 5;
});
},
});
</script>
44 changes: 44 additions & 0 deletions packages/vue/test/base/tests/unit/routing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,50 @@ describe('Routing', () => {
expect(beforeRouteEnterSpy).toHaveBeenCalledTimes(2);
});

// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/25013
it('should run beforeRouteEnter next() callback with the component instance', async () => {
const enterCallbackSpy = vi.fn();
const Page = {
data() {
return { member: 0 };
},
beforeRouteEnter(_to: any, _from: any, next: (cb: (vm: any) => void) => void) {
next((vm: any) => {
enterCallbackSpy(vm);
vm.member = 5;
});
},
name: 'PageWithEnterCb',
components: { IonPage },
template: `<ion-page><span data-test="member">{{ member }}</span></ion-page>`
};

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{ path: '/', component: { template: '<ion-page></ion-page>', components: { IonPage } } },
{ path: '/page', component: Page },
]
});

router.push('/');
await router.isReady();
const wrapper = mount(IonRouterOutlet, {
global: {
plugins: [router, IonicVue]
}
});

router.push('/page');
await waitForRouter();

expect(enterCallbackSpy).toHaveBeenCalledTimes(1);
const instance = enterCallbackSpy.mock.calls[0][0];
expect(instance).toBeDefined();
expect(instance.member).toBe(5);
expect(wrapper.find('[data-test="member"]').text()).toBe('5');
});

// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/24109
it('canGoBack() should return the correct value', async () => {
const Page = {
Expand Down