diff --git a/packages/angular/src/common/directives/navigation/tabs.ts b/packages/angular/src/common/directives/navigation/tabs.ts index 6f04e927913..18a0c4ca6a4 100644 --- a/packages/angular/src/common/directives/navigation/tabs.ts +++ b/packages/angular/src/common/directives/navigation/tabs.ts @@ -118,7 +118,10 @@ export abstract class IonTabs implements AfterViewInit, AfterContentInit, AfterC /** * @internal */ - onStackWillChange({ enteringView, tabSwitch }: StackWillChangeEvent): void { + onStackWillChange({ enteringView, tabSwitch }: StackWillChangeEvent, outlet?: any): void { + if (outlet && !this.outlet) { + this.outlet = outlet; + } const stackId = enteringView.stackId; if (tabSwitch && stackId !== undefined) { this.ionTabsWillChange.emit({ tab: stackId }); diff --git a/packages/angular/src/lazy/directives/navigation/ion-tabs.ts b/packages/angular/src/lazy/directives/navigation/ion-tabs.ts index 2c71159ed67..645175035b1 100644 --- a/packages/angular/src/lazy/directives/navigation/ion-tabs.ts +++ b/packages/angular/src/lazy/directives/navigation/ion-tabs.ts @@ -19,7 +19,7 @@ import { IonRouterOutlet } from './ion-router-outlet'; *ngIf="tabs.length === 0" #outlet tabs="true" - (stackWillChange)="onStackWillChange($event)" + (stackWillChange)="onStackWillChange($event, outlet)" (stackDidChange)="onStackDidChange($event)" > diff --git a/packages/angular/src/standalone/navigation/tabs.ts b/packages/angular/src/standalone/navigation/tabs.ts index ccce63094c7..0eda22c1bce 100644 --- a/packages/angular/src/standalone/navigation/tabs.ts +++ b/packages/angular/src/standalone/navigation/tabs.ts @@ -20,7 +20,7 @@ import { IonRouterOutlet } from './router-outlet'; *ngIf="tabs.length === 0" #outlet tabs="true" - (stackWillChange)="onStackWillChange($event)" + (stackWillChange)="onStackWillChange($event, outlet)" (stackDidChange)="onStackDidChange($event)" > diff --git a/packages/angular/test/base/e2e/src/lazy/tabs.spec.ts b/packages/angular/test/base/e2e/src/lazy/tabs.spec.ts index 51c9650517f..094b3bfe78a 100644 --- a/packages/angular/test/base/e2e/src/lazy/tabs.spec.ts +++ b/packages/angular/test/base/e2e/src/lazy/tabs.spec.ts @@ -513,4 +513,22 @@ test('Tabs should support conditional slots', async ({ page }) => { // The tab bar should be added to the bottom await expect(page.locator('ion-tabs .tabs-inner + ion-tab-bar')).toHaveCount(1); + test.describe('Deferred IonTabs initialization', () => { + test('should populate outlet before emitting initial ionTabsWillChange', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31674', + }); + + await page.goto('/lazy/tabs-outlet-init'); + await page.locator('#show-tabs').click(); + + await expect(page.locator('app-tabs-outlet-init-tab-one')).toBeVisible(); + await expect(page.locator('#will-change-log')).toHaveText('tab-one:true:undefined'); + + await page.locator('#tab-button-tab-two').click(); + await expect(page.locator('app-tabs-outlet-init-tab-two')).toBeVisible(); + await expect(page.locator('#will-change-log')).toHaveText('tab-one:true:undefined,tab-two:true:tab-one'); + }); + }); }); diff --git a/packages/angular/test/base/e2e/src/standalone/tabs.spec.ts b/packages/angular/test/base/e2e/src/standalone/tabs.spec.ts index 49f253a673a..991297e14a4 100644 --- a/packages/angular/test/base/e2e/src/standalone/tabs.spec.ts +++ b/packages/angular/test/base/e2e/src/standalone/tabs.spec.ts @@ -46,4 +46,23 @@ test.describe('Tabs', () => { await expect(page).toHaveURL(/.*\/tabs-basic/); }); }); + + test.describe('Deferred IonTabs initialization', () => { + test('should populate outlet before emitting initial ionTabsWillChange', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31674', + }); + + await page.goto('/standalone/tabs-outlet-init'); + await page.locator('#show-tabs').click(); + + await expect(page.locator('app-tab-one')).toBeVisible(); + await expect(page.locator('#will-change-log')).toHaveText('tab-one:true:undefined'); + + await page.locator('#tab-button-tab-two').click(); + await expect(page.locator('app-tab-two')).toBeVisible(); + await expect(page.locator('#will-change-log')).toHaveText('tab-one:true:undefined,tab-two:true:tab-one'); + }); + }); }); diff --git a/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts b/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts index 6c8512b46f7..962fe435275 100644 --- a/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts +++ b/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts @@ -78,6 +78,11 @@ export const routes: Routes = [ path: 'tabs-slots', loadComponent: () => import('../tabs-slots.component').then(c => c.TabsSlotsComponent) }, + { path: 'tabs-outlet-init', redirectTo: '/lazy/tabs-outlet-init/tab-one', pathMatch: 'full' }, + { + path: 'tabs-outlet-init', + loadChildren: () => import('../tabs-outlet-init/tabs-outlet-init.module').then(m => m.TabsOutletInitModule) + }, { path: 'tabs-basic', component: TabsBasicComponent }, { path: 'nested-outlet', diff --git a/packages/angular/test/base/src/app/lazy/tabs-outlet-init/tabs-outlet-init.component.ts b/packages/angular/test/base/src/app/lazy/tabs-outlet-init/tabs-outlet-init.component.ts new file mode 100644 index 00000000000..90dc97b4693 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/tabs-outlet-init/tabs-outlet-init.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { IonTabs } from '@ionic/angular/lazy'; + +@Component({ + selector: 'app-tabs-outlet-init', + changeDetection: ChangeDetectionStrategy.Default, + standalone: false, + template: ` + + + + + Tab One + + + Tab Two + + + +
{{ willChangeLog.join(',') }}
+ `, +}) +export class TabsOutletInitComponent { + showTabs = false; + willChangeLog: string[] = []; + + onWillChange(ev: { tab: string }, tabs: IonTabs) { + this.willChangeLog.push(`${ev.tab}:${Boolean(tabs.outlet?.nativeEl)}:${String(tabs.getSelected())}`); + } +} + +@Component({ + selector: 'app-tabs-outlet-init-tab-one', + changeDetection: ChangeDetectionStrategy.Default, + standalone: false, + template: 'Tab 1', +}) +export class TabsOutletInitTabOneComponent {} + +@Component({ + selector: 'app-tabs-outlet-init-tab-two', + changeDetection: ChangeDetectionStrategy.Default, + standalone: false, + template: 'Tab 2', +}) +export class TabsOutletInitTabTwoComponent {} diff --git a/packages/angular/test/base/src/app/lazy/tabs-outlet-init/tabs-outlet-init.module.ts b/packages/angular/test/base/src/app/lazy/tabs-outlet-init/tabs-outlet-init.module.ts new file mode 100644 index 00000000000..acf8a4fbd6e --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/tabs-outlet-init/tabs-outlet-init.module.ts @@ -0,0 +1,27 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular/lazy'; + +import { + TabsOutletInitComponent, + TabsOutletInitTabOneComponent, + TabsOutletInitTabTwoComponent, +} from './tabs-outlet-init.component'; + +const routes: Routes = [ + { + path: '', + component: TabsOutletInitComponent, + children: [ + { path: 'tab-one', component: TabsOutletInitTabOneComponent }, + { path: 'tab-two', component: TabsOutletInitTabTwoComponent }, + ], + }, +]; + +@NgModule({ + imports: [CommonModule, IonicModule, RouterModule.forChild(routes)], + declarations: [TabsOutletInitComponent, TabsOutletInitTabOneComponent, TabsOutletInitTabTwoComponent], +}) +export class TabsOutletInitModule {} diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index 37a2bae6314..e1441161794 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -72,6 +72,15 @@ export const routes: Routes = [ { path: 'tab-three', loadComponent: () => import('../tabs/tab3.component').then(c => c.TabThreeComponent) } ] }, + { path: 'tabs-outlet-init', redirectTo: '/standalone/tabs-outlet-init/tab-one', pathMatch: 'full' }, + { + path: 'tabs-outlet-init', + loadComponent: () => import('../tabs/tabs-outlet-init.component').then(c => c.TabsOutletInitComponent), + children: [ + { path: 'tab-one', loadComponent: () => import('../tabs/tab1.component').then(c => c.TabOneComponent) }, + { path: 'tab-two', loadComponent: () => import('../tabs/tab2.component').then(c => c.TabTwoComponent) } + ] + }, { path: 'tabs-basic', loadComponent: () => import('../tabs-basic/tabs-basic.component').then(c => c.TabsBasicComponent) }, { path: 'tabs-search-params', redirectTo: '/standalone/tabs-search-params/tab1?foo=bar', pathMatch: 'full' }, { diff --git a/packages/angular/test/base/src/app/standalone/tabs/tabs-outlet-init.component.ts b/packages/angular/test/base/src/app/standalone/tabs/tabs-outlet-init.component.ts new file mode 100644 index 00000000000..6c2f1930ffb --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tabs/tabs-outlet-init.component.ts @@ -0,0 +1,32 @@ +import { NgIf } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { IonLabel, IonTabBar, IonTabButton, IonTabs } from '@ionic/angular'; + +@Component({ + selector: 'app-tabs-outlet-init', + changeDetection: ChangeDetectionStrategy.Default, + standalone: true, + imports: [NgIf, IonLabel, IonTabBar, IonTabButton, IonTabs], + template: ` + + + + + Tab One + + + Tab Two + + + +
{{ willChangeLog.join(',') }}
+ `, +}) +export class TabsOutletInitComponent { + showTabs = false; + willChangeLog: string[] = []; + + onWillChange(ev: { tab: string }, tabs: IonTabs) { + this.willChangeLog.push(`${ev.tab}:${Boolean(tabs.outlet?.nativeEl)}:${String(tabs.getSelected())}`); + } +}