mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +08:00
refactor(vue): remove support for child routes nested inside of tabs (#22919)
BREAKING CHANGE: Support for child routes nested inside of tabs has been removed to better conform to Vue Router's best practices. Additional routes should be written as sibling routes with the parent tab as the path prefix.
This commit is contained in:
84
BREAKING.md
84
BREAKING.md
@ -21,6 +21,8 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
|||||||
* [Transition Shadow](#transition-shadow)
|
* [Transition Shadow](#transition-shadow)
|
||||||
- [Angular](#angular)
|
- [Angular](#angular)
|
||||||
* [Config Provider](#config-provider)
|
* [Config Provider](#config-provider)
|
||||||
|
- [Vue](#vue)
|
||||||
|
* [Tabs Config](#tabs-config)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -73,6 +75,88 @@ The `experimentalTransitionShadow` config option has been removed. The transitio
|
|||||||
The `Config.set()` method has been removed. See https://ionicframework.com/docs/angular/config for examples on how to set config globally, per-component, and per-platform.
|
The `Config.set()` method has been removed. See https://ionicframework.com/docs/angular/config for examples on how to set config globally, per-component, and per-platform.
|
||||||
|
|
||||||
|
|
||||||
|
### Vue
|
||||||
|
|
||||||
|
#### Tabs Config
|
||||||
|
|
||||||
|
Support for child routes nested inside of tabs has been removed to better conform to Vue Router's best practices. Additional routes should be written as sibling routes with the parent tab as the path prefix:
|
||||||
|
|
||||||
|
**Old**
|
||||||
|
```typescript
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/tabs/tab1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tabs/',
|
||||||
|
component: Tabs,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: 'tab1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab1',
|
||||||
|
component: () => import('@/views/Tab1.vue'),
|
||||||
|
children: {
|
||||||
|
{
|
||||||
|
path: 'view',
|
||||||
|
component: () => import('@/views/Tab1View.vue')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab2',
|
||||||
|
component: () => import('@/views/Tab2.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab3',
|
||||||
|
component: () => import('@/views/Tab3.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**New**
|
||||||
|
```typescript
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/tabs/tab1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tabs/',
|
||||||
|
component: Tabs,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: 'tab1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab1',
|
||||||
|
component: () => import('@/views/Tab1.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab1/view',
|
||||||
|
component: () => import('@/views/Tab1View.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab2',
|
||||||
|
component: () => import('@/views/Tab2.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tab3',
|
||||||
|
component: () => import('@/views/Tab3.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above `tabs/tab1/view` has been rewritten has a sibling route to `tabs/tab1`. The `path` field now includes the `tab1` prefix.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Version 5.x
|
## Version 5.x
|
||||||
|
@ -22,16 +22,16 @@ export const createViewStacks = (router: Router) => {
|
|||||||
viewItem.ionRoute = true;
|
viewItem.ionRoute = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number, useDeprecatedRouteSetup: boolean = false) => {
|
const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => {
|
||||||
return findViewItemByPath(routeInfo.pathname, outletId, false, useDeprecatedRouteSetup);
|
return findViewItemByPath(routeInfo.pathname, outletId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number, mustBeIonRoute: boolean = true, useDeprecatedRouteSetup: boolean = false) => {
|
const findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number, mustBeIonRoute: boolean = true) => {
|
||||||
return findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute, useDeprecatedRouteSetup);
|
return findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
const findViewItemByPathname = (pathname: string, outletId?: number, useDeprecatedRouteSetup: boolean = false) => {
|
const findViewItemByPathname = (pathname: string, outletId?: number) => {
|
||||||
return findViewItemByPath(pathname, outletId, false, useDeprecatedRouteSetup);
|
return findViewItemByPath(pathname, outletId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const findViewItemInStack = (path: string, stack: ViewItem[]): ViewItem | undefined => {
|
const findViewItemInStack = (path: string, stack: ViewItem[]): ViewItem | undefined => {
|
||||||
@ -44,7 +44,7 @@ export const createViewStacks = (router: Router) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const findViewItemByPath = (path: string, outletId?: number, mustBeIonRoute: boolean = false, useDeprecatedRouteSetup: boolean = false): ViewItem | undefined => {
|
const findViewItemByPath = (path: string, outletId?: number, mustBeIonRoute: boolean = false): ViewItem | undefined => {
|
||||||
const matchView = (viewItem: ViewItem) => {
|
const matchView = (viewItem: ViewItem) => {
|
||||||
if (
|
if (
|
||||||
(mustBeIonRoute && !viewItem.ionRoute) ||
|
(mustBeIonRoute && !viewItem.ionRoute) ||
|
||||||
@ -54,13 +54,7 @@ export const createViewStacks = (router: Router) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = router.resolve(path);
|
const resolvedPath = router.resolve(path);
|
||||||
let findMatchedRoute;
|
const findMatchedRoute = resolvedPath.matched.find((matchedRoute: RouteLocationMatched) => matchedRoute === viewItem.matchedRoute);
|
||||||
// TODO: Remove in Ionic Vue v6.0
|
|
||||||
if (useDeprecatedRouteSetup) {
|
|
||||||
findMatchedRoute = resolvedPath.matched.find((matchedRoute: RouteLocationMatched) => matchedRoute === viewItem.matchedRoute && (path === viewItem.pathname || matchedRoute.path.includes(':')));
|
|
||||||
} else {
|
|
||||||
findMatchedRoute = resolvedPath.matched.find((matchedRoute: RouteLocationMatched) => matchedRoute === viewItem.matchedRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (findMatchedRoute) {
|
if (findMatchedRoute) {
|
||||||
return viewItem;
|
return viewItem;
|
||||||
|
@ -17,26 +17,11 @@ import { fireLifecycle, generateId, getConfig } from '../utils';
|
|||||||
let viewDepthKey: InjectionKey<0> = Symbol(0);
|
let viewDepthKey: InjectionKey<0> = Symbol(0);
|
||||||
export const IonRouterOutlet = defineComponent({
|
export const IonRouterOutlet = defineComponent({
|
||||||
name: 'IonRouterOutlet',
|
name: 'IonRouterOutlet',
|
||||||
setup(_, { attrs }) {
|
setup() {
|
||||||
const injectedRoute = inject(routeLocationKey)!;
|
const injectedRoute = inject(routeLocationKey)!;
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const depth = inject(viewDepthKey, 0);
|
const depth = inject(viewDepthKey, 0);
|
||||||
let usingDeprecatedRouteSetup = false;
|
const matchedRouteRef: any = computed(() => route.matched[depth]);
|
||||||
|
|
||||||
// TODO: Remove in Ionic Vue v6.0
|
|
||||||
if (attrs.tabs && route.matched[depth]?.children?.length > 0) {
|
|
||||||
console.warn('[@ionic/vue Deprecation]: Your child routes are nested inside of each tab in your routing config. This format will not be supported in Ionic Vue v6.0. Instead, write your child routes as sibling routes. See https://ionicframework.com/docs/vue/navigation#child-routes-within-tabs for more information.');
|
|
||||||
usingDeprecatedRouteSetup = true;
|
|
||||||
}
|
|
||||||
const matchedRouteRef: any = computed(() => {
|
|
||||||
const matchedRoute = route.matched[depth];
|
|
||||||
|
|
||||||
if (matchedRoute && attrs.tabs && route.matched[depth + 1] && usingDeprecatedRouteSetup) {
|
|
||||||
return route.matched[route.matched.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchedRoute;
|
|
||||||
});
|
|
||||||
|
|
||||||
provide(viewDepthKey, depth + 1)
|
provide(viewDepthKey, depth + 1)
|
||||||
provide(matchedRouteKey, matchedRouteRef);
|
provide(matchedRouteKey, matchedRouteRef);
|
||||||
@ -83,15 +68,15 @@ export const IonRouterOutlet = defineComponent({
|
|||||||
* to make sure the view is in the outlet we want.
|
* to make sure the view is in the outlet we want.
|
||||||
*/
|
*/
|
||||||
const routeInfo = ionRouter.getCurrentRouteInfo();
|
const routeInfo = ionRouter.getCurrentRouteInfo();
|
||||||
const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id, usingDeprecatedRouteSetup);
|
const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id);
|
||||||
|
|
||||||
return !!enteringViewItem;
|
return !!enteringViewItem;
|
||||||
}
|
}
|
||||||
const onStart = async () => {
|
const onStart = async () => {
|
||||||
const routeInfo = ionRouter.getCurrentRouteInfo();
|
const routeInfo = ionRouter.getCurrentRouteInfo();
|
||||||
const { routerAnimation } = routeInfo;
|
const { routerAnimation } = routeInfo;
|
||||||
const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id, usingDeprecatedRouteSetup);
|
const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id);
|
||||||
const leavingViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id, usingDeprecatedRouteSetup);
|
const leavingViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id);
|
||||||
|
|
||||||
if (leavingViewItem) {
|
if (leavingViewItem) {
|
||||||
let animationBuilder = routerAnimation;
|
let animationBuilder = routerAnimation;
|
||||||
@ -146,7 +131,7 @@ export const IonRouterOutlet = defineComponent({
|
|||||||
* re-hide the page that was going to enter.
|
* re-hide the page that was going to enter.
|
||||||
*/
|
*/
|
||||||
const routeInfo = ionRouter.getCurrentRouteInfo();
|
const routeInfo = ionRouter.getCurrentRouteInfo();
|
||||||
const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id, usingDeprecatedRouteSetup);
|
const enteringViewItem = viewStacks.findViewItemByRouteInfo({ pathname: routeInfo.pushedByRoute || '' }, id);
|
||||||
enteringViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
enteringViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
||||||
enteringViewItem.ionPageElement.classList.add('ion-page-hidden');
|
enteringViewItem.ionPageElement.classList.add('ion-page-hidden');
|
||||||
}
|
}
|
||||||
@ -201,14 +186,14 @@ export const IonRouterOutlet = defineComponent({
|
|||||||
const routeInfo = ionRouter.getCurrentRouteInfo();
|
const routeInfo = ionRouter.getCurrentRouteInfo();
|
||||||
const { routerDirection, routerAction, routerAnimation, prevRouteLastPathname } = routeInfo;
|
const { routerDirection, routerAction, routerAnimation, prevRouteLastPathname } = routeInfo;
|
||||||
|
|
||||||
const enteringViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id, usingDeprecatedRouteSetup);
|
const enteringViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id);
|
||||||
let leavingViewItem = viewStacks.findLeavingViewItemByRouteInfo(routeInfo, id, true, usingDeprecatedRouteSetup);
|
let leavingViewItem = viewStacks.findLeavingViewItemByRouteInfo(routeInfo, id);
|
||||||
const enteringEl = enteringViewItem.ionPageElement;
|
const enteringEl = enteringViewItem.ionPageElement;
|
||||||
|
|
||||||
if (enteringViewItem === leavingViewItem) return;
|
if (enteringViewItem === leavingViewItem) return;
|
||||||
|
|
||||||
if (!leavingViewItem && prevRouteLastPathname) {
|
if (!leavingViewItem && prevRouteLastPathname) {
|
||||||
leavingViewItem = viewStacks.findViewItemByPathname(prevRouteLastPathname, id, usingDeprecatedRouteSetup);
|
leavingViewItem = viewStacks.findViewItemByPathname(prevRouteLastPathname, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fireLifecycle(enteringViewItem.vueComponent, enteringViewItem.vueComponentRef, LIFECYCLE_WILL_ENTER);
|
fireLifecycle(enteringViewItem.vueComponent, enteringViewItem.vueComponentRef, LIFECYCLE_WILL_ENTER);
|
||||||
@ -303,7 +288,7 @@ export const IonRouterOutlet = defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentRoute = ionRouter.getCurrentRouteInfo();
|
const currentRoute = ionRouter.getCurrentRouteInfo();
|
||||||
let enteringViewItem = viewStacks.findViewItemByRouteInfo(currentRoute, id, usingDeprecatedRouteSetup);
|
let enteringViewItem = viewStacks.findViewItemByRouteInfo(currentRoute, id);
|
||||||
|
|
||||||
if (!enteringViewItem) {
|
if (!enteringViewItem) {
|
||||||
enteringViewItem = viewStacks.createViewItem(id, matchedRouteRef.value.components.default, matchedRouteRef.value, currentRoute);
|
enteringViewItem = viewStacks.createViewItem(id, matchedRouteRef.value.components.default, matchedRouteRef.value, currentRoute);
|
||||||
|
@ -19,7 +19,7 @@ export const IonTabs = defineComponent({
|
|||||||
'contain': 'layout size style'
|
'contain': 'layout size style'
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
h(IonRouterOutlet, { tabs: true })
|
h(IonRouterOutlet)
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -83,41 +83,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '',
|
path: '',
|
||||||
redirect: '/tabs/tab1'
|
redirect: '/tabs/tab1'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'tab1',
|
|
||||||
component: () => import('@/views/Tab1.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'child-one',
|
|
||||||
component: () => import('@/views/Tab1ChildOne.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'child-two',
|
|
||||||
component: () => import('@/views/Tab1ChildTwo.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'tab2',
|
|
||||||
component: () => import('@/views/Tab2.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'tab3',
|
|
||||||
beforeEnter: (to, from, next) => {
|
|
||||||
next({ path: '/tabs/tab1' });
|
|
||||||
},
|
|
||||||
component: () => import('@/views/Tab3.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/tabs-new/',
|
|
||||||
component: () => import('@/views/Tabs.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
redirect: '/tabs-new/tab1'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'tab1',
|
path: 'tab1',
|
||||||
component: () => import('@/views/Tab1.vue'),
|
component: () => import('@/views/Tab1.vue'),
|
||||||
|
@ -190,23 +190,6 @@ describe('Tabs', () => {
|
|||||||
cy.ionPageVisible('tab2');
|
cy.ionPageVisible('tab2');
|
||||||
cy.ionPageVisible('tabs');
|
cy.ionPageVisible('tabs');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verifies 1 of 2 fixes for https://github.com/ionic-team/ionic-framework/issues/22519
|
|
||||||
it('should not create a new tabs instance when switching between tabbed and non-tabbed contexts - new tabs setup', () => {
|
|
||||||
cy.visit('http://localhost:8080/tabs-new/tab1');
|
|
||||||
|
|
||||||
cy.routerPush('/');
|
|
||||||
cy.ionPageHidden('tabs');
|
|
||||||
cy.ionPageVisible('home');
|
|
||||||
|
|
||||||
cy.routerPush('/tabs-new/tab2');
|
|
||||||
cy.ionPageHidden('tab1');
|
|
||||||
|
|
||||||
cy.ionPageHidden('home');
|
|
||||||
|
|
||||||
cy.ionPageVisible('tab2');
|
|
||||||
cy.ionPageVisible('tabs');
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Tabs - Swipe to Go Back', () => {
|
describe('Tabs - Swipe to Go Back', () => {
|
||||||
|
@ -68,8 +68,7 @@ describe('ion-tab-bar', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const innerHTML = wrapper.find('ion-tabs').html();
|
const innerHTML = wrapper.find('ion-tabs').html();
|
||||||
// TODO: Remove tabs="true" in Ionic Vue v6.0
|
expect(innerHTML).toContain(`<div class="tabs-inner" style="position: relative; flex: 1; contain: layout size style;"><ion-router-outlet></ion-router-outlet></div><ion-tab-bar slot="bottom"></ion-tab-bar>`);
|
||||||
expect(innerHTML).toContain(`<div class="tabs-inner" style="position: relative; flex: 1; contain: layout size style;"><ion-router-outlet tabs="true"></ion-router-outlet></div><ion-tab-bar slot="bottom"></ion-tab-bar>`);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,8 +100,7 @@ describe('ion-tab-bar', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const innerHTML = wrapper.find('ion-tabs').html();
|
const innerHTML = wrapper.find('ion-tabs').html();
|
||||||
// TODO: Remove tabs="true" in Ionic Vue v6.0
|
expect(innerHTML).toContain(`<div class="tabs-inner" style="position: relative; flex: 1; contain: layout size style;"><ion-router-outlet></ion-router-outlet></div><ion-tab-bar></ion-tab-bar></ion-tabs>`)
|
||||||
expect(innerHTML).toContain(`<div class="tabs-inner" style="position: relative; flex: 1; contain: layout size style;"><ion-router-outlet tabs="true"></ion-router-outlet></div><ion-tab-bar></ion-tab-bar></ion-tabs>`)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verifies the fix for https://github.com/ionic-team/ionic-framework/issues/22642
|
// Verifies the fix for https://github.com/ionic-team/ionic-framework/issues/22642
|
||||||
|
Reference in New Issue
Block a user