Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vue): tabs correctly fire lifecycle events #22479

Merged
merged 5 commits into from
Nov 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/vue/src/components/IonTabBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ interface Tab {

export const IonTabBar = defineComponent({
name: 'IonTabBar',
props: {
_tabsWillChange: { type: Function, default: () => {} },
_tabsDidChange: { type: Function, default: () => {} }
},
mounted() {
const ionRouter: any = inject('navManager');
const tabState: TabState = {
Expand Down Expand Up @@ -102,12 +106,16 @@ export const IonTabBar = defineComponent({
}
}

const activeChild = childNodes.find((child: VNode) => child.el.tab === activeTab);
const activeChild = childNodes.find((child: VNode) => child.props.tab === activeTab);
const tabBar = this.$refs.ionTabBar;

const tabDidChange = activeTab !== prevActiveTab;
if (activeChild && tabBar) {
tabDidChange && this.$props._tabsWillChange(activeTab);

ionRouter.handleSetCurrentTab(activeTab);
tabBar.selectedTab = tabState.activeTab = activeTab;

tabDidChange && this.$props._tabsDidChange(activeTab);
}
};

Expand Down
29 changes: 22 additions & 7 deletions packages/vue/src/components/IonTabs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { h, defineComponent, VNode } from 'vue';
import { IonRouterOutlet } from './IonRouterOutlet';

const WILL_CHANGE = 'ionTabsWillChange';
const DID_CHANGE = 'ionTabsDidChange';

export const IonTabs = defineComponent({
name: 'IonTabs',
emits: [WILL_CHANGE, DID_CHANGE],
render() {
const { $slots: slots } = this;
const { $slots: slots, $emit } = this;
const slottedContent = slots.default && slots.default();
let childrenToRender = [
h('div', {
Expand All @@ -25,14 +29,25 @@ export const IonTabs = defineComponent({
* not show above the tab content.
*/
if (slottedContent && slottedContent.length > 0) {
const topSlottedTabBar = slottedContent.find((child: VNode) => {
const isTabBar = child.type && (child.type as any).name === 'IonTabBar';
const hasTopSlot = child.props?.slot === 'top';
const slottedTabBar = slottedContent.find((child: VNode) => child.type && (child.type as any).name === 'IonTabBar');
const hasTopSlotTabBar = slottedTabBar && slottedTabBar.props?.slot === 'top';

return isTabBar && hasTopSlot;
});
if (slottedTabBar) {
if (!slottedTabBar.props) {
slottedTabBar.props = {};
}
/**
* ionTabsWillChange and ionTabsDidChange are
* fired from `ion-tabs`, so we need to pass these down
* as props so they can fire when the active tab changes.
* TODO: We may want to move logic from the tab bar into here
* so we do not have code split across two components.
*/
slottedTabBar.props._tabsWillChange = (tab: string) => $emit(WILL_CHANGE, { tab });
slottedTabBar.props._tabsDidChange = (tab: string) => $emit(DID_CHANGE, { tab });
}

if (topSlottedTabBar) {
if (hasTopSlotTabBar) {
childrenToRender = [
...slottedContent,
...childrenToRender
Expand Down
246 changes: 246 additions & 0 deletions packages/vue/test-app/tests/unit/tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { mount, flushPromises } from '@vue/test-utils';
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { IonicVue, IonApp, IonRouterOutlet, IonPage, IonTabs, IonTabBar, IonTabButton, IonLabel } from '@ionic/vue';

const App = {
components: { IonApp, IonRouterOutlet },
template: '<ion-app><ion-router-outlet /></ion-app>',
}

const Tabs = {
components: { IonPage, IonTabs, IonTabBar, IonTabButton, IonLabel },
template: `
<ion-page>
<ion-tabs>
<ion-tab-bar slot="top">
<ion-tab-button tab="tab1" href="/tab1">
<ion-label>Tab 1</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2" href="/tab2">
<ion-label>Tab 2</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-page>
`,
}
const Tab1 = {
components: { IonPage },
template: `<ion-page>Tab 1</ion-page>`
}
const Tab2 = {
components: { IonPage },
template: `<ion-page>Tab 2</ion-page>`
}

describe('ion-tabs', () => {
(HTMLElement.prototype as HTMLIonRouterOutletElement).commit = jest.fn();

it('should emit will change and did change events when changing tab', async () => {
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{
path: '/',
component: Tabs,
children: [
{
path: '',
redirect: 'tab1'
},
{
path: 'tab1',
component: Tab1,
},
{
path: 'tab2',
component: Tab2
}
]
}
]
});

router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});

const tabs = wrapper.findComponent(IonTabs);
expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);

router.push('/tab2')
await flushPromises()

expect(tabs.emitted().ionTabsWillChange.length).toEqual(2);
expect(tabs.emitted().ionTabsWillChange[1]).toEqual([{ tab: 'tab2' }]);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(2);
expect(tabs.emitted().ionTabsDidChange[1]).toEqual([{ tab: 'tab2' }]);
});

it('should not emit will change and did change events when going to same tab again', async () => {
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{
path: '/',
component: Tabs,
children: [
{
path: '',
redirect: 'tab1'
},
{
path: 'tab1',
component: Tab1,
},
{
path: 'tab2',
component: Tab2
}
]
}
]
});

router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});

const tabs = wrapper.findComponent(IonTabs);
expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);

router.push('/tab1')
await flushPromises()

expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
});

it('should not emit will change and did change events when going to a non tabs page', async () => {
const Sibling = {
components: { IonPage },
template: `<ion-page>Sibling Page</ion-page>`
}
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{
path: '/',
component: Tabs,
children: [
{
path: '',
redirect: 'tab1'
},
{
path: 'tab1',
component: Tab1
},
{
path: 'tab2',
component: Tab2
}
]
},
{
path: '/sibling',
component: Sibling
}
]
});

router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});

const tabs = wrapper.findComponent(IonTabs);
expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);

router.push('/sibling');
await flushPromises();

await new Promise((r) => setTimeout(r, 100));

expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
});

it('should not emit will change and did change events when going to child tab page', async () => {
const Child = {
components: { IonPage },
template: `<ion-page>Child Page</ion-page>`
}
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{
path: '/',
component: Tabs,
children: [
{
path: '',
redirect: 'tab1'
},
{
path: 'tab1',
component: Tab1,
children: [
{
path: 'child',
component: Child
}
]
},
{
path: 'tab2',
component: Tab2
}
]
}
]
});

router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});

const tabs = wrapper.findComponent(IonTabs);
expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsWillChange[0]).toEqual([{ tab: 'tab1' }]);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange[0]).toEqual([{ tab: 'tab1' }]);

router.push('/tab1/child');
await flushPromises();

await new Promise((r) => setTimeout(r, 100));

expect(tabs.emitted().ionTabsWillChange.length).toEqual(1);
expect(tabs.emitted().ionTabsDidChange.length).toEqual(1);
});
});