Skip to content

Commit 5bc4399

Browse files
authored
fix(vue): tabs and parameterized routes work with latest vue (#28846)
Issue number: resolves #28774 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> There are two issues causing Ionic Vue apps to not behave as intended with certain versions of Vue: 1. In Vue 3.3 a [breaking change shipped](vuejs/core#9916) that changes the default behavior of the `watch` inside of IonRouterOutlet to be a shallow watcher instead of a deep watcher. This caused the router outlet to not consistent re-render. While the change was later reverted by the Vue team, they expressed that the change [may re-land in a future minor release](vuejs/core#9965 (comment)). As a result, we will need to account for this inside of Ionic. 2. In Vue 3.2 a [custom elements improvement shipped](https://github.com/vuejs/core/blob/main/changelogs/CHANGELOG-3.2.md#3238-2022-08-30) that changed how custom elements are referred to in VNodes. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - The affected `watch` call now is now explicitly a deep watcher. This change is backwards compatible as well as forward compatible with upcoming Vue changes. - Updated IonTabs to account for the new VNode behavior for custom elements. Ionic still supports version of Vue that do not have this improvement, so we need to account for both behaviors for now. I also added a tech debt ticket to remove the old checks when we drop support for older versions of Vue. - Updated E2E test dependencies. During this update some of our tests needed to be updated to account for newer versions of Vue/Vitest. Overall I was able to simplify a lot of our tests as a result. I plan to add renovatebot to these E2E test apps, but I will handle that in a separate PR. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 4. Update the BREAKING.md file with the breaking change. 5. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev build: `7.6.6-dev.11705526292.1bc0acb5` Note: Both of the issues cause tests to fail when using the latest dependencies in the Vue E2E test app. However, I need to use the latest dependencies so I can demonstrate that my changes do fix the reported issues. As a result, I have both fixes in the same PR.
1 parent 9262f7d commit 5bc4399

File tree

10 files changed

+81
-93
lines changed

10 files changed

+81
-93
lines changed

packages/vue/src/components/IonRouterOutlet.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,15 @@ export const IonRouterOutlet = /*@__PURE__*/ defineComponent({
115115

116116
previousMatchedRouteRef = currentMatchedRouteRef;
117117
previousMatchedPath = currentRoute.path;
118-
}
118+
},
119+
/**
120+
* Future versions of Vue may default watching nested
121+
* reactive objects to "deep: false".
122+
* We explicitly set this watcher to "deep: true" to
123+
* account for that.
124+
* https://github.com/vuejs/core/issues/9965#issuecomment-1875067499
125+
*/
126+
{ deep: true }
119127
);
120128

121129
const canStart = () => {

packages/vue/src/components/IonTabs.ts

+27-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ const DID_CHANGE = "ionTabsDidChange";
66

77
// TODO(FW-2969): types
88

9+
/**
10+
* Vue 3.2.38 fixed an issue where Web Component
11+
* names are respected using kebab case instead of pascal case.
12+
* As a result, we need to account for both here since we support
13+
* versions of Vue < 3.2.38.
14+
*/
15+
// TODO FW-5904
16+
const isRouterOutlet = (node: VNode) => {
17+
return (
18+
node.type &&
19+
((node.type as any).name === "IonRouterOutlet" ||
20+
node.type === "ion-router-outlet")
21+
);
22+
};
23+
24+
const isTabBar = (node: VNode) => {
25+
return (
26+
node.type &&
27+
((node.type as any).name === "IonTabBar" || node.type === "ion-tab-bar")
28+
);
29+
};
30+
931
export const IonTabs = /*@__PURE__*/ defineComponent({
1032
name: "IonTabs",
1133
emits: [WILL_CHANGE, DID_CHANGE],
@@ -19,9 +41,8 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
1941
* inside of ion-tabs.
2042
*/
2143
if (slottedContent && slottedContent.length > 0) {
22-
routerOutlet = slottedContent.find(
23-
(child: VNode) =>
24-
child.type && (child.type as any).name === "IonRouterOutlet"
44+
routerOutlet = slottedContent.find((child: VNode) =>
45+
isRouterOutlet(child)
2546
);
2647
}
2748

@@ -57,13 +78,11 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
5778
* since that needs to be inside of `.tabs-inner`.
5879
*/
5980
const filteredContent = slottedContent.filter(
60-
(child: VNode) =>
61-
!child.type ||
62-
(child.type && (child.type as any).name !== "IonRouterOutlet")
81+
(child: VNode) => !child.type || !isRouterOutlet(child)
6382
);
6483

65-
const slottedTabBar = filteredContent.find(
66-
(child: VNode) => child.type && (child.type as any).name === "IonTabBar"
84+
const slottedTabBar = filteredContent.find((child: VNode) =>
85+
isTabBar(child)
6786
);
6887
const hasTopSlotTabBar =
6988
slottedTabBar && slottedTabBar.props?.slot === "top";

packages/vue/test/apps/vue3/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"@ionic/vue": "^6.0.12",
2020
"@ionic/vue-router": "^6.0.12",
2121
"ionicons": "^6.0.4",
22-
"vue": "^3.2.31",
23-
"vue-router": "^4.0.14"
22+
"vue": "^3.4.14",
23+
"vue-router": "^4.2.5"
2424
},
2525
"devDependencies": {
2626
"@typescript-eslint/eslint-plugin": "^5.4.0",
@@ -35,7 +35,7 @@
3535
"jsdom": "^20.0.0",
3636
"typescript": "~4.5.5",
3737
"vite": "^3.1.4",
38-
"vitest": "^0.23.4",
38+
"vitest": "^1.2.1",
3939
"wait-on": "^5.3.0"
4040
},
4141
"engines": {

packages/vue/test/base/tests/unit/hooks.spec.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { mount } from '@vue/test-utils';
22
import { describe, expect, it } from 'vitest';
33
import { createRouter, createWebHistory } from '@ionic/vue-router';
4-
import { IonicVue, IonApp, IonRouterOutlet, IonPage, useIonRouter, createAnimation } from '@ionic/vue';
4+
import { IonicVue, IonRouterOutlet, IonPage, useIonRouter } from '@ionic/vue';
55
import { mockAnimation, waitForRouter } from './utils';
66

7-
const App = {
8-
components: { IonApp, IonRouterOutlet },
9-
template: '<ion-app><ion-router-outlet /></ion-app>',
10-
}
11-
127
const BasePage = {
138
template: '<ion-page></ion-page>',
149
components: { IonPage },
@@ -40,7 +35,7 @@ describe('useIonRouter', () => {
4035

4136
router.push('/');
4237
await router.isReady();
43-
const wrapper = mount(App, {
38+
const wrapper = mount(IonRouterOutlet, {
4439
global: {
4540
plugins: [router, IonicVue]
4641
}
@@ -85,7 +80,7 @@ describe('useIonRouter', () => {
8580

8681
router.push('/');
8782
await router.isReady();
88-
const wrapper = mount(App, {
83+
const wrapper = mount(IonRouterOutlet, {
8984
global: {
9085
plugins: [router, IonicVue]
9186
}
@@ -137,7 +132,7 @@ describe('useIonRouter', () => {
137132

138133
router.push('/');
139134
await router.isReady();
140-
const wrapper = mount(App, {
135+
const wrapper = mount(IonRouterOutlet, {
141136
global: {
142137
plugins: [router, IonicVue]
143138
}
@@ -181,7 +176,7 @@ describe('useIonRouter', () => {
181176

182177
router.push('/');
183178
await router.isReady();
184-
const wrapper = mount(App, {
179+
const wrapper = mount(IonRouterOutlet, {
185180
global: {
186181
plugins: [router, IonicVue]
187182
}
@@ -229,7 +224,7 @@ describe('useIonRouter', () => {
229224

230225
router.push('/');
231226
await router.isReady();
232-
const wrapper = mount(App, {
227+
const wrapper = mount(IonRouterOutlet, {
233228
global: {
234229
plugins: [router, IonicVue]
235230
}

packages/vue/test/base/tests/unit/lifecycle.spec.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import { mount } from '@vue/test-utils';
22
import { describe, expect, it, vi } from 'vitest';
33
import { createRouter, createWebHistory } from '@ionic/vue-router';
4-
import { IonicVue, IonApp, IonRouterOutlet, IonTabs, IonPage } from '@ionic/vue';
4+
import { IonicVue, IonRouterOutlet, IonTabs, IonPage } from '@ionic/vue';
55
import { defineComponent } from 'vue';
66
import { waitForRouter } from './utils';
77

8-
const App = {
9-
components: { IonApp, IonRouterOutlet },
10-
template: '<ion-app><ion-router-outlet /></ion-app>',
11-
}
12-
138
const BasePage = {
149
template: '<ion-page :data-pageid="name"></ion-page>',
1510
components: { IonPage },
@@ -54,7 +49,7 @@ describe('Lifecycle Events', () => {
5449
// Initial render
5550
router.push('/');
5651
await router.isReady();
57-
const wrapper = mount(App, {
52+
const wrapper = mount(IonRouterOutlet, {
5853
global: {
5954
plugins: [router, IonicVue]
6055
}
@@ -108,7 +103,7 @@ describe('Lifecycle Events', () => {
108103
template: `
109104
<ion-page>
110105
<ion-tabs>
111-
<ion-router-outlet></ion-router-outlet>
106+
<IonRouterOutlet />
112107
</ion-tabs>
113108
</ion-page>
114109
`,
@@ -148,7 +143,7 @@ describe('Lifecycle Events', () => {
148143
// Initial render
149144
router.push('/tab1');
150145
await router.isReady();
151-
const wrapper = mount(App, {
146+
const wrapper = mount(IonRouterOutlet, {
152147
global: {
153148
plugins: [router, IonicVue]
154149
}

packages/vue/test/base/tests/unit/page.spec.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { mount } from '@vue/test-utils';
22
import { describe, expect, it } from 'vitest';
33
import { createRouter, createWebHistory } from '@ionic/vue-router';
4-
import { IonicVue, IonApp, IonRouterOutlet, IonPage } from '@ionic/vue';
5-
6-
const App = {
7-
components: { IonApp, IonRouterOutlet },
8-
template: '<ion-app><ion-router-outlet /></ion-app>',
9-
}
4+
import { IonicVue, IonRouterOutlet, IonPage } from '@ionic/vue';
105

116
describe('IonPage', () => {
127
it('should add ion-page class', async () => {
@@ -25,7 +20,7 @@ describe('IonPage', () => {
2520

2621
router.push('/');
2722
await router.isReady();
28-
const wrapper = mount(App, {
23+
const wrapper = mount(IonRouterOutlet, {
2924
global: {
3025
plugins: [router, IonicVue]
3126
}
@@ -50,7 +45,7 @@ describe('IonPage', () => {
5045

5146
router.push('/');
5247
await router.isReady();
53-
const wrapper = mount(App, {
48+
const wrapper = mount(IonRouterOutlet, {
5449
global: {
5550
plugins: [router, IonicVue]
5651
}

packages/vue/test/base/tests/unit/router-outlet.spec.ts

+2-10
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,14 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
33
import { createRouter, createWebHistory } from '@ionic/vue-router';
44
import {
55
IonicVue,
6-
IonApp,
76
IonRouterOutlet,
87
IonPage,
98
useIonRouter,
10-
createAnimation
119
} from '@ionic/vue';
12-
import { onBeforeRouteLeave } from 'vue-router';
1310
import { mockAnimation, waitForRouter } from './utils';
1411

1512
enableAutoUnmount(afterEach);
1613

17-
const App = {
18-
components: { IonApp, IonRouterOutlet },
19-
template: '<ion-app><ion-router-outlet /></ion-app>',
20-
}
21-
2214
const BasePage = {
2315
template: '<ion-page :data-pageid="name"></ion-page>',
2416
components: { IonPage },
@@ -60,7 +52,7 @@ describe('Routing', () => {
6052

6153
router.push('/');
6254
await router.isReady();
63-
const wrapper = mount(App, {
55+
const wrapper = mount(IonRouterOutlet, {
6456
global: {
6557
plugins: [router, IonicVue]
6658
}
@@ -122,7 +114,7 @@ describe('Routing', () => {
122114

123115
router.push('/');
124116
await router.isReady();
125-
const wrapper = mount(App, {
117+
const wrapper = mount(IonRouterOutlet, {
126118
global: {
127119
plugins: [router, IonicVue]
128120
}

0 commit comments

Comments
 (0)