refactor(angular): lazy loading tabs (#16637)

Fixes #16619
This commit is contained in:
Manu MA
2018-12-08 17:23:39 +01:00
committed by GitHub
parent 86fc9a557e
commit 437ad09122
34 changed files with 11361 additions and 246 deletions

View File

@@ -1,4 +1,4 @@
import { defineCustomElements } from '@ionic/core/loader';
import * as c from '@ionic/core/loader';
import { Config } from './providers/config';
import { IonicWindow } from './types/interfaces';
@@ -32,8 +32,104 @@ export function appInitialize(config: Config) {
}
};
// define all of Ionic's custom elements
defineCustomElements(win);
c.defineCustomElement(win, [
c.IonActionSheet,
c.IonActionSheetController,
c.IonAlert,
c.IonAlertController,
c.IonAnchor,
c.IonAnimationController,
c.IonApp,
c.IonAvatar,
c.IonBackButton,
c.IonBackdrop,
c.IonBadge,
c.IonButton,
c.IonButtons,
c.IonCard,
c.IonCardContent,
c.IonCardHeader,
c.IonCardSubtitle,
c.IonCardTitle,
c.IonCheckbox,
c.IonChip,
c.IonCol,
c.IonContent,
c.IonDatetime,
c.IonFab,
c.IonFabButton,
c.IonFabList,
c.IonFooter,
c.IonGrid,
c.IonHeader,
c.IonIcon,
c.IonImg,
c.IonInfiniteScroll,
c.IonInfiniteScrollContent,
c.IonInput,
c.IonItem,
c.IonItemDivider,
c.IonItemGroup,
c.IonItemOption,
c.IonItemOptions,
c.IonItemSliding,
c.IonLabel,
c.IonList,
c.IonListHeader,
c.IonLoading,
c.IonLoadingController,
c.IonMenu,
c.IonMenuButton,
c.IonMenuController,
c.IonMenuToggle,
c.IonModal,
c.IonModalController,
c.IonNav,
c.IonNavPop,
c.IonNavPush,
c.IonNavSetRoot,
c.IonNote,
c.IonPicker,
c.IonPickerColumn,
c.IonPickerController,
c.IonPopover,
c.IonPopoverController,
c.IonRadio,
c.IonRadioGroup,
c.IonRange,
c.IonRefresher,
c.IonRefresherContent,
c.IonReorder,
c.IonReorderGroup,
c.IonRippleEffect,
c.IonRoute,
c.IonRouteRedirect,
c.IonRouter,
c.IonRouterOutlet,
c.IonRow,
c.IonSearchbar,
c.IonSegment,
c.IonSegmentButton,
c.IonSelect,
c.IonSelectOption,
c.IonSelectPopover,
c.IonSkeletonText,
c.IonSlide,
c.IonSlides,
c.IonSpinner,
c.IonSplitPane,
c.IonTabBar,
c.IonTabButton,
c.IonText,
c.IonTextarea,
c.IonThumbnail,
c.IonTitle,
c.IonToast,
c.IonToastController,
c.IonToggle,
c.IonToolbar,
c.IonVirtualScroll
]);
}
};
}

View File

@@ -5,10 +5,9 @@ export { RadioValueAccessor } from './control-value-accessors/radio-value-access
export { SelectValueAccessor } from './control-value-accessors/select-value-accessor';
export { TextValueAccessor } from './control-value-accessors/text-value-accessor';
export { IonTabs } from './navigation/ion-tabs';
export { IonBackButtonDelegate } from './navigation/ion-back-button';
export { NavDelegate } from './navigation/nav-delegate';
export { TabDelegate } from './navigation/tab-delegate';
export { TabsDelegate } from './navigation/tabs-delegate';
export { IonRouterOutlet } from './navigation/ion-router-outlet';
export { RouterLinkDelegate } from './navigation/router-link-delegate';
export { NavParams } from './navigation/nav-params';

View File

