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)"
>
0" select="ion-tab">
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)"
>
0" select="ion-tab">
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())}`);
+ }
+}