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

feat(react, vue, angular): use tabs without router #29794

Merged
merged 3 commits into from
Aug 26, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion core/src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ export class Tabs implements NavOutlet {

async componentWillLoad() {
if (!this.useRouter) {
this.useRouter = !!document.querySelector('ion-router') && !this.el.closest('[no-router]');
/**
* JavaScript and StencilJS use `ion-router`, while
* the other frameworks use `ion-router-outlet`.
*
* If either component is present then tabs will not use
* a basic tab-based navigation. It will use the history
* stack or URL updates associated with the router.
*/
this.useRouter =
(!!this.el.querySelector('ion-router-outlet') || !!document.querySelector('ion-router')) &&
!this.el.closest('[no-router]');
}
if (!this.useRouter) {
const tabs = this.tabs;
Expand Down
2 changes: 0 additions & 2 deletions core/stencil.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const getAngularOutputTargets = () => {

// tabs
'ion-tabs',
'ion-tab',

// auxiliar
'ion-picker-legacy-column',
Expand Down Expand Up @@ -177,7 +176,6 @@ export const config: Config = {
'ion-back-button',
'ion-tab-button',
'ion-tabs',
'ion-tab',
'ion-tab-bar',

// Overlays
Expand Down
80 changes: 78 additions & 2 deletions packages/angular/common/src/directives/navigation/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
HostListener,
Output,
ViewChild,
AfterViewInit,
QueryList,
} from '@angular/core';

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

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

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

private tabBarSlot = 'bottom';

private hasTab = false;
private selectedTab?: { tab: string };
private leavingTab?: any;

constructor(private navCtrl: NavController) {}

ngAfterViewInit(): void {
/**
* Developers must pass at least one ion-tab
* inside of ion-tabs if they want to use a
* basic tab-based navigation without the
* history stack or URL updates associated
* with the router.
*/
const firstTab = this.tabs.length > 0 ? this.tabs.first : undefined;

if (firstTab) {
this.hasTab = true;
this.setActiveTab(firstTab.tab);
this.tabSwitch();
}
}

ngAfterContentInit(): void {
this.detectSlotChanges();
}
Expand Down Expand Up @@ -96,6 +120,19 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
const isTabString = typeof tabOrEvent === 'string';
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;

/**
* If the tabs are not using the router, then
* the tab switch logic is handled by the tabs
* component itself.
*/
if (this.hasTab) {
this.setActiveTab(tab);
this.tabSwitch();

return;
}

const alreadySelected = this.outlet.getActiveStackId() === tab;
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;

Expand Down Expand Up @@ -142,7 +179,46 @@ export abstract class IonTabs implements AfterContentInit, AfterContentChecked {
}
}

private setActiveTab(tab: string): void {
const tabs = this.tabs;
const selectedTab = tabs.find((t: any) => t.tab === tab);

if (!selectedTab) {
console.error(`[Ionic Error]: Tab with id: "${tab}" does not exist`);
return;
}

this.leavingTab = this.selectedTab;
this.selectedTab = selectedTab;

this.ionTabsWillChange.emit({ tab });

selectedTab.el.active = true;
}

private tabSwitch(): void {
const { selectedTab, leavingTab } = this;

if (this.tabBar && selectedTab) {
this.tabBar.selectedTab = selectedTab.tab;
}

if (leavingTab?.tab !== selectedTab?.tab) {
if (leavingTab?.el) {
leavingTab.el.active = false;
}
}

if (selectedTab) {
this.ionTabsDidChange.emit({ tab: selectedTab.tab });
}
}

getSelected(): string | undefined {
if (this.hasTab) {
return this.selectedTab?.tab;
}

return this.outlet.getActiveStackId();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/angular/src/app-initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const appInitialize = (config: Config, doc: Document, zone: NgZone) => {

return applyPolyfills().then(() => {
return defineCustomElements(win, {
exclude: ['ion-tabs', 'ion-tab'],
exclude: ['ion-tabs'],
syncQueue: true,
raf,
jmp: (h: any) => zone.runOutsideAngular(h),
Expand Down
5 changes: 4 additions & 1 deletion packages/angular/src/directives/navigation/ion-tabs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
import { IonTabs as IonTabsBase } from '@ionic/angular/common';

import { IonTabBar } from '../proxies';
import { IonTabBar, IonTab } from '../proxies';

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

Expand All @@ -11,11 +11,13 @@ import { IonRouterOutlet } from './ion-router-outlet';
<ng-content select="[slot=top]"></ng-content>
<div class="tabs-inner" #tabsInner>
<ion-router-outlet
*ngIf="tabs.length === 0"
#outlet
tabs="true"
(stackWillChange)="onStackWillChange($event)"
(stackDidChange)="onStackDidChange($event)"
></ion-router-outlet>
<ng-content *ngIf="tabs.length > 0" select="ion-tab"></ng-content>
</div>
<ng-content></ng-content>
`,
Expand Down Expand Up @@ -52,4 +54,5 @@ export class IonTabs extends IonTabsBase {

@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
@ContentChildren(IonTab) tabs: QueryList<IonTab>;
}
1 change: 1 addition & 0 deletions packages/angular/src/directives/proxies-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const DIRECTIVES = [
d.IonSkeletonText,
d.IonSpinner,
d.IonSplitPane,
d.IonTab,
d.IonTabBar,
d.IonTabButton,
d.IonText,
Expand Down
23 changes: 23 additions & 0 deletions packages/angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2148,6 +2148,29 @@ export declare interface IonSplitPane extends Components.IonSplitPane {
}


@ProxyCmp({
inputs: ['component', 'tab'],
methods: ['setActive']
})
@Component({
selector: 'ion-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['component', 'tab'],
})
export class IonTab {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}


export declare interface IonTab extends Components.IonTab {}


@ProxyCmp({
inputs: ['color', 'mode', 'selectedTab', 'translucent']
})
Expand Down
26 changes: 26 additions & 0 deletions packages/angular/standalone/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js';
import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js';
import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js';
import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js';
import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js';
import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js';
Expand Down Expand Up @@ -1939,6 +1940,31 @@ export declare interface IonSplitPane extends Components.IonSplitPane {
}


@ProxyCmp({
defineCustomElementFn: defineIonTab,
inputs: ['component', 'tab'],
methods: ['setActive']
})
@Component({
selector: 'ion-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['component', 'tab'],
standalone: true
})
export class IonTab {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}


export declare interface IonTab extends Components.IonTab {}


@ProxyCmp({
defineCustomElementFn: defineIonTabBar,
inputs: ['color', 'mode', 'selectedTab', 'translucent']
Expand Down
8 changes: 6 additions & 2 deletions packages/angular/standalone/src/navigation/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NgIf } from '@angular/common';
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
import { IonTabs as IonTabsBase } from '@ionic/angular/common';

import { IonTabBar } from '../directives/proxies';
import { IonTabBar, IonTab } from '../directives/proxies';

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

Expand All @@ -11,11 +12,13 @@ import { IonRouterOutlet } from './router-outlet';
<ng-content select="[slot=top]"></ng-content>
<div class="tabs-inner" #tabsInner>
<ion-router-outlet
*ngIf="tabs.length === 0"
#outlet
tabs="true"
(stackWillChange)="onStackWillChange($event)"
(stackDidChange)="onStackDidChange($event)"
></ion-router-outlet>
<ng-content *ngIf="tabs.length > 0" select="ion-tab"></ng-content>
</div>
<ng-content></ng-content>
`,
Expand Down Expand Up @@ -46,12 +49,13 @@ import { IonRouterOutlet } from './router-outlet';
}
`,
],
imports: [IonRouterOutlet],
imports: [IonRouterOutlet, NgIf],
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class IonTabs extends IonTabsBase {
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;

@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
@ContentChildren(IonTab) tabs: QueryList<IonTab>;
}
Loading
Loading