diff --git a/packages/vue-router/src/viewStacks.ts b/packages/vue-router/src/viewStacks.ts index 7479b2ebfa..9e8ac1db03 100644 --- a/packages/vue-router/src/viewStacks.ts +++ b/packages/vue-router/src/viewStacks.ts @@ -6,14 +6,6 @@ import { RouteInfo, export const createViewStacks = () => { 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) => { return viewStacks[outletId]; @@ -31,6 +23,19 @@ export const createViewStacks = () => { 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 => { return stack.find((viewItem: ViewItem) => { if (viewItem.pathname === path) { @@ -100,9 +105,8 @@ export const createViewStacks = () => { } return { - addTabsPrefix, - hasTabsPrefix, findViewItemByRouteInfo, + findViewItemByMatchedRoute, findLeavingViewItemByRouteInfo, createViewItem, getChildrenToRender, diff --git a/packages/vue/src/components/IonRouterOutlet.ts b/packages/vue/src/components/IonRouterOutlet.ts index 77d164b5e6..be923f6d73 100644 --- a/packages/vue/src/components/IonRouterOutlet.ts +++ b/packages/vue/src/components/IonRouterOutlet.ts @@ -19,17 +19,9 @@ export const IonRouterOutlet = defineComponent({ setup(_, { attrs }) { const route = useRoute(); const depth = inject(viewDepthKey, 0); - - // TODO types - let tabsPrefix: string | undefined; const matchedRouteRef: any = computed(() => { 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]) { return route.matched[route.matched.length - 1]; } @@ -50,9 +42,10 @@ export const IonRouterOutlet = defineComponent({ let skipTransition = false; - watch(matchedRouteRef, () => { - setupViewItem(matchedRouteRef); - }); + // The base url for this router outlet + let parentOutletPath: string; + + watch(matchedRouteRef, () => setupViewItem(matchedRouteRef)); const canStart = () => { const stack = viewStacks.getViewStack(id); @@ -252,20 +245,46 @@ export const IonRouterOutlet = defineComponent({ components.value = viewStacks.getChildrenToRender(id); } - // TODO types const setupViewItem = (matchedRouteRef: any) => { - if (!matchedRouteRef.value) { - return; + 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; } 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); 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); viewStacks.add(enteringViewItem); } diff --git a/packages/vue/test-app/package-lock.json b/packages/vue/test-app/package-lock.json index 4369858b2d..f268abef42 100644 --- a/packages/vue/test-app/package-lock.json +++ b/packages/vue/test-app/package-lock.json @@ -1306,27 +1306,27 @@ } }, "@ionic/core": { - "version": "5.4.0-dev.202010081857.bfc0b25", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0-dev.202010081857.bfc0b25.tgz", - "integrity": "sha512-yCH1GTlTTTS3dt9kYk/y5K82UdKOLidRqziGTsFeOoqTJI2w87SgRx0V0Il92dcB1XaWPB/SNnaM7Tt+GD7+Lg==", + "version": "5.4.0-rc.1", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0-rc.1.tgz", + "integrity": "sha512-8/Mcz86GZEA8Q9x86MOpWU+v4PQBmMfjja1LNVlGN+OIh2oIVBbY6EL6+dw9o6lcHoMY4bUHHpgyPvpYDNXfJw==", "requires": { "ionicons": "^5.1.2", "tslib": "^1.10.0" } }, "@ionic/vue": { - "version": "5.4.0-dev.202010081857.bfc0b25", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-5.4.0-dev.202010081857.bfc0b25.tgz", - "integrity": "sha512-A2s0skOjoytlwC2xJVo+jmv6ZcYKV+HAaGitqKs08bPHATFDh+yzpJLXRkS0poZ3N2Bk4ossXsd5gn4eXC7cKw==", + "version": "5.4.0-rc.1", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-5.4.0-rc.1.tgz", + "integrity": "sha512-DqlZIE61Awm3D1jNX6ADbGcfQWKhRZty1EZVjnJ9xPnPLm4h/yKUU2jRUwfN5ia7pYIvKoz+FAqwBgGxO4G8UQ==", "requires": { - "@ionic/core": "5.4.0-dev.202010081857.bfc0b25", + "@ionic/core": "5.4.0-rc.1", "ionicons": "^5.1.2" } }, "@ionic/vue-router": { - "version": "5.4.0-dev.202010081857.bfc0b25", - "resolved": "https://registry.npmjs.org/@ionic/vue-router/-/vue-router-5.4.0-dev.202010081857.bfc0b25.tgz", - "integrity": "sha512-qlFRY32CuttCZHqFFTZoDOERFFLDursJvMeb75KjE5lO/gD+dbelb7Cn0dOyaKyrM06X/lwb8eXgjCwSsGbHxg==" + "version": "5.4.0-rc.1", + "resolved": "https://registry.npmjs.org/@ionic/vue-router/-/vue-router-5.4.0-rc.1.tgz", + "integrity": "sha512-Yq+yPJdaCdHZTEefes7ry9fEUyMkxPvxPjzc/e4vgDz1a3/UISM3SGfxP4qRv2RtRs5nbPv0tb1DZuPbMOYT9g==" }, "@jest/console": { "version": "24.9.0", @@ -7691,6 +7691,12 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "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": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -12295,12 +12301,6 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", diff --git a/packages/vue/test-app/package.json b/packages/vue/test-app/package.json index 63abd67fa2..cd8e70300b 100644 --- a/packages/vue/test-app/package.json +++ b/packages/vue/test-app/package.json @@ -12,8 +12,8 @@ "sync": "sh ./scripts/sync.sh" }, "dependencies": { - "@ionic/vue": "5.4.0-dev.202010081857.bfc0b25", - "@ionic/vue-router": "5.4.0-dev.202010081857.bfc0b25", + "@ionic/vue": "^5.4.0-rc.1", + "@ionic/vue-router": "^5.4.0-rc.1", "core-js": "^3.6.5", "vue": "^3.0.0-0", "vue-router": "^4.0.0-0" diff --git a/packages/vue/test-app/scripts/sync.sh b/packages/vue/test-app/scripts/sync.sh index d93d11d384..cc057dd5ad 100644 --- a/packages/vue/test-app/scripts/sync.sh +++ b/packages/vue/test-app/scripts/sync.sh @@ -4,6 +4,11 @@ cp -a ../dist node_modules/@ionic/vue/dist cp -a ../css node_modules/@ionic/vue/css 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 rm -rf node_modules/@ionic/core/dist node_modules/@ionic/core/loader cp -a ../../../core/dist node_modules/@ionic/core/dist diff --git a/packages/vue/test-app/src/router/index.ts b/packages/vue/test-app/src/router/index.ts index be47e97907..19d4ac6893 100644 --- a/packages/vue/test-app/src/router/index.ts +++ b/packages/vue/test-app/src/router/index.ts @@ -85,6 +85,28 @@ const routes: Array = [ } ] }, + { + 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({ diff --git a/packages/vue/test-app/src/views/Home.vue b/packages/vue/test-app/src/views/Home.vue index fa10900a60..ee3077b92d 100644 --- a/packages/vue/test-app/src/views/Home.vue +++ b/packages/vue/test-app/src/views/Home.vue @@ -38,6 +38,9 @@ Tabs + + Tabs Secondary + diff --git a/packages/vue/test-app/src/views/NestedChild.vue b/packages/vue/test-app/src/views/NestedChild.vue index fa3423ded0..dfd890507b 100644 --- a/packages/vue/test-app/src/views/NestedChild.vue +++ b/packages/vue/test-app/src/views/NestedChild.vue @@ -17,6 +17,7 @@
+ Tab 1 Nested Child Two
diff --git a/packages/vue/test-app/src/views/RouterOutlet.vue b/packages/vue/test-app/src/views/RouterOutlet.vue index b917045ead..5057301adf 100644 --- a/packages/vue/test-app/src/views/RouterOutlet.vue +++ b/packages/vue/test-app/src/views/RouterOutlet.vue @@ -1,5 +1,5 @@ diff --git a/packages/vue/test-app/src/views/Tab1Secondary.vue b/packages/vue/test-app/src/views/Tab1Secondary.vue new file mode 100644 index 0000000000..e4c344af51 --- /dev/null +++ b/packages/vue/test-app/src/views/Tab1Secondary.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/vue/test-app/src/views/Tab2Secondary.vue b/packages/vue/test-app/src/views/Tab2Secondary.vue new file mode 100644 index 0000000000..4d4322a0de --- /dev/null +++ b/packages/vue/test-app/src/views/Tab2Secondary.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/vue/test-app/src/views/Tab3Secondary.vue b/packages/vue/test-app/src/views/Tab3Secondary.vue new file mode 100644 index 0000000000..ae5cc438d4 --- /dev/null +++ b/packages/vue/test-app/src/views/Tab3Secondary.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/vue/test-app/src/views/TabsSecondary.vue b/packages/vue/test-app/src/views/TabsSecondary.vue new file mode 100644 index 0000000000..fcc2d81253 --- /dev/null +++ b/packages/vue/test-app/src/views/TabsSecondary.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/vue/test-app/tests/e2e/specs/nested.js b/packages/vue/test-app/tests/e2e/specs/nested.js index be7a95c9f0..5e6f73cbe3 100644 --- a/packages/vue/test-app/tests/e2e/specs/nested.js +++ b/packages/vue/test-app/tests/e2e/specs/nested.js @@ -7,15 +7,29 @@ describe('Nested', () => { cy.ionPageVisible('nestedchild'); }); - it.skip('should go to second page', () => { + it('should go to second page', () => { cy.get('#nested-child-two').click(); 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.ionBackClick('nestedchildtwo'); 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'); + }); }) diff --git a/packages/vue/test-app/tests/e2e/specs/tabs.js b/packages/vue/test-app/tests/e2e/specs/tabs.js index f37d5bfc25..5227d25270 100644 --- a/packages/vue/test-app/tests/e2e/specs/tabs.js +++ b/packages/vue/test-app/tests/e2e/specs/tabs.js @@ -36,6 +36,21 @@ describe('Tabs', () => { 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 it.skip('should return to tab root when clicking tab button', () => { cy.visit('http://localhost:8080/tabs') @@ -134,3 +149,30 @@ describe('Tabs - Swipe to Go Back', () => { 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'); + }); +})