Skip to content

Commit 867066b

Browse files
thetaPCbrandyscarneysean-perkins
authored
feat(react, vue, angular): use tabs without router (#29794)
Issue number: resolves #25184 --------- Co-authored-by: Brandy Carney <[email protected]> Co-authored-by: Sean Perkins <[email protected]>
1 parent 4580edc commit 867066b

File tree

38 files changed

+1875
-1210
lines changed

38 files changed

+1875
-1210
lines changed

core/src/components/tabs/tabs.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,17 @@ export class Tabs implements NavOutlet {
4343

4444
async componentWillLoad() {
4545
if (!this.useRouter) {
46-
this.useRouter = !!document.querySelector('ion-router') && !this.el.closest('[no-router]');
46+
/**
47+
* JavaScript and StencilJS use `ion-router`, while
48+
* the other frameworks use `ion-router-outlet`.
49+
*
50+
* If either component is present then tabs will not use
51+
* a basic tab-based navigation. It will use the history
52+
* stack or URL updates associated with the router.
53+
*/
54+
this.useRouter =
55+
(!!this.el.querySelector('ion-router-outlet') || !!document.querySelector('ion-router')) &&
56+
!this.el.closest('[no-router]');
4757
}
4858
if (!this.useRouter) {
4959
const tabs = this.tabs;

core/stencil.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const getAngularOutputTargets = () => {
2626

2727
// tabs
2828
'ion-tabs',
29-
'ion-tab',
3029

3130
// auxiliar
3231
'ion-picker-legacy-column',
@@ -177,7 +176,6 @@ export const config: Config = {
177176
'ion-back-button',
178177
'ion-tab-button',
179178
'ion-tabs',
180-
'ion-tab',
181179
'ion-tab-bar',
182180

183181
// Overlays

packages/angular/common/src/directives/navigation/tabs.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
HostListener,
88
Output,
99
ViewChild,
10+
AfterViewInit,
11+
QueryList,
1012
} from '@angular/core';
1113

1214
import { NavController } from '../../providers/nav-controller';
@@ -17,14 +19,15 @@ import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
1719
selector: 'ion-tabs',
1820
})
1921
// eslint-disable-next-line @angular-eslint/directive-class-suffix
20-
export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
22+
export abstract class IonTabs implements AfterViewInit, AfterContentInit, AfterContentChecked {
2123
/**
2224
* Note: These must be redeclared on each child class since it needs
2325
* access to generated components such as IonRouterOutlet and IonTabBar.
2426
*/
2527
abstract outlet: any;
2628
abstract tabBar: any;
27-
abstract tabBars: any;
29+
abstract tabBars: QueryList<any>;
30+
abstract tabs: QueryList<any>;
2831

2932
@ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef<HTMLDivElement>;
3033

@@ -39,8 +42,29 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
3942

4043
private tabBarSlot = 'bottom';
4144

45+
private hasTab = false;
46+
private selectedTab?: { tab: string };
47+
private leavingTab?: any;
48+
4249
constructor(private navCtrl: NavController) {}
4350

51+
ngAfterViewInit(): void {
52+
/**
53+
* Developers must pass at least one ion-tab
54+
* inside of ion-tabs if they want to use a
55+
* basic tab-based navigation without the
56+
* history stack or URL updates associated
57+
* with the router.
58+
*/
59+
const firstTab = this.tabs.length > 0 ? this.tabs.first : undefined;
60+
61+
if (firstTab) {
62+
this.hasTab = true;
63+
this.setActiveTab(firstTab.tab);
64+
this.tabSwitch();
65+
}
66+
}
67+
4468
ngAfterContentInit(): void {
4569
this.detectSlotChanges();
4670
}
@@ -96,6 +120,19 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
96120
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
97121
const isTabString = typeof tabOrEvent === 'string';
98122
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;
123+
124+
/**
125+
* If the tabs are not using the router, then
126+
* the tab switch logic is handled by the tabs
127+
* component itself.
128+
*/
129+
if (this.hasTab) {
130+
this.setActiveTab(tab);
131+
this.tabSwitch();
132+
133+
return;
134+
}
135+
99136
const alreadySelected = this.outlet.getActiveStackId() === tab;
100137
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
101138

@@ -142,7 +179,46 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
142179
}
143180
}
144181

182+
private setActiveTab(tab: string): void {
183+
const tabs = this.tabs;
184+
const selectedTab = tabs.find((t: any) => t.tab === tab);
185+
186+
if (!selectedTab) {
187+
console.error(`[Ionic Error]: Tab with id: "${tab}" does not exist`);
188+
return;
189+
}
190+
191+
this.leavingTab = this.selectedTab;
192+
this.selectedTab = selectedTab;
193+
194+
this.ionTabsWillChange.emit({ tab });
195+
196+
selectedTab.el.active = true;
197+
}
198+
199+
private tabSwitch(): void {
200+
const { selectedTab, leavingTab } = this;
201+
202+
if (this.tabBar && selectedTab) {
203+
this.tabBar.selectedTab = selectedTab.tab;
204+
}
205+
206+
if (leavingTab?.tab !== selectedTab?.tab) {
207+
if (leavingTab?.el) {
208+
leavingTab.el.active = false;
209+
}
210+
}
211+
212+
if (selectedTab) {
213+
this.ionTabsDidChange.emit({ tab: selectedTab.tab });
214+
}
215+
}
216+
145217
getSelected(): string | undefined {
218+
if (this.hasTab) {
219+
return this.selectedTab?.tab;
220+
}
221+
146222
return this.outlet.getActiveStackId();
147223
}
148224

packages/angular/src/app-initialize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const appInitialize = (config: Config, doc: Document, zone: NgZone) => {
2020

2121
return applyPolyfills().then(() => {
2222
return defineCustomElements(win, {
23-
exclude: ['ion-tabs', 'ion-tab'],
23+
exclude: ['ion-tabs'],
2424
syncQueue: true,
2525
raf,
2626
jmp: (h: any) => zone.runOutsideAngular(h),

packages/angular/src/directives/navigation/ion-tabs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
22
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
33

4-
import { IonTabBar } from '../proxies';
4+
import { IonTabBar, IonTab } from '../proxies';
55

66
import { IonRouterOutlet } from './ion-router-outlet';
77

@@ -11,11 +11,13 @@ import { IonRouterOutlet } from './ion-router-outlet';
1111
<ng-content select="[slot=top]"></ng-content>
1212
<div class="tabs-inner" #tabsInner>
1313
<ion-router-outlet
14+
*ngIf="tabs.length === 0"
1415
#outlet
1516
tabs="true"
1617
(stackWillChange)="onStackWillChange($event)"
1718
(stackDidChange)="onStackDidChange($event)"
1819
></ion-router-outlet>
20+
<ng-content *ngIf="tabs.length > 0" select="ion-tab"></ng-content>
1921
</div>
2022
<ng-content></ng-content>
2123
`,
@@ -52,4 +54,5 @@ export class IonTabs extends IonTabsBase {
5254

5355
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
5456
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
57+
@ContentChildren(IonTab) tabs: QueryList<IonTab>;
5558
}

packages/angular/src/directives/proxies-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export const DIRECTIVES = [
7474
d.IonSkeletonText,
7575
d.IonSpinner,
7676
d.IonSplitPane,
77+
d.IonTab,
7778
d.IonTabBar,
7879
d.IonTabButton,
7980
d.IonText,

packages/angular/src/directives/proxies.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2148,6 +2148,29 @@ export declare interface IonSplitPane extends Components.IonSplitPane {
21482148
}
21492149

21502150

2151+
@ProxyCmp({
2152+
inputs: ['component', 'tab'],
2153+
methods: ['setActive']
2154+
})
2155+
@Component({
2156+
selector: 'ion-tab',
2157+
changeDetection: ChangeDetectionStrategy.OnPush,
2158+
template: '<ng-content></ng-content>',
2159+
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2160+
inputs: ['component', 'tab'],
2161+
})
2162+
export class IonTab {
2163+
protected el: HTMLElement;
2164+
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
2165+
c.detach();
2166+
this.el = r.nativeElement;
2167+
}
2168+
}
2169+
2170+
2171+
export declare interface IonTab extends Components.IonTab {}
2172+
2173+
21512174
@ProxyCmp({
21522175
inputs: ['color', 'mode', 'selectedTab', 'translucent']
21532176
})

packages/angular/standalone/src/directives/proxies.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon
6969
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
7070
import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js';
7171
import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js';
72+
import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js';
7273
import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js';
7374
import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js';
7475
import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js';
@@ -1939,6 +1940,31 @@ export declare interface IonSplitPane extends Components.IonSplitPane {
19391940
}
19401941

19411942

1943+
@ProxyCmp({
1944+
defineCustomElementFn: defineIonTab,
1945+
inputs: ['component', 'tab'],
1946+
methods: ['setActive']
1947+
})
1948+
@Component({
1949+
selector: 'ion-tab',
1950+
changeDetection: ChangeDetectionStrategy.OnPush,
1951+
template: '<ng-content></ng-content>',
1952+
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
1953+
inputs: ['component', 'tab'],
1954+
standalone: true
1955+
})
1956+
export class IonTab {
1957+
protected el: HTMLElement;
1958+
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
1959+
c.detach();
1960+
this.el = r.nativeElement;
1961+
}
1962+
}
1963+
1964+
1965+
export declare interface IonTab extends Components.IonTab {}
1966+
1967+
19421968
@ProxyCmp({
19431969
defineCustomElementFn: defineIonTabBar,
19441970
inputs: ['color', 'mode', 'selectedTab', 'translucent']

packages/angular/standalone/src/navigation/tabs.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { NgIf } from '@angular/common';
12
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
23
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
34

4-
import { IonTabBar } from '../directives/proxies';
5+
import { IonTabBar, IonTab } from '../directives/proxies';
56

67
import { IonRouterOutlet } from './router-outlet';
78

@@ -11,11 +12,13 @@ import { IonRouterOutlet } from './router-outlet';
1112
<ng-content select="[slot=top]"></ng-content>
1213
<div class="tabs-inner" #tabsInner>
1314
<ion-router-outlet
15+
*ngIf="tabs.length === 0"
1416
#outlet
1517
tabs="true"
1618
(stackWillChange)="onStackWillChange($event)"
1719
(stackDidChange)="onStackDidChange($event)"
1820
></ion-router-outlet>
21+
<ng-content *ngIf="tabs.length > 0" select="ion-tab"></ng-content>
1922
</div>
2023
<ng-content></ng-content>
2124
`,
@@ -46,12 +49,13 @@ import { IonRouterOutlet } from './router-outlet';
4649
}
4750
`,
4851
],
49-
imports: [IonRouterOutlet],
52+
imports: [IonRouterOutlet, NgIf],
5053
})
5154
// eslint-disable-next-line @angular-eslint/component-class-suffix
5255
export class IonTabs extends IonTabsBase {
5356
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;
5457

5558
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
5659
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
60+
@ContentChildren(IonTab) tabs: QueryList<IonTab>;
5761
}

0 commit comments

Comments
 (0)