mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(angular): add tabs events (#17125)
* feat(angular): add tabs events * lint
This commit is contained in:
@@ -25,6 +25,7 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
|
||||
tabsPrefix: string | undefined;
|
||||
|
||||
@Output() stackEvents = new EventEmitter<any>();
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
@@ -173,9 +174,9 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
this.activatedView = enteringView;
|
||||
this.stackCtrl.setActive(enteringView).then(() => {
|
||||
this.stackCtrl.setActive(enteringView).then(data => {
|
||||
this.activateEvents.emit(cmpRef.instance);
|
||||
emitEvent(this.nativeEl, enteringView!);
|
||||
this.stackEvents.emit(data);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,15 +198,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
function emitEvent(el: HTMLElement, view: RouteView) {
|
||||
const ev = new CustomEvent('ionRouterOutletActivated', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: { view }
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
class OutletInjector implements Injector {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component, ContentChild, HostListener, ViewChild } from '@angular/core';
|
||||
import { Component, ContentChild, EventEmitter, HostListener, Output, ViewChild } from '@angular/core';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { IonTabBar } from '../proxies';
|
||||
|
||||
import { IonRouterOutlet } from './ion-router-outlet';
|
||||
import { RouteView } from './stack-utils';
|
||||
import { StackEvent } from './stack-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-tabs',
|
||||
template: `
|
||||
<ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner">
|
||||
<ion-router-outlet #outlet tabs="true"></ion-router-outlet>
|
||||
<ion-router-outlet #outlet tabs="true" (stackEvents)="onPageSelected($event)"></ion-router-outlet>
|
||||
</div>
|
||||
<ng-content></ng-content>`,
|
||||
styles: [`
|
||||
@@ -45,6 +45,9 @@ export class IonTabs {
|
||||
@ViewChild('outlet', { read: IonRouterOutlet }) outlet: IonRouterOutlet;
|
||||
@ContentChild(IonTabBar) tabBar: IonTabBar | undefined;
|
||||
|
||||
@Output() ionTabsWillChange = new EventEmitter<{tab: string}>();
|
||||
@Output() ionTabsDidChange = new EventEmitter<{tab: string}>();
|
||||
|
||||
constructor(
|
||||
private navCtrl: NavController,
|
||||
) {}
|
||||
@@ -52,10 +55,14 @@ export class IonTabs {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('ionRouterOutletActivated', ['$event.detail'])
|
||||
onPageSelected(detail: {view: RouteView}) {
|
||||
if (this.tabBar) {
|
||||
this.tabBar.selectedTab = detail.view.stackId;
|
||||
onPageSelected(detail: StackEvent) {
|
||||
const stackId = detail.enteringView.stackId;
|
||||
if (detail.tabSwitch && stackId !== undefined) {
|
||||
if (this.tabBar) {
|
||||
this.tabBar.selectedTab = stackId;
|
||||
}
|
||||
this.ionTabsWillChange.emit({ tab: stackId });
|
||||
this.ionTabsDidChange.emit({ tab: stackId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RouterDirection } from '@ionic/core';
|
||||
import { bindLifecycleEvents } from '../../providers/angular-delegate';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
|
||||
import { RouteView, computeStackId, destroyView, getUrl, insertView, isTabSwitch, toSegments } from './stack-utils';
|
||||
import { RouteView, StackEvent, computeStackId, destroyView, getUrl, insertView, isTabSwitch, toSegments } from './stack-utils';
|
||||
|
||||
export class StackController {
|
||||
|
||||
@@ -50,16 +50,23 @@ export class StackController {
|
||||
return view;
|
||||
}
|
||||
|
||||
async setActive(enteringView: RouteView) {
|
||||
async setActive(enteringView: RouteView): Promise<StackEvent> {
|
||||
let { direction, animation } = this.navCtrl.consumeTransition();
|
||||
const leavingView = this.activeView;
|
||||
if (isTabSwitch(enteringView, leavingView)) {
|
||||
const tabSwitch = isTabSwitch(enteringView, leavingView);
|
||||
if (tabSwitch) {
|
||||
direction = 'back';
|
||||
animation = undefined;
|
||||
}
|
||||
this.insertView(enteringView, direction);
|
||||
await this.transition(enteringView, leavingView, animation, this.canGoBack(1), false);
|
||||
requestAnimationFrame(() => this.cleanup());
|
||||
await this.cleanupAsync();
|
||||
return {
|
||||
enteringView,
|
||||
direction,
|
||||
animation,
|
||||
tabSwitch
|
||||
};
|
||||
}
|
||||
|
||||
canGoBack(deep: number, stackId = this.getActiveStackId()): boolean {
|
||||
@@ -122,6 +129,15 @@ export class StackController {
|
||||
this.views = insertView(this.views, enteringView, direction);
|
||||
}
|
||||
|
||||
private cleanupAsync() {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
this.cleanup();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
const activeRoute = this.activeView;
|
||||
const views = this.views;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentRef } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterDirection } from '@ionic/core';
|
||||
import { NavDirection, RouterDirection } from '@ionic/core';
|
||||
|
||||
export function insertView(views: RouteView[], view: RouteView, direction: RouterDirection) {
|
||||
if (direction === 'root') {
|
||||
@@ -44,7 +44,7 @@ export function getUrl(router: Router, activatedRoute: ActivatedRoute) {
|
||||
|
||||
export function isTabSwitch(enteringView: RouteView, leavingView: RouteView | undefined) {
|
||||
if (!leavingView) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return enteringView.stackId !== leavingView.stackId;
|
||||
}
|
||||
@@ -80,6 +80,13 @@ export function destroyView(view: RouteView | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StackEvent {
|
||||
enteringView: RouteView;
|
||||
direction: RouterDirection;
|
||||
animation: NavDirection | undefined;
|
||||
tabSwitch: boolean;
|
||||
}
|
||||
|
||||
export interface RouteView {
|
||||
id: number;
|
||||
url: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('tabs', () => {
|
||||
it('should redirect and load tab-account', async () => {
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
|
||||
await testState(1, 'account');
|
||||
});
|
||||
|
||||
it('should simulate stack + double tab click', async () => {
|
||||
@@ -20,36 +21,44 @@ describe('tabs', () => {
|
||||
await tab.$('#goto-tab1-page2').click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested']);
|
||||
await testState(1, 'account');
|
||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(true);
|
||||
|
||||
await element(by.css('#tab-button-contact')).click();
|
||||
await testTabTitle('Tab 2 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
await testState(2, 'contact');
|
||||
|
||||
await element(by.css('#tab-button-account')).click();
|
||||
tab = await testTabTitle('Tab 1 - Page 2');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||
await testState(3, 'account');
|
||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(true);
|
||||
|
||||
await element(by.css('#tab-button-account')).click();
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
await testState(3, 'account');
|
||||
});
|
||||
|
||||
it('should simulate stack + back button click', async () => {
|
||||
const tab = await getSelectedTab();
|
||||
await tab.$('#goto-tab1-page2').click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
await testState(1, 'account');
|
||||
|
||||
await element(by.css('#tab-button-contact')).click();
|
||||
await testTabTitle('Tab 2 - Page 1');
|
||||
await testState(2, 'contact');
|
||||
|
||||
await element(by.css('#tab-button-account')).click();
|
||||
await testTabTitle('Tab 1 - Page 2');
|
||||
await testState(3, 'account');
|
||||
|
||||
await element(by.css('ion-back-button')).click();
|
||||
await testTabTitle('Tab 1 - Page 1');
|
||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||
await testState(3, 'account');
|
||||
});
|
||||
|
||||
it('should switch tabs and go back', async () => {
|
||||
@@ -123,6 +132,10 @@ describe('tabs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function testState(count: number, tab: string) {
|
||||
expect(await element(by.css('#tabs-state')).getText()).toEqual(`${count}.${tab}`);
|
||||
}
|
||||
|
||||
async function testTabTitle(title: string) {
|
||||
await waitTime(600);
|
||||
const tab = await getSelectedTab();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ion-tabs>
|
||||
<ion-tabs (ionTabsDidChange)="tabChanged($event)">
|
||||
<ion-tab-bar>
|
||||
<ion-tab-button tab="account">
|
||||
<ion-label>Tab One</ion-label>
|
||||
@@ -17,3 +17,6 @@
|
||||
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
<ion-fab horizontal="end" vertical="top">
|
||||
<ion-fab-button id="tabs-state" style="width: 100px;">{{tabCounter}}.{{tabEvent}}</ion-fab-button>
|
||||
</ion-fab>
|
||||
|
||||
@@ -4,4 +4,12 @@ import { Component } from '@angular/core';
|
||||
selector: 'app-tabs',
|
||||
templateUrl: './tabs.component.html',
|
||||
})
|
||||
export class TabsComponent { }
|
||||
export class TabsComponent {
|
||||
tabCounter = 0;
|
||||
tabEvent = '';
|
||||
|
||||
tabChanged(ev: {tab: string}) {
|
||||
this.tabCounter++;
|
||||
this.tabEvent = ev.tab;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user