fix(vue): pages now render in correct outlet when using multiple nested outlets (#22301)

resolves #22286
This commit is contained in:
Liam DeBeasi
2020-10-13 14:22:51 -04:00
committed by GitHub
parent fff82d0bdc
commit 52f655c9d4
16 changed files with 300 additions and 50 deletions

View File

@ -6,14 +6,6 @@ import { RouteInfo,
export const createViewStacks = () => { export const createViewStacks = () => {
let viewStacks: ViewStacks = {}; let viewStacks: ViewStacks = {};
const tabsPrefixes = new Set();
const addTabsPrefix = (prefix: string) => tabsPrefixes.add(prefix);
const hasTabsPrefix = (path: string) => {
const values = Array.from(tabsPrefixes.values());
const hasPrefix = values.find((v: string) => path.includes(v));
return hasPrefix !== undefined;
}
const getViewStack = (outletId: number) => { const getViewStack = (outletId: number) => {
return viewStacks[outletId]; return viewStacks[outletId];
@ -31,6 +23,19 @@ export const createViewStacks = () => {
return findViewItemByPath(routeInfo.lastPathname, outletId); return findViewItemByPath(routeInfo.lastPathname, outletId);
} }
const findViewItemByMatchedRoute = (matchedRoute: any, outletId: number): ViewItem | undefined => {
const stack = viewStacks[outletId];
if (!stack) return undefined;
return stack.find((viewItem: ViewItem) => {
if (viewItem.matchedRoute.path === matchedRoute.path) {
return viewItem;
}
return undefined;
});
}
const findViewItemInStack = (path: string, stack: ViewItem[]): ViewItem | undefined => { const findViewItemInStack = (path: string, stack: ViewItem[]): ViewItem | undefined => {
return stack.find((viewItem: ViewItem) => { return stack.find((viewItem: ViewItem) => {
if (viewItem.pathname === path) { if (viewItem.pathname === path) {
@ -100,9 +105,8 @@ export const createViewStacks = () => {
} }
return { return {
addTabsPrefix,
hasTabsPrefix,
findViewItemByRouteInfo, findViewItemByRouteInfo,
findViewItemByMatchedRoute,
findLeavingViewItemByRouteInfo, findLeavingViewItemByRouteInfo,
createViewItem, createViewItem,
getChildrenToRender, getChildrenToRender,

View File

@ -19,17 +19,9 @@ export const IonRouterOutlet = defineComponent({
setup(_, { attrs }) { setup(_, { attrs }) {
const route = useRoute(); const route = useRoute();
const depth = inject(viewDepthKey, 0); const depth = inject(viewDepthKey, 0);
// TODO types
let tabsPrefix: string | undefined;
const matchedRouteRef: any = computed(() => { const matchedRouteRef: any = computed(() => {
const matchedRoute = route.matched[depth]; const matchedRoute = route.matched[depth];
if (attrs.tabs && !tabsPrefix) {
tabsPrefix = route.matched[0].path;
viewStacks.addTabsPrefix(tabsPrefix);
}
if (matchedRoute && attrs.tabs && route.matched[depth + 1]) { if (matchedRoute && attrs.tabs && route.matched[depth + 1]) {
return route.matched[route.matched.length - 1]; return route.matched[route.matched.length - 1];
} }
@ -50,9 +42,10 @@ export const IonRouterOutlet = defineComponent({
let skipTransition = false; let skipTransition = false;
watch(matchedRouteRef, () => { // The base url for this router outlet
setupViewItem(matchedRouteRef); let parentOutletPath: string;
});
watch(matchedRouteRef, () => setupViewItem(matchedRouteRef));
const canStart = () => { const canStart = () => {
const stack = viewStacks.getViewStack(id); const stack = viewStacks.getViewStack(id);
@ -252,20 +245,46 @@ export const IonRouterOutlet = defineComponent({
components.value = viewStacks.getChildrenToRender(id); components.value = viewStacks.getChildrenToRender(id);
} }
// TODO types
const setupViewItem = (matchedRouteRef: any) => { const setupViewItem = (matchedRouteRef: any) => {
if (!matchedRouteRef.value) { const firstMatchedRoute = route.matched[0];
if (!parentOutletPath) {
parentOutletPath = firstMatchedRoute.path;
}
/**
* If no matched route, do not do anything in this outlet.
* If there is a match, but it the first matched path
* is not the root path for this outlet, then this view
* change needs to be rendered in a different outlet.
* We also add an exception for when the matchedRouteRef is
* equal to the first matched route (i.e. the base router outlet).
* This logic is mainly to help nested outlets/multi-tab
* setups work better.
*/
if (
!matchedRouteRef.value ||
(matchedRouteRef.value !== firstMatchedRoute && firstMatchedRoute.path !== parentOutletPath)
) {
return; return;
} }
const currentRoute = ionRouter.getCurrentRouteInfo(); const currentRoute = ionRouter.getCurrentRouteInfo();
const hasTabsPrefix = viewStacks.hasTabsPrefix(currentRoute.pathname)
const isLastPathTabs = viewStacks.hasTabsPrefix(currentRoute.lastPathname);
if (hasTabsPrefix && isLastPathTabs && !attrs.tabs) { return; }
let enteringViewItem = viewStacks.findViewItemByRouteInfo(currentRoute, id); let enteringViewItem = viewStacks.findViewItemByRouteInfo(currentRoute, id);
if (!enteringViewItem) { if (!enteringViewItem) {
/**
* If we have no existing entering item, we need
* make sure that there is no existing view according to the
* matched route rather than what is in the url bar.
* This is mainly for tabs when outlet 1 renders ion-tabs
* and outlet 2 renders the individual tab view. We don't
* want outlet 1 creating a new ion-tabs instance every time
* we switch tabs.
*/
if (viewStacks.findViewItemByMatchedRoute(matchedRouteRef.value, id)) {
return;
}
enteringViewItem = viewStacks.createViewItem(id, matchedRouteRef.value.components.default, matchedRouteRef.value, currentRoute); enteringViewItem = viewStacks.createViewItem(id, matchedRouteRef.value.components.default, matchedRouteRef.value, currentRoute);
viewStacks.add(enteringViewItem); viewStacks.add(enteringViewItem);
} }

View File

@ -1306,27 +1306,27 @@
} }
}, },
"@ionic/core": { "@ionic/core": {
"version": "5.4.0-dev.202010081857.bfc0b25", "version": "5.4.0-rc.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0-dev.202010081857.bfc0b25.tgz", "resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0-rc.1.tgz",
"integrity": "sha512-yCH1GTlTTTS3dt9kYk/y5K82UdKOLidRqziGTsFeOoqTJI2w87SgRx0V0Il92dcB1XaWPB/SNnaM7Tt+GD7+Lg==", "integrity": "sha512-8/Mcz86GZEA8Q9x86MOpWU+v4PQBmMfjja1LNVlGN+OIh2oIVBbY6EL6+dw9o6lcHoMY4bUHHpgyPvpYDNXfJw==",
"requires": { "requires": {
"ionicons": "^5.1.2", "ionicons": "^5.1.2",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@ionic/vue": { "@ionic/vue": {
"version": "5.4.0-dev.202010081857.bfc0b25", "version": "5.4.0-rc.1",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-5.4.0-dev.202010081857.bfc0b25.tgz", "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-5.4.0-rc.1.tgz",
"integrity": "sha512-A2s0skOjoytlwC2xJVo+jmv6ZcYKV+HAaGitqKs08bPHATFDh+yzpJLXRkS0poZ3N2Bk4ossXsd5gn4eXC7cKw==", "integrity": "sha512-DqlZIE61Awm3D1jNX6ADbGcfQWKhRZty1EZVjnJ9xPnPLm4h/yKUU2jRUwfN5ia7pYIvKoz+FAqwBgGxO4G8UQ==",
"requires": { "requires": {
"@ionic/core": "5.4.0-dev.202010081857.bfc0b25", "@ionic/core": "5.4.0-rc.1",
"ionicons": "^5.1.2" "ionicons": "^5.1.2"
} }
}, },
"@ionic/vue-router": { "@ionic/vue-router": {
"version": "5.4.0-dev.202010081857.bfc0b25", "version": "5.4.0-rc.1",
"resolved": "https://registry.npmjs.org/@ionic/vue-router/-/vue-router-5.4.0-dev.202010081857.bfc0b25.tgz", "resolved": "https://registry.npmjs.org/@ionic/vue-router/-/vue-router-5.4.0-rc.1.tgz",
"integrity": "sha512-qlFRY32CuttCZHqFFTZoDOERFFLDursJvMeb75KjE5lO/gD+dbelb7Cn0dOyaKyrM06X/lwb8eXgjCwSsGbHxg==" "integrity": "sha512-Yq+yPJdaCdHZTEefes7ry9fEUyMkxPvxPjzc/e4vgDz1a3/UISM3SGfxP4qRv2RtRs5nbPv0tb1DZuPbMOYT9g=="
}, },
"@jest/console": { "@jest/console": {
"version": "24.9.0", "version": "24.9.0",
@ -7691,6 +7691,12 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true "dev": true
}, },
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
"dev": true
},
"qs": { "qs": {
"version": "6.7.0", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
@ -12295,12 +12301,6 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true "dev": true
}, },
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
"dev": true
},
"path-type": { "path-type": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",

View File

@ -12,8 +12,8 @@
"sync": "sh ./scripts/sync.sh" "sync": "sh ./scripts/sync.sh"
}, },
"dependencies": { "dependencies": {
"@ionic/vue": "5.4.0-dev.202010081857.bfc0b25", "@ionic/vue": "^5.4.0-rc.1",
"@ionic/vue-router": "5.4.0-dev.202010081857.bfc0b25", "@ionic/vue-router": "^5.4.0-rc.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"vue": "^3.0.0-0", "vue": "^3.0.0-0",
"vue-router": "^4.0.0-0" "vue-router": "^4.0.0-0"

View File

@ -4,6 +4,11 @@ cp -a ../dist node_modules/@ionic/vue/dist
cp -a ../css node_modules/@ionic/vue/css cp -a ../css node_modules/@ionic/vue/css
cp -a ../package.json node_modules/@ionic/vue/package.json cp -a ../package.json node_modules/@ionic/vue/package.json
# Copy ionic vue router dist
rm -rf node_modules/@ionic/vue-router/dist
cp -a ../../vue-router/dist node_modules/@ionic/vue-router/dist
cp -a ../../vue-router/package.json node_modules/@ionic/vue-router/package.json
# Copy core dist # Copy core dist
rm -rf node_modules/@ionic/core/dist node_modules/@ionic/core/loader rm -rf node_modules/@ionic/core/dist node_modules/@ionic/core/loader
cp -a ../../../core/dist node_modules/@ionic/core/dist cp -a ../../../core/dist node_modules/@ionic/core/dist

View File

@ -85,6 +85,28 @@ const routes: Array<RouteRecordRaw> = [
} }
] ]
}, },
{
path: '/tabs-secondary/',
component: () => import('@/views/TabsSecondary.vue'),
children: [
{
path: '',
redirect: '/tabs-secondary/tab1'
},
{
path: 'tab1',
component: () => import('@/views/Tab1Secondary.vue')
},
{
path: 'tab2',
component: () => import('@/views/Tab2Secondary.vue')
},
{
path: 'tab3',
component: () => import('@/views/Tab3Secondary.vue')
}
]
}
] ]
const router = createRouter({ const router = createRouter({

View File

@ -38,6 +38,9 @@
<ion-item router-link="/tabs" id="tabs"> <ion-item router-link="/tabs" id="tabs">
<ion-label>Tabs</ion-label> <ion-label>Tabs</ion-label>
</ion-item> </ion-item>
<ion-item router-link="/tabs-secondary" id="tab-secondary">
<ion-label>Tabs Secondary</ion-label>
</ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>

View File

@ -17,6 +17,7 @@
</ion-header> </ion-header>
<div class="ion-padding"> <div class="ion-padding">
<ion-button router-link="/tabs/tab1" id="nested-tabs">Tab 1</ion-button>
<ion-button router-link="/nested/two" id="nested-child-two">Nested Child Two</ion-button> <ion-button router-link="/nested/two" id="nested-child-two">Nested Child Two</ion-button>
</div> </div>
</ion-content> </ion-content>

View File

@ -1,5 +1,5 @@
<template> <template>
<ion-page> <ion-page data-pageid="routeroutlet">
<ion-content> <ion-content>
<ion-router-outlet></ion-router-outlet> <ion-router-outlet></ion-router-outlet>
</ion-content> </ion-content>

View File

@ -20,6 +20,17 @@
<ion-item router-link="tab1/child-one" id="child-one"> <ion-item router-link="tab1/child-one" id="child-one">
<ion-label>Go to Tab 1 Child 1</ion-label> <ion-label>Go to Tab 1 Child 1</ion-label>
</ion-item> </ion-item>
<ion-item router-link="/nested" id="nested">
<ion-label>Go to Nested Outlet</ion-label>
</ion-item>
<ion-item router-link="/tabs-secondary" id="tabs-secondary">
<ion-label>Go to Secondary Tabs</ion-label>
</ion-item>
<ion-item router-link="/tabs" id="tabs-primary">
<ion-label>Go to Primary Tabs</ion-label>
</ion-item>
</ion-content> </ion-content>
</ion-page> </ion-page>
</template> </template>

View File

@ -0,0 +1,34 @@
<template>
<ion-page data-pageid="tab1-secondary">
<ion-header>
<ion-toolbar>
<ion-buttons>
<ion-back-button default-href="/"></ion-back-button>
</ion-buttons>
<ion-title>Tab 1 - Secondary</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 1 - Secondary</ion-title>
</ion-toolbar>
</ion-header>
<ExploreContainer name="Tab 1 page" />
<ion-item router-link="/tabs" id="tabs-primary">
<ion-label>Go to Primary Tabs</ion-label>
</ion-item>
</ion-content>
</ion-page>
</template>
<script>
import { IonButtons, IonBackButton, IonPage, IonHeader, IonItem, IonLabel, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import ExploreContainer from '@/components/ExploreContainer.vue';
export default {
components: { IonButtons, IonBackButton, ExploreContainer, IonHeader, IonItem, IonLabel, IonToolbar, IonTitle, IonContent, IonPage }
}
</script>

View File

@ -0,0 +1,27 @@
<template>
<ion-page data-pageid="tab2-secondary">
<ion-header>
<ion-toolbar>
<ion-title>Tab 2 - Secondary</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 2 - Secondary</ion-title>
</ion-toolbar>
</ion-header>
<ExploreContainer name="Tab 2 page" />
</ion-content>
</ion-page>
</template>
<script>
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import ExploreContainer from '@/components/ExploreContainer.vue';
export default {
components: { ExploreContainer, IonHeader, IonToolbar, IonTitle, IonContent, IonPage }
}
</script>

View File

@ -0,0 +1,27 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Tab 3 - Secondary</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 3 - Secondary</ion-title>
</ion-toolbar>
</ion-header>
<ExploreContainer name="Tab 3 page" />
</ion-content>
</ion-page>
</template>
<script>
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import ExploreContainer from '@/components/ExploreContainer.vue';
export default {
components: { ExploreContainer, IonHeader, IonToolbar, IonTitle, IonContent, IonPage }
}
</script>

View File

@ -0,0 +1,41 @@
<template>
<ion-page data-pageid="tabs-secondary">
<ion-content>
<ion-tabs id="tabs">
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1" href="/tabs-secondary/tab1">
<ion-icon :icon="triangle" />
<ion-label>Tab 1</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2" href="/tabs-secondary/tab2">
<ion-icon :icon="ellipse" />
<ion-label>Tab 2</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3" href="/tabs-secondary/tab3">
<ion-icon :icon="square" />
<ion-label>Tab 3</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { IonTabBar, IonTabButton, IonTabs, IonContent, IonLabel, IonIcon, IonPage } from '@ionic/vue';
import { ellipse, square, triangle } from 'ionicons/icons';
export default {
name: 'Tabs',
components: { IonContent, IonLabel, IonTabs, IonTabBar, IonTabButton, IonIcon, IonPage },
setup() {
return {
ellipse,
square,
triangle,
}
}
}
</script>

View File

@ -7,15 +7,29 @@ describe('Nested', () => {
cy.ionPageVisible('nestedchild'); cy.ionPageVisible('nestedchild');
}); });
it.skip('should go to second page', () => { it('should go to second page', () => {
cy.get('#nested-child-two').click(); cy.get('#nested-child-two').click();
cy.ionPageVisible('nestedchildtwo'); cy.ionPageVisible('nestedchildtwo');
cy.ionPageInvisible('nestedchild'); cy.ionPageHidden('nestedchild');
}); });
it.skip('should go back to first page', () => { it('should go back to first page', () => {
cy.get('#nested-child-two').click(); cy.get('#nested-child-two').click();
cy.ionBackClick('nestedchildtwo'); cy.ionBackClick('nestedchildtwo');
cy.ionPageVisible('nestedchild'); cy.ionPageVisible('nestedchild');
}); });
it('should go navigate across nested outlet contexts', () => {
cy.ionPageVisible('nestedchild');
cy.get('#nested-tabs').click();
cy.ionPageHidden('routeroutlet');
cy.ionPageVisible('tab1');
cy.ionBackClick('tab1');
cy.ionPageDoesNotExist('tab1');
cy.ionPageVisible('routeroutlet');
});
}) })

View File

@ -36,6 +36,21 @@ describe('Tabs', () => {
cy.get('ion-tab-button#tab-button-tab1').click(); cy.get('ion-tab-button#tab-button-tab1').click();
}); });
it('should go to correct tab when going back via browser', () => {
cy.visit('http://localhost:8080/tabs')
cy.get('#child-one').click();
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageVisible('tab2');
cy.ionPageHidden('tab1childone');
cy.go('back');
cy.ionPageVisible('tab1childone');
cy.ionPageHidden('tab1');
});
// TODO this does not work // TODO this does not work
it.skip('should return to tab root when clicking tab button', () => { it.skip('should return to tab root when clicking tab button', () => {
cy.visit('http://localhost:8080/tabs') cy.visit('http://localhost:8080/tabs')
@ -134,3 +149,30 @@ describe('Tabs - Swipe to Go Back', () => {
cy.ionPageVisible('home'); cy.ionPageVisible('home');
}); });
}) })
describe('Multi Tabs', () => {
it('should navigate to multiple tabs instances', () => {
cy.visit('http://localhost:8080/tabs')
cy.get('#tabs-secondary').click();
cy.ionPageHidden('tabs');
cy.ionPageVisible('tabs-secondary');
cy.get('[data-pageid="tab1-secondary"] #tabs-primary').click();
cy.ionPageHidden('tabs-secondary');
cy.ionPageVisible('tabs');
cy.ionBackClick('tab1');
cy.ionPageVisible('tabs-secondary');
cy.ionPageDoesNotExist('tabs');
cy.ionBackClick('tab1-secondary');
cy.ionPageVisible('tabs');
cy.ionPageDoesNotExist('tabs-secondary');
cy.ionBackClick('tab1');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('tabs');
});
})