@@ -1,8 +1,9 @@
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, Input, NgZone, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core';
import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router';
import { RouteView, StackController } from './stack-controller';
import { StackController } from './stack-controller';
import { NavController } from '../../providers/nav-controller';
import { bindLifecycleEvents } from '../../providers/angular-delegate';
import { RouteView, getUrl } from './stack-utils';
@Directive({
selector: 'ion-router-outlet',
@@ -20,6 +21,8 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
private nativeEl: HTMLIonRouterOutletElement;
private hasStack = false;
tabsPrefix: string | undefined;
@Output('activate') activateEvents = new EventEmitter<any>();
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
@@ -43,18 +46,20 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
private location: ViewContainerRef,
private resolver: ComponentFactoryResolver,
@Attribute('name') name: string,
@Optional() @Attribute('stack') stack: any,
@Optional() @Attribute('tabs') tabs: string,
private changeDetector: ChangeDetectorRef,
private navCtrl: NavController,
navCtrl: NavController,
elementRef: ElementRef,
router: Router,
zone: NgZone
zone: NgZone,
activatedRoute: ActivatedRoute
) {
this.nativeEl = elementRef.nativeElement;
this.name = name || PRIMARY_OUTLET;
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, navCtrl, zone);
parentContexts.onChildOutletCreated(this.name, this as any);
this.hasStack = stack !== 'false' && stack !== false;
this.stackCtrl = new StackController(this.hasStack, this.nativeEl, router, this.navCtrl, zone);
}
ngOnDestroy(): void {
@@ -169,32 +174,32 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
}
const { direction, animated } = this.navCtrl.consumeTransition();
this.activatedView = enteringView;
this.stackCtrl.setActive(enteringView, direction, animated).then(() => {
this.stackCtrl.setActive(enteringView).then(() => {
this.activateEvents.emit(cmpRef.instance);
emitEvent(this.nativeEl);
emitEvent(this.nativeEl, enteringView!);
});
}
canGoBack(deep = 1) {
return this.stackCtrl.canGoBack(deep);
canGoBack(deep = 1, stackId?: string) {
return this.stackCtrl.canGoBack(deep, stackId);
}
pop(deep = 1) {
return this.stackCtrl.pop(deep);
pop(deep = 1, stackId?: string) {
return this.stackCtrl.pop(deep, stackId);
}
getLastUrl() {
const active = this.stackCtrl.getActive();
getLastUrl(stackId?: string) {
const active = this.stackCtrl.getLastUrl(stackId);
return active ? active.url : undefined;
}
}
function emitEvent(el: HTMLElement) {
function emitEvent(el: HTMLElement, view: RouteView) {
const ev = new CustomEvent('ionRouterOutletActivated', {
bubbles: true,
cancelable: true,
detail: { view }
});
el.dispatchEvent(ev);
}

View File

@@ -0,0 +1,69 @@
import { Component, ContentChild, HostListener, ViewChild } from '@angular/core';
import { TabButtonClickDetail } from '@ionic/core';
import { IonRouterOutlet } from './ion-router-outlet';
import { NavController } from '../../providers';
import { IonTabBar } from '../proxies';
import { RouteView } from './stack-utils';
@Component({
selector: 'ion-tabs',
template: `
<div class="tabs-inner">
<ion-router-outlet #outlet tabs="true"></ion-router-outlet>
</div>
<ng-content></ng-content>`,
styles: [`
:host {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
flex-direction: column;
width: 100%;
height: 100%;
contain: layout size style;
z-index: $z-index-page-container;
}
.tabs-inner {
position: relative;
flex: 1;
contain: layout size style;
}`
]
})
export class IonTabs {
@ViewChild('outlet', { read: IonRouterOutlet }) outlet: IonRouterOutlet;
@ContentChild(IonTabBar) tabBar: IonTabBar | undefined;
constructor(
private navCtrl: NavController,
) {}
@HostListener('ionRouterOutletActivated', ['$event.detail'])
onPageSelected(detail: {view: RouteView}) {
if (this.tabBar) {
this.tabBar.selectedTab = detail.view.stackId;
}
}
@HostListener('ionTabButtonClick', ['$event.detail'])
onTabButtonClick(detail: TabButtonClickDetail) {
const { tab, selected } = detail;
if (tab) {
const href = `${this.outlet.tabsPrefix}/${tab}`;
const url = selected
? href
: this.outlet.getLastUrl(tab) || href;
this.navCtrl.navigateBack(url, true);
}
}
}

View File

@@ -39,7 +39,6 @@ export class RouterLinkDelegate {
private updateTargetUrlAndHref() {
if (this.routerLink) {
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
console.log(href);
this.elementRef.nativeElement.href = href;
}
}

View File

@@ -2,6 +2,8 @@ import { ComponentRef, NgZone } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NavController, NavDirection } from '../../providers/nav-controller';
import { RouterDirection } from '@ionic/core';
import { RouteView, computeStackId, destroyView, getUrl, insertView, isTabSwitch, toSegments } from './stack-utils';
export class StackController {
@@ -10,50 +12,65 @@ export class StackController {
private views: RouteView[] = [];
private runningTransition?: Promise<boolean>;
private skipTransition = false;
private tabsPrefix: string[] | undefined;
private activeView: RouteView | undefined;
private nextId = 0;
constructor(
private stack: boolean,
tabsPrefix: string | undefined,
private containerEl: HTMLIonRouterOutletElement,
private router: Router,
private navCtrl: NavController,
private zone: NgZone,
) {}
) {
this.tabsPrefix = tabsPrefix ? toSegments(tabsPrefix) : undefined;
}
createView(enteringRef: ComponentRef<any>, activatedRoute: ActivatedRoute): RouteView {
const url = getUrl(this.router, activatedRoute);
return {
id: this.nextId++,
ref: enteringRef,
element: (enteringRef && enteringRef.location && enteringRef.location.nativeElement) as HTMLElement,
url: this.getUrl(activatedRoute)
stackId: computeStackId(this.tabsPrefix, url),
url,
};
}
getExistingView(activatedRoute: ActivatedRoute): RouteView | undefined {
const activatedUrlKey = this.getUrl(activatedRoute);
const activatedUrlKey = getUrl(this.router, activatedRoute);
return this.views.find(vw => vw.url === activatedUrlKey);
}
async setActive(enteringView: RouteView, direction: NavDirection, animated: boolean) {
const leavingView = this.getActive();
async setActive(enteringView: RouteView) {
let { direction, animated } = this.navCtrl.consumeTransition();
const leavingView = this.activeView;
if (isTabSwitch(enteringView, leavingView)) {
direction = 'back';
animated = false;
}
this.insertView(enteringView, direction);
await this.transition(enteringView, leavingView, direction, animated, this.canGoBack(1), false);
this.cleanup();
}
canGoBack(deep: number): boolean {
return this.views.length > deep;
canGoBack(deep: number, stackId = this.getActiveStackId()): boolean {
return this.getStack(stackId).length > deep;
}
pop(deep: number) {
pop(deep: number, stackId = this.getActiveStackId()) {
this.zone.run(() => {
const view = this.views[this.views.length - deep - 1];
const views = this.getStack(stackId);
const view = views[views.length - deep - 1];
this.navCtrl.navigateBack(view.url);
});
}
startBackTransition() {
startBackTransition(stackId?: string) {
const views = this.getStack(stackId);
this.transition(
this.views[this.views.length - 2], // entering view
this.views[this.views.length - 1], // leaving view
views[views.length - 2], // entering view
views[views.length - 1], // leaving view
'back',
true,
true,
@@ -68,55 +85,41 @@ export class StackController {
}
}
getLastUrl(stackId?: string) {
const views = this.getStack(stackId);
return views.length > 0 ? views[views.length - 1] : undefined;
}
private insertView(enteringView: RouteView, direction: NavDirection) {
// no stack
if (!this.stack) {
this.views = [enteringView];
return;
}
private getActiveStackId(): string | undefined {
return this.activeView ? this.activeView.stackId : undefined;
}
// stack setRoot
if (direction === 'root') {
this.views = [enteringView];
return;
}
private getStack(stackId: string | undefined) {
return this.views.filter(v => v.stackId === stackId);
}
// stack
const index = this.views.indexOf(enteringView);
if (index >= 0) {
this.views = this.views.slice(0, index + 1);
} else {
if (direction === 'forward') {
this.views.push(enteringView);
} else {
this.views = [enteringView];
}
}
private insertView(enteringView: RouteView, direction: RouterDirection) {
this.activeView = enteringView;
this.views = insertView(this.views, enteringView, direction);
}
private cleanup() {
const activeRoute = this.activeView;
const views = this.views;
this.viewsSnapshot
.filter(view => !views.includes(view))
.forEach(view => destroyView(view));
for (let i = 0; i < views.length - 1; i++) {
const view = views[i];
const element = view.element;
element.setAttribute('aria-hidden', 'true');
element.classList.add('ion-page-hidden');
// TODO
// view.ref.changeDetectorRef.detach();
}
views.forEach(view => {
if (view !== activeRoute) {
const element = view.element;
element.setAttribute('aria-hidden', 'true');
element.classList.add('ion-page-hidden');
}
});
this.viewsSnapshot = views.slice();
}
getActive(): RouteView | undefined {
const views = this.views;
return views.length > 0 ? views[views.length - 1] : undefined;
}
private async transition(
enteringView: RouteView | undefined,
leavingView: RouteView | undefined,
@@ -158,23 +161,4 @@ export class StackController {
await this.runningTransition;
}
}
private getUrl(activatedRoute: ActivatedRoute) {
const urlTree = this.router.createUrlTree(['.'], { relativeTo: activatedRoute });
return this.router.serializeUrl(urlTree);
}
}
function destroyView(view: RouteView) {
if (view) {
// TODO lifecycle event
view.ref.destroy();
}
}
export interface RouteView {
url: string;
element: HTMLElement;
ref: ComponentRef<any>;
savedData?: any;
}

View File

@@ -0,0 +1,90 @@
import { ComponentRef } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterDirection } from '@ionic/core';
export function insertView(views: RouteView[], view: RouteView, direction: RouterDirection) {
if (direction === 'root') {
return setRoot(views, view);
} else if (direction === 'forward') {
return setForward(views, view);
} else {
return setBack(views, view);
}
}
function setRoot(views: RouteView[], view: RouteView) {
views = views.filter(v => v.stackId !== view.stackId);
views.push(view);
return views;
}
function setForward(views: RouteView[], view: RouteView) {
const index = views.indexOf(view);
if (index >= 0) {
views = views.filter(v => v.stackId !== view.stackId || v.id <= view.id);
} else {
views.push(view);
}
return views;
}
function setBack(views: RouteView[], view: RouteView) {
const index = views.indexOf(view);
if (index >= 0) {
return views.filter(v => v.stackId !== view.stackId || v.id <= view.id);
} else {
return setRoot(views, view);
}
}
export function getUrl(router: Router, activatedRoute: ActivatedRoute) {
const urlTree = router.createUrlTree(['.'], { relativeTo: activatedRoute });
return router.serializeUrl(urlTree);
}
export function isTabSwitch(enteringView: RouteView, leavingView: RouteView | undefined) {
if (!leavingView) {
return false;
}
return enteringView.stackId !== leavingView.stackId;
}
export function computeStackId(prefixUrl: string[] | undefined, url: string) {
if (!prefixUrl) {
return undefined;
}
const segments = toSegments(url);
for (let i = 0; i < segments.length; i++) {
if (i >= prefixUrl.length) {
return segments[i];
}
if (segments[i] !== prefixUrl[i]) {
return undefined;
}
}
return undefined;
}
export function toSegments(path: string): string[] {
return path
.split('/')
.map(s => s.trim())
.filter(s => s !== '');
}
export function destroyView(view: RouteView) {
if (view) {
// TODO lifecycle event
view.ref.destroy();
}
}
export interface RouteView {
id: number;
url: string;
stackId: string | undefined;
element: HTMLElement;
ref: ComponentRef<any>;
savedData?: any;
}

View File

@@ -1,46 +0,0 @@
import { ComponentFactoryResolver, ContentChild, Directive, ElementRef, HostListener, Injector, ViewContainerRef } from '@angular/core';
import { AngularDelegate } from '../../providers/angular-delegate';
import { IonRouterOutlet } from './ion-router-outlet';
@Directive({
selector: 'ion-tab'
})
export class TabDelegate {
@ContentChild(IonRouterOutlet) outlet?: IonRouterOutlet;
private nativeEl: HTMLIonTabElement;
constructor(
elementRef: ElementRef,
resolver: ComponentFactoryResolver,
injector: Injector,
angularDelegate: AngularDelegate,
location: ViewContainerRef
) {
this.nativeEl = elementRef.nativeElement;
this.nativeEl.delegate = angularDelegate.create(resolver, injector, location);
}
get tab() {
return this.nativeEl.tab;
}
getLastUrl() {
return this.outlet ? this.outlet.getLastUrl() : undefined;
}
@HostListener('ionRouterOutletActivated')
async onNavChanged() {
const tab = this.nativeEl;
await tab.componentOnReady();
const tabs = tab.closest('ion-tabs');
if (tabs) {
await tabs.componentOnReady();
await tabs.select(tab);
}
}
}

View File

@@ -1,37 +0,0 @@
import { ContentChildren, Directive, ElementRef, HostListener, Optional, QueryList } from '@angular/core';
import { TabDelegate } from './tab-delegate';
import { TabButtonClickDetail } from '@ionic/core';
import { NavController } from '../../providers';
@Directive({
selector: 'ion-tabs'
})
export class TabsDelegate {
@ContentChildren(TabDelegate) tabs!: QueryList<TabDelegate>;
constructor(
@Optional() private navCtrl: NavController,
elementRef: ElementRef
) {
const nativeEl = elementRef.nativeElement as HTMLIonTabsElement;
nativeEl.useRouter = true;
}
urlForTab(tab: string) {
const tabDelegate = this.tabs.find(item => item.tab === tab);
return tabDelegate ? tabDelegate.getLastUrl() : undefined;
}
@HostListener('ionTabButtonClick', ['$event.detail'])
onTabbarClick(detail: TabButtonClickDetail) {
const { tab, href, selected } = detail;
if (tab && href) {
const url = selected
? href
: this.urlForTab(tab) || href;
this.navCtrl.navigateBack(url, true);
}
}
}

View File

@@ -67,10 +67,8 @@ export const DIRECTIVES = [
d.IonSlides,
d.IonSpinner,
d.IonSplitPane,
d.IonTab,
d.IonTabBar,
d.IonTabButton,
d.IonTabs,
d.IonText,
d.IonTextarea,
d.IonThumbnail,

View File

@@ -834,18 +834,6 @@ export class IonSplitPane {
}
}
export declare interface IonTab extends StencilComponents<'IonTab'> {}
@Component({ selector: 'ion-tab', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['active', 'delegate', 'tab', 'component'] })
export class IonTab {
constructor(c: ChangeDetectorRef, r: ElementRef) {
c.detach();
const el = r.nativeElement;
proxyMethods(this, el, ['setActive']);
proxyInputs(this, el, ['active', 'delegate', 'tab', 'component']);
}
}
export declare interface IonTabBar extends StencilComponents<'IonTabBar'> {}
@Component({ selector: 'ion-tab-bar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['mode', 'color', 'selectedTab', 'translucent'] })
export class IonTabBar {
@@ -872,23 +860,6 @@ export class IonTabButton {
}
}
export declare interface IonTabs extends StencilComponents<'IonTabs'> {}
@Component({ selector: 'ion-tabs', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['useRouter'] })
export class IonTabs {
ionChange!: EventEmitter<CustomEvent>;
ionNavWillLoad!: EventEmitter<CustomEvent>;
ionNavWillChange!: EventEmitter<CustomEvent>;
ionNavDidChange!: EventEmitter<CustomEvent>;
constructor(c: ChangeDetectorRef, r: ElementRef) {
c.detach();
const el = r.nativeElement;
proxyMethods(this, el, ['select', 'setRouteId', 'getRouteId', 'getTab', 'getSelected']);
proxyInputs(this, el, ['useRouter']);
proxyOutputs(this, el, ['ionChange', 'ionNavWillLoad', 'ionNavWillChange', 'ionNavDidChange']);
}
}
export declare interface IonText extends StencilComponents<'IonText'> {}
@Component({ selector: 'ion-text', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['color', 'mode'] })
export class IonText {

View File

@@ -76,10 +76,8 @@ const DECLARATIONS = [
d.IonSlides,
d.IonSpinner,
d.IonSplitPane,
d.IonTab,
d.IonTabBar,
d.IonTabButton,
d.IonTabs,
d.IonText,
d.IonTextarea,
d.IonThumbnail,
@@ -87,6 +85,8 @@ const DECLARATIONS = [
d.IonToolbar,
d.IonTitle,
c.IonTabs,
// ngModel accessors
c.BooleanValueAccessor,
c.NumericValueAccessor,
@@ -98,8 +98,6 @@ const DECLARATIONS = [
c.IonRouterOutlet,
c.IonBackButtonDelegate,
c.NavDelegate,
c.TabDelegate,
c.TabsDelegate,
c.RouterLinkDelegate,
// virtual scroll

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { NavigationExtras, NavigationStart, Router, UrlTree } from '@angular/router';
import { Platform } from './platform';
import { RouterDirection } from '@ionic/core';
export type NavDirection = 'forward' | 'back' | 'root' | 'auto';
@@ -10,7 +11,7 @@ export class NavController {
private direction: NavDirection = DEFAULT_DIRECTION;
private animated = DEFAULT_ANIMATED;
private guessDirection: NavDirection = 'forward';
private guessDirection: RouterDirection = 'forward';
private guessAnimation = false;
private lastNavId = -1;
@@ -74,7 +75,7 @@ export class NavController {
}
consumeTransition() {
let direction: NavDirection = 'root';
let direction: RouterDirection = 'root';
let animated = false;
if (this.direction === 'auto') {

View File

@@ -0,0 +1,85 @@
import { browser, element, by, ElementFinder } from 'protractor';
import { waitTime, testStack, handleErrorMessages } from './utils';
describe('tabs', () => {
beforeEach(async () => {
await browser.get('/tabs');
});
afterEach(() => {
handleErrorMessages();
});
it('should redirect and load tab-account', async () => {
await testTabTitle('Tab 1 - Page 1');
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
});
it('should simulate stack + double tab click', async () => {
const tab = await getSelectedTab() as ElementFinder;
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 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 element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 2');
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
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']);
});
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 element(by.css('#tab-button-contact')).click();
await testTabTitle('Tab 2 - Page 1');
await element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 2');
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']);
});
it('should switch tabs and go back', async () => {
await element(by.css('#tab-button-contact')).click();
const tab = await testTabTitle('Tab 2 - Page 1');
await tab.$('#goto-tab1-page1').click();
await testTabTitle('Tab 1 - Page 1');
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
});
it('should switch tabs and go to nested', async () => {
await element(by.css('#tab-button-contact')).click();
const tab = await testTabTitle('Tab 2 - Page 1');
await tab.$('#goto-tab1-page2').click();
await testTabTitle('Tab 1 - Page 2');
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab2', 'app-tabs-tab1-nested']);
});
});
async function testTabTitle(title: string) {
await waitTime(600);
const tab = await getSelectedTab();
expect(await tab.$('ion-title').getText()).toEqual(title);
return tab;
}
async function getSelectedTab(): Promise<ElementFinder> {
const tabs = element.all(by.css('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)'));
expect(await tabs.count()).toEqual(1);
const tab = tabs.first();
return tab;
}

View File

@@ -59,7 +59,7 @@ export async function testLifeCycle(selector: string, expected: LifeCycleCount)
}
export async function testStack(selector: string, expected: string[]) {
const children = browser.executeScript(`
const children = await browser.executeScript(`
return Array.from(
document.querySelector('${selector}').children
).map(el => el.tagName.toLowerCase());

View File

@@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start": "npm run sync && ng serve",
"sync:build": "sh scripts/build-ionic.sh",
"sync": "sh scripts/sync.sh",
"build": "ng build --prod --no-progress",

View File

@@ -6,6 +6,9 @@ import { RouterLinkComponent } from './router-link/router-link.component';
import { RouterLinkPageComponent } from './router-link-page/router-link-page.component';
import { HomePageComponent } from './home-page/home-page.component';
import { TabsComponent } from './tabs/tabs.component';
import { TabsTab1Component } from './tabs-tab1/tabs-tab1.component';
import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.component';
import { TabsTab2Component } from './tabs-tab2/tabs-tab2.component';
const routes: Routes = [
{ path: '', component: HomePageComponent },
@@ -13,7 +16,40 @@ const routes: Routes = [
{ path: 'modals', component: ModalComponent },
{ path: 'router-link', component: RouterLinkComponent },
{ path: 'router-link-page', component: RouterLinkPageComponent },
{ path: 'tabs', component: TabsComponent }
{ path: 'tabs', redirectTo: '/tabs/account', pathMatch: 'full' },
{
path: 'tabs',
component: TabsComponent,
children: [
{
path: 'account',
children: [
{
path: 'nested/:id',
component: TabsTab1NestedComponent
},
{
path: '',
component: TabsTab1Component
}
]
},
{
path: 'contact',
children: [
{
path: 'one',
component: TabsTab2Component
},
{
path: '',
redirectTo: 'one',
pathMatch: 'full'
}
]
}
]
}
];
@NgModule({

View File

@@ -12,6 +12,9 @@ import { RouterLinkComponent } from './router-link/router-link.component';
import { RouterLinkPageComponent } from './router-link-page/router-link-page.component';
import { HomePageComponent } from './home-page/home-page.component';
import { TabsComponent } from './tabs/tabs.component';
import { TabsTab1Component } from './tabs-tab1/tabs-tab1.component';
import { TabsTab2Component } from './tabs-tab2/tabs-tab2.component';
import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.component';
@NgModule({
declarations: [
@@ -22,7 +25,10 @@ import { TabsComponent } from './tabs/tabs.component';
RouterLinkComponent,
RouterLinkPageComponent,
HomePageComponent,
TabsComponent
TabsComponent,
TabsTab1Component,
TabsTab2Component,
TabsTab1NestedComponent
],
imports: [
BrowserModule,

View File

@@ -0,0 +1,16 @@
<ion-header>
<ion-toolbar>
<ion-title>Tab 1 - Page 2</ion-title>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content padding>
<h1>Welcome to NESTED PAGE</h1>
<p>
<ion-button routerLink="/tabs/account" id="goto-tab1-page1">Go to Tab 1 - Page 1</ion-button>
<ion-button routerLink="/tabs/contact" id="goto-tab2-page1">Go to Tab 2 - Page 1</ion-button>
</p>
</ion-content>

View File

@@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-tabs-tab1-nested',
templateUrl: './tabs-tab1-nested.component.html',
})
export class TabsTab1NestedComponent { }

View File

@@ -0,0 +1,12 @@
<ion-header>
<ion-toolbar>
<ion-title>Tab 1 - Page 1</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<h1>Welcome to Tab1</h1>
<p>
<ion-button routerLink="/tabs/account/nested/12" id="goto-tab1-page2">Go to Page 2</ion-button>
</p>
</ion-content>

View File

@@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-tabs-tab1',
templateUrl: './tabs-tab1.component.html',
})
export class TabsTab1Component { }

View File

@@ -0,0 +1,13 @@
<ion-header>
<ion-toolbar>
<ion-title>Tab 2 - Page 1</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<h1>Welcome to Tab 2</h1>
<p>
<ion-button routerLink="/tabs/account" id="goto-tab1-page1">Go to Tab 1 - Page 1</ion-button>
<ion-button routerLink="/tabs/account/nested/12" id="goto-tab1-page2">Go to Tab 1 - Page 2</ion-button>
</p>
</ion-content>

View File

@@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-tabs-tab2',
templateUrl: './tabs-tab2.component.html',
})
export class TabsTab2Component { }

View File

@@ -1,21 +1,12 @@
<ion-tabs>
<ion-tab tab="tab-one">
<ion-router-outlet name="tab-one"></ion-router-outlet>
</ion-tab>
<ion-tab tab="tab-two">
<ion-router-outlet name="tab-one"></ion-router-outlet>
</ion-tab>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab-one">
<ion-tab-bar>
<ion-tab-button tab="account" id="account-tab-button">
<ion-label>Tab One</ion-label>
<ion-icon name="add"></ion-icon>
</ion-tab-button>
<ion-tab-button tab="tab-two">
<ion-tab-button tab="contact" id="contact-tab-button">
<ion-label>Tab Two</ion-label>
<ion-icon name="plane"></ion-icon>
<ion-icon name="logo-ionic"></ion-icon>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>

View File

@@ -4,6 +4,4 @@ import { Component } from '@angular/core';
selector: 'app-tabs',
templateUrl: './tabs.component.html',
})
export class TabsComponent {
}
export class TabsComponent { }