Skip to content

Commit 3b01cdc

Browse files
authored
fix(tabs, tab-bar): use standalone tab bar in Vue (#29925)
1 parent bbcbf5c commit 3b01cdc

File tree

2 files changed

+88
-56
lines changed

2 files changed

+88
-56
lines changed

packages/vue/src/components/IonTabBar.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCustomElement } from "@ionic/core/components/ion-tab-bar.js";
2-
import type { VNode } from "vue";
2+
import type { VNode, Ref } from "vue";
33
import { h, defineComponent, getCurrentInstance, inject } from "vue";
44

55
// TODO(FW-2969): types
@@ -16,6 +16,12 @@ interface Tab {
1616
ref: VNode;
1717
}
1818

19+
interface TabBarData {
20+
hasRouterOutlet: boolean;
21+
_tabsWillChange: Function;
22+
_tabsDidChange: Function;
23+
}
24+
1925
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
2026

2127
const getTabs = (nodes: VNode[]) => {
@@ -34,28 +40,31 @@ const getTabs = (nodes: VNode[]) => {
3440

3541
export const IonTabBar = defineComponent({
3642
name: "IonTabBar",
37-
props: {
38-
/* eslint-disable @typescript-eslint/no-empty-function */
39-
_tabsWillChange: { type: Function, default: () => {} },
40-
_tabsDidChange: { type: Function, default: () => {} },
41-
_hasRouterOutlet: { type: Boolean, default: false },
42-
/* eslint-enable @typescript-eslint/no-empty-function */
43-
},
4443
data() {
4544
return {
4645
tabState: {
4746
activeTab: undefined,
4847
tabs: {},
48+
/**
49+
* Passing this prop to each tab button
50+
* lets it be aware of the presence of
51+
* the router outlet.
52+
*/
53+
hasRouterOutlet: false,
4954
},
5055
tabVnodes: [],
56+
/* eslint-disable @typescript-eslint/no-empty-function */
57+
_tabsWillChange: { type: Function, default: () => {} },
58+
_tabsDidChange: { type: Function, default: () => {} },
59+
/* eslint-enable @typescript-eslint/no-empty-function */
5160
};
5261
},
5362
updated() {
5463
this.setupTabState(inject("navManager", null));
5564
},
5665
methods: {
5766
setupTabState(ionRouter: any) {
58-
const hasRouterOutlet = this.$props._hasRouterOutlet;
67+
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
5968
/**
6069
* For each tab, we need to keep track of its
6170
* base href as well as any child page that
@@ -75,13 +84,6 @@ export const IonTabBar = defineComponent({
7584
ref: child,
7685
};
7786

78-
/**
79-
* Passing this prop to each tab button
80-
* lets it be aware of the presence of
81-
* the router outlet.
82-
*/
83-
tabState.hasRouterOutlet = hasRouterOutlet;
84-
8587
/**
8688
* Passing this prop to each tab button
8789
* lets it be aware of the state that
@@ -126,7 +128,7 @@ export const IonTabBar = defineComponent({
126128
* @param ionRouter
127129
*/
128130
checkActiveTab(ionRouter: any) {
129-
const hasRouterOutlet = this.$props._hasRouterOutlet;
131+
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
130132
const currentRoute = ionRouter?.getCurrentRouteInfo();
131133
const childNodes = this.$data.tabVnodes;
132134
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
@@ -216,7 +218,7 @@ export const IonTabBar = defineComponent({
216218
this.tabSwitch(activeTab);
217219
},
218220
tabSwitch(activeTab: string, ionRouter?: any) {
219-
const hasRouterOutlet = this.$props._hasRouterOutlet;
221+
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
220222
const childNodes = this.$data.tabVnodes;
221223
const { activeTab: prevActiveTab } = this.$data.tabState;
222224
const tabState = this.$data.tabState;
@@ -227,15 +229,15 @@ export const IonTabBar = defineComponent({
227229
const tabDidChange = activeTab !== prevActiveTab;
228230
if (tabBar) {
229231
if (activeChild) {
230-
tabDidChange && this.$props._tabsWillChange(activeTab);
232+
tabDidChange && this.$data._tabsWillChange(activeTab);
231233

232234
if (hasRouterOutlet && ionRouter !== null) {
233235
ionRouter.handleSetCurrentTab(activeTab);
234236
}
235237

236238
tabBar.selectedTab = tabState.activeTab = activeTab;
237239

238-
tabDidChange && this.$props._tabsDidChange(activeTab);
240+
tabDidChange && this.$data._tabsDidChange(activeTab);
239241
} else {
240242
/**
241243
* When going to a tab that does
@@ -250,6 +252,17 @@ export const IonTabBar = defineComponent({
250252
},
251253
mounted() {
252254
const ionRouter: any = inject("navManager", null);
255+
/**
256+
* Tab bar can be used as a standalone component,
257+
* so it cannot be modified directly through
258+
* IonTabs. Instead, data will be passed through
259+
* the provide/inject.
260+
*/
261+
const tabBarData = inject<Ref<TabBarData>>("tabBarData");
262+
263+
this.$data.tabState.hasRouterOutlet = tabBarData.value.hasRouterOutlet;
264+
this.$data._tabsWillChange = tabBarData.value._tabsWillChange;
265+
this.$data._tabsDidChange = tabBarData.value._tabsDidChange;
253266

254267
this.setupTabState(ionRouter);
255268

packages/vue/src/components/IonTabs.ts

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
22
import type { VNode } from "vue";
3-
import { h, defineComponent, Fragment, isVNode } from "vue";
3+
import {
4+
h,
5+
defineComponent,
6+
Fragment,
7+
isVNode,
8+
provide,
9+
shallowRef,
10+
} from "vue";
411

512
import { IonTab } from "../proxies";
613

@@ -9,6 +16,12 @@ const DID_CHANGE = "ionTabsDidChange";
916

1017
// TODO(FW-2969): types
1118

19+
interface TabBarData {
20+
hasRouterOutlet: boolean;
21+
_tabsWillChange: Function;
22+
_tabsDidChange: Function;
23+
}
24+
1225
/**
1326
* Vue 3.2.38 fixed an issue where Web Component
1427
* names are respected using kebab case instead of pascal case.
@@ -24,13 +37,6 @@ const isRouterOutlet = (node: VNode) => {
2437
);
2538
};
2639

27-
const isTabBar = (node: VNode) => {
28-
return (
29-
node.type &&
30-
((node.type as any).name === "IonTabBar" || node.type === "ion-tab-bar")
31-
);
32-
};
33-
3440
const isTab = (node: VNode): boolean => {
3541
// The `ion-tab` component was created with the `v-for` directive.
3642
if (node.type === Fragment) {
@@ -49,7 +55,43 @@ const isTab = (node: VNode): boolean => {
4955
export const IonTabs = /*@__PURE__*/ defineComponent({
5056
name: "IonTabs",
5157
emits: [WILL_CHANGE, DID_CHANGE],
58+
data() {
59+
return {
60+
hasRouterOutlet: false,
61+
};
62+
},
5263
setup(props, { slots, emit }) {
64+
const slottedContent: VNode[] | undefined =
65+
slots.default && slots.default();
66+
let routerOutlet: VNode | undefined = undefined;
67+
68+
if (slottedContent && slottedContent.length > 0) {
69+
/**
70+
* Developers must pass an ion-router-outlet
71+
* inside of ion-tabs if they want to use
72+
* the history stack or URL updates associated
73+
* with the router.
74+
*/
75+
routerOutlet = slottedContent.find((child: VNode) =>
76+
isRouterOutlet(child)
77+
);
78+
}
79+
80+
/**
81+
* Tab bar can be used as a standalone component,
82+
* so it cannot be modified directly through
83+
* IonTabs. Instead, data will be passed through
84+
* the provide/inject.
85+
*/
86+
provide(
87+
"tabBarData",
88+
shallowRef<TabBarData>({
89+
hasRouterOutlet: !!routerOutlet,
90+
_tabsWillChange: (tab: string) => emit(WILL_CHANGE, { tab }),
91+
_tabsDidChange: (tab: string) => emit(DID_CHANGE, { tab }),
92+
})
93+
);
94+
5395
return {
5496
props,
5597
slots,
@@ -68,17 +110,18 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
68110
defineCustomElement();
69111
},
70112
render() {
71-
const { slots, emit, props } = this;
72-
const slottedContent = slots.default && slots.default();
73-
let routerOutlet;
113+
const { slots, props } = this;
114+
const slottedContent: VNode[] | undefined =
115+
slots.default && slots.default();
116+
let routerOutlet: VNode | undefined = undefined;
74117
let hasTab = false;
75118

76119
if (slottedContent && slottedContent.length > 0) {
77120
/**
78121
* Developers must pass an ion-router-outlet
79122
* inside of ion-tabs if they want to use
80123
* the history stack or URL updates associated
81-
* wit the router.
124+
* with the router.
82125
*/
83126
routerOutlet = slottedContent.find((child: VNode) =>
84127
isRouterOutlet(child)
@@ -103,30 +146,6 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
103146
);
104147
}
105148

106-
if (slottedContent && slottedContent.length > 0) {
107-
const slottedTabBar = slottedContent.find((child: VNode) =>
108-
isTabBar(child)
109-
);
110-
111-
if (slottedTabBar) {
112-
if (!slottedTabBar.props) {
113-
slottedTabBar.props = {};
114-
}
115-
/**
116-
* ionTabsWillChange and ionTabsDidChange are
117-
* fired from `ion-tabs`, so we need to pass these down
118-
* as props so they can fire when the active tab changes.
119-
* TODO: We may want to move logic from the tab bar into here
120-
* so we do not have code split across two components.
121-
*/
122-
slottedTabBar.props._tabsWillChange = (tab: string) =>
123-
emit(WILL_CHANGE, { tab });
124-
slottedTabBar.props._tabsDidChange = (tab: string) =>
125-
emit(DID_CHANGE, { tab });
126-
slottedTabBar.props._hasRouterOutlet = !!routerOutlet;
127-
}
128-
}
129-
130149
if (hasTab) {
131150
return h(
132151
"ion-tabs",

0 commit comments

Comments
 (0)