refactor(nav): queue based transitions

This commit is contained in:
Adam Bradley
2016-09-13 17:13:43 -05:00
parent bc7d328bc0
commit ed221eff1d
18 changed files with 2970 additions and 2083 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
export interface NavOptions {
animate?: boolean;
animation?: string;
direction?: string;
duration?: number;
easing?: string;
id?: string;
keyboardClose?: boolean;
preload?: boolean;
transitionDelay?: number;
progressAnimation?: boolean;
climbNav?: boolean;
ev?: any;
}
export const DIRECTION_BACK = 'back';
export const DIRECTION_FORWARD = 'forward';

View File

@@ -1,6 +1,9 @@
import { Directive, HostListener, Input, Optional } from '@angular/core';
import { NavController } from './nav-controller';
import { noop } from '../../util/util';
import { AfterViewInit, Directive, HostBinding, HostListener, OnChanges, Optional } from '@angular/core';
import { DeepLinker } from '../../navigation/deep-linker';
import { NavController } from '../../navigation/nav-controller';
import { ViewController } from '../../navigation/view-controller';
/**
* @name NavPop
@@ -27,9 +30,9 @@ import { noop } from '../../util/util';
})
export class NavPop {
constructor(@Optional() private _nav: NavController) {
constructor(@Optional() public _nav: NavController) {
if (!_nav) {
console.error('nav-pop must be within a NavController');
console.error('navPop must be within a NavController');
}
}
@@ -37,7 +40,7 @@ export class NavPop {
onClick(): boolean {
// If no target, or if target is _self, prevent default browser behavior
if (this._nav) {
this._nav.pop(null, noop);
this._nav.pop(null, null);
return false;
}
@@ -45,3 +48,39 @@ export class NavPop {
}
}
/**
* @private
*/
@Directive({
selector: 'a[navPop]'
})
export class NavPopAnchor implements OnChanges, AfterViewInit {
constructor(
@Optional() public host: NavPop,
public linker: DeepLinker,
@Optional() public viewCtrl: ViewController) {}
@HostBinding() href: string;
updateHref() {
if (this.host && this.viewCtrl) {
const previousView = this.host._nav.getPrevious(this.viewCtrl);
this.href = (previousView && this.linker.createUrl(this.host._nav, this.viewCtrl.component, this.viewCtrl.data)) || '#';
} else {
this.href = '#';
}
}
ngOnChanges() {
this.updateHref();
}
ngAfterViewInit() {
this.updateHref();
}
}

View File

@@ -1,7 +1,7 @@
import { Directive, HostListener, Input, Optional } from '@angular/core';
import { AfterViewInit, Directive, Host, HostBinding, HostListener, Input, Optional, OnChanges } from '@angular/core';
import { NavController } from './nav-controller';
import { noop } from '../../util/util';
import { DeepLinker } from '../../navigation/deep-linker';
import { NavController } from '../../navigation/nav-controller';
/**
* @name NavPush
@@ -60,7 +60,7 @@ export class NavPush {
@Input() navParams: {[k: string]: any};
constructor(@Optional() private _nav: NavController) {
constructor(@Optional() public _nav: NavController) {
if (!_nav) {
console.error('navPush must be within a NavController');
}
@@ -68,13 +68,44 @@ export class NavPush {
@HostListener('click')
onClick(): boolean {
// If no target, or if target is _self, prevent default browser behavior
if (this._nav) {
this._nav.push(this.navPush, this.navParams, noop);
this._nav.push(this.navPush, this.navParams, null);
return false;
}
return true;
}
}
/**
* @private
*/
@Directive({
selector: 'a[navPush]'
})
export class NavPushAnchor implements OnChanges, AfterViewInit {
constructor(
@Host() public host: NavPush,
@Optional() public linker: DeepLinker) {}
@HostBinding() href: string;
updateHref() {
if (this.host && this.linker) {
this.href = this.linker.createUrl(this.host._nav, this.host.navPush, this.host.navParams) || '#';
} else {
this.href = '#';
}
}
ngOnChanges() {
this.updateHref();
}
ngAfterViewInit() {
this.updateHref();
}
}

View File

@@ -1,12 +1,15 @@
import { AfterViewInit, Component, ComponentResolver, ElementRef, Input, Optional, NgZone, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { AfterViewInit, Component, ComponentFactoryResolver, ElementRef, Input, Optional, NgZone, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
import { Keyboard } from '../../util/keyboard';
import { DeepLinker } from '../../navigation/deep-linker';
import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util';
import { NavControllerBase } from './nav-controller-base';
import { ViewController } from './view-controller';
import { Keyboard } from '../../util/keyboard';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { NavOptions } from '../../navigation/nav-util';
import { TransitionController } from '../../transitions/transition-controller';
import { ViewController } from '../../navigation/view-controller';
/**
* @name Nav
@@ -24,32 +27,27 @@ import { ViewController } from './view-controller';
*
* ```ts
* import { Component } from '@angular/core';
* import { ionicBootstrap } from 'ionic-angular';
* import { GettingStartedPage } from './getting-started';
*
* @Component({
* template: `<ion-nav [root]="root"></ion-nav>`
* })
* class MyApp {
* private root: any = GettingStartedPage;
* root = GettingStartedPage;
*
* constructor(){
* }
* }
*
* ionicBootstrap(MyApp);
* ```
*
*
* @demo /docs/v2/demos/navigation/
* @see {@link /docs/v2/components#navigation Navigation Component Docs}
*/
@Component({
selector: 'ion-nav',
template: `
<div #viewport nav-viewport></div>
<div class="nav-decor"></div>
`,
template:
'<div #viewport nav-viewport></div>' +
'<div class="nav-decor"></div>',
encapsulation: ViewEncapsulation.None,
})
export class Nav extends NavControllerBase implements AfterViewInit {
@@ -65,16 +63,17 @@ export class Nav extends NavControllerBase implements AfterViewInit {
elementRef: ElementRef,
zone: NgZone,
renderer: Renderer,
compiler: ComponentResolver,
gestureCtrl: GestureController
cfr: ComponentFactoryResolver,
gestureCtrl: GestureController,
transCtrl: TransitionController,
@Optional() linker: DeepLinker
) {
super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
super(parent, app, config, keyboard, elementRef, zone, renderer, cfr, gestureCtrl, transCtrl, linker);
if (viewCtrl) {
// an ion-nav can also act as an ion-page within a parent ion-nav
// this would happen when an ion-nav nests a child ion-nav.
viewCtrl.setContent(this);
viewCtrl.setContentRef(elementRef);
viewCtrl._setContent(this);
}
if (parent) {
@@ -89,7 +88,7 @@ export class Nav extends NavControllerBase implements AfterViewInit {
} else if (app && !app.getRootNav()) {
// a root nav has not been registered yet with the app
// this is the root navcontroller for the entire app
app.setRootNav(this);
app._setRootNav(this);
}
}
@@ -101,17 +100,26 @@ export class Nav extends NavControllerBase implements AfterViewInit {
this.setViewport(val);
}
/**
* @private
*/
ngAfterViewInit() {
this._hasInit = true;
if (this._root) {
this.push(this._root);
let navSegment = this._linker.initNav(this);
if (navSegment && navSegment.component) {
// there is a segment match in the linker
this.setPages(this._linker.initViews(navSegment), null, null);
} else if (this._root) {
// no segment match, so use the root property
this.push(this._root, this.rootParams, {
isNavRoot: (<any>this._app.getRootNav() === this)
}, null);
}
}
goToRoot(opts: NavOptions) {
this.setRoot(this._root, this.rootParams, opts, null);
}
/**
* @input {Page} The Page component to load as the root page within this nav.
*/
@@ -127,6 +135,11 @@ export class Nav extends NavControllerBase implements AfterViewInit {
}
}
/**
* @input {object} Any nav-params to pass to the root page of this nav.
*/
@Input() rootParams: any;
/**
* @input {boolean} Whether it's possible to swipe-to-go-back on this nav controller or not.
*/
@@ -138,4 +151,11 @@ export class Nav extends NavControllerBase implements AfterViewInit {
this._sbEnabled = isTrueProperty(val);
}
/**
* @private
*/
destroy() {
this.destroy();
}
}

View File

@@ -1,18 +1,20 @@
import { ComponentResolver, Directive, ElementRef, forwardRef, Inject, NgZone, Optional, Renderer, ViewContainerRef } from '@angular/core';
import { ComponentFactoryResolver, Directive, ElementRef, forwardRef, Inject, NgZone, Optional, Renderer, ViewContainerRef } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
import { DeepLinker } from '../../navigation/deep-linker';
import { GestureController } from '../../gestures/gesture-controller';
import { Keyboard } from '../../util/keyboard';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { TransitionController } from '../../transitions/transition-controller';
/**
* @private
*/
@Directive({
selector: '[nav-portal]'
selector: '[overlay-portal]'
})
export class NavPortal extends NavControllerBase {
export class OverlayPortal extends NavControllerBase {
constructor(
@Inject(forwardRef(() => App)) app: App,
config: Config,
@@ -20,15 +22,16 @@ export class NavPortal extends NavControllerBase {
elementRef: ElementRef,
zone: NgZone,
renderer: Renderer,
compiler: ComponentResolver,
cfr: ComponentFactoryResolver,
gestureCtrl: GestureController,
transCtrl: TransitionController,
@Optional() linker: DeepLinker,
viewPort: ViewContainerRef
) {
super(null, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
super(null, app, config, keyboard, elementRef, zone, renderer, cfr, gestureCtrl, transCtrl, linker);
this._isPortal = true;
this._init = true;
this.setViewport(viewPort);
app.setPortal(this);
// on every page change make sure the portal has
// dismissed any views that should be auto dismissed on page change

View File

@@ -1,116 +0,0 @@
import { LifeCycleEvent, ViewController } from '../../../../src';
export function run() {
describe('ViewController', () => {
afterEach(() => {
if ( subscription ) {
subscription.unsubscribe();
}
});
describe('willEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.willEnter.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireWillEnter();
}, 10000);
});
describe('didEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.didEnter.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireDidEnter();
}, 10000);
});
describe('willLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.willLeave.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireWillLeave();
}, 10000);
});
describe('didLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.didLeave.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireDidLeave();
}, 10000);
});
describe('willUnload', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.willUnload.subscribe((event: LifeCycleEvent) => {
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireWillUnload();
}, 10000);
});
describe('destroy', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.didUnload.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.destroy();
}, 10000);
});
});
let subscription: any = null;
class FakePage {}
}

View File

@@ -1,15 +1,17 @@
import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core';
import { ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, EventEmitter, Input, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
import { DeepLinker } from '../../navigation/deep-linker';
import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util';
import { Keyboard } from '../../util/keyboard';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavOptions } from '../nav/nav-interfaces';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { NavOptions } from '../../navigation/nav-util';
import { TabButton } from './tab-button';
import { Tabs } from './tabs';
import { ViewController } from '../nav/view-controller';
import { TransitionController } from '../../transitions/transition-controller';
import { ViewController } from '../../navigation/view-controller';
/**
@@ -93,14 +95,14 @@ import { ViewController } from '../nav/view-controller';
* the tabs.
*
* ```html
* <ion-tabs preloadTabs="false">
* <ion-tabs>
* <ion-tab (ionSelect)="chat()"></ion-tab>
* </ion-tabs>
* ```
*
* ```ts
* export class Tabs {
* constructor(private modalCtrl: ModalController) {
* constructor(public modalCtrl: ModalController) {
*
* }
*
@@ -120,23 +122,40 @@ import { ViewController } from '../nav/view-controller';
*/
@Component({
selector: 'ion-tab',
template:
'<div #viewport></div><div class="nav-decor"></div>',
host: {
'[class.show-tab]': 'isSelected',
'[attr.id]': '_tabId',
'[attr.aria-labelledby]': '_btnId',
'role': 'tabpanel'
},
template: '<div #viewport></div><div class="nav-decor"></div>',
encapsulation: ViewEncapsulation.None,
})
export class Tab extends NavControllerBase {
private _isInitial: boolean;
private _isEnabled: boolean = true;
private _isShown: boolean = true;
private _tabId: string;
private _btnId: string;
private _loaded: boolean;
private _loadTmr: any;
/**
* @private
*/
_isInitial: boolean;
/**
* @private
*/
_isEnabled: boolean = true;
/**
* @private
*/
_isShown: boolean = true;
/**
* @private
*/
_tabId: string;
/**
* @private
*/
_btnId: string;
/**
* @private
*/
_loaded: boolean;
/**
* @private
@@ -158,6 +177,11 @@ export class Tab extends NavControllerBase {
*/
@Input() rootParams: any;
/**
* @input {string} The URL path name to represent this tab within the URL.
*/
@Input() tabUrlPath: string;
/**
* @input {string} The title of the tab button.
*/
@@ -221,21 +245,23 @@ export class Tab extends NavControllerBase {
@Output() ionSelect: EventEmitter<Tab> = new EventEmitter<Tab>();
constructor(
@Inject(forwardRef(() => Tabs)) public parent: Tabs,
parent: Tabs,
app: App,
config: Config,
keyboard: Keyboard,
elementRef: ElementRef,
zone: NgZone,
renderer: Renderer,
compiler: ComponentResolver,
cfr: ComponentFactoryResolver,
private _cd: ChangeDetectorRef,
gestureCtrl: GestureController
gestureCtrl: GestureController,
transCtrl: TransitionController,
@Optional() private linker: DeepLinker
) {
// A Tab is a NavController for its child pages
super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
super(parent, app, config, keyboard, elementRef, zone, renderer, cfr, gestureCtrl, transCtrl, linker);
parent.add(this);
this.id = parent.add(this);
this._tabId = 'tabpanel-' + this.id;
this._btnId = 'tab-' + this.id;
@@ -261,52 +287,33 @@ export class Tab extends NavControllerBase {
*/
load(opts: NavOptions, done?: Function) {
if (!this._loaded && this.root) {
this.push(this.root, this.rootParams, opts, () => {
done(true);
});
this.push(this.root, this.rootParams, opts, done);
this._loaded = true;
} else {
done(false);
done(true);
}
}
/**
* @private
*/
preload(wait: number) {
this._loadTmr = setTimeout(() => {
if (!this._loaded) {
console.debug('Tabs, preload', this.id);
this.load({
animate: false,
preload: true
}, function(){});
}
}, wait);
}
/**
* @private
*/
loadPage(viewCtrl: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) {
let isTabSubPage = (this.parent.subPages && viewCtrl.index > 0);
_viewInsert(viewCtrl: ViewController, componentRef: ComponentRef<any>, viewport: ViewContainerRef) {
const isTabSubPage = (this.parent._subPages && viewCtrl.index > 0);
if (isTabSubPage) {
viewport = this.parent.portal;
}
super.loadPage(viewCtrl, viewport, opts, () => {
if (isTabSubPage) {
// add the .tab-subpage css class to tabs pages that should act like subpages
let pageEleRef = viewCtrl.pageRef();
if (pageEleRef) {
this._renderer.setElementClass(pageEleRef.nativeElement, 'tab-subpage', true);
}
super._viewInsert(viewCtrl, componentRef, viewport);
if (isTabSubPage) {
// add the .tab-subpage css class to tabs pages that should act like subpages
const pageEleRef = viewCtrl.pageRef();
if (pageEleRef) {
this._renderer.setElementClass(pageEleRef.nativeElement, 'tab-subpage', true);
}
done();
});
}
}
/**
@@ -315,6 +322,9 @@ export class Tab extends NavControllerBase {
setSelected(isSelected: boolean) {
this.isSelected = isSelected;
this.setElementClass('show-tab', isSelected);
this.setElementAttribute('aria-hidden', (!isSelected).toString());
if (isSelected) {
// this is the selected tab, detect changes
this._cd.reattach();
@@ -335,9 +345,18 @@ export class Tab extends NavControllerBase {
/**
* @private
*/
ngOnDestroy() {
clearTimeout(this._loadTmr);
super.ngOnDestroy();
updateHref(component: any, data: any) {
if (this.btn && this.linker) {
let href = this.linker.createUrl(this, component, data) || '#';
this.btn.updateHref(href);
}
}
/**
* @private
*/
destroy() {
this.destroy();
}
}

View File

@@ -1,22 +1,19 @@
import { Component, ElementRef, EventEmitter, Input, Output, Optional, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { NgClass, NgFor, NgIf } from '@angular/common';
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, Optional, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Badge } from '../badge/badge';
import { Config } from '../../config/config';
import { Content } from '../content/content';
import { Icon } from '../icon/icon';
import { DeepLinker } from '../../navigation/deep-linker';
import { Ion } from '../ion';
import { isBlank, isPresent, isTrueProperty } from '../../util/util';
import { isBlank } from '../../util/util';
import { nativeRaf } from '../../util/dom';
import { NavController } from '../nav/nav-controller';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavOptions, DIRECTION_FORWARD } from '../nav/nav-interfaces';
import { NavController } from '../../navigation/nav-controller';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { NavOptions, DIRECTION_SWITCH } from '../../navigation/nav-util';
import { Platform } from '../../platform/platform';
import { Tab } from './tab';
import { TabButton } from './tab-button';
import { TabHighlight } from './tab-highlight';
import { ViewController } from '../nav/view-controller';
import { ViewController } from '../../navigation/view-controller';
/**
@@ -143,72 +140,61 @@ import { ViewController } from '../nav/view-controller';
*/
@Component({
selector: 'ion-tabs',
template: `
<ion-tabbar role="tablist" #tabbar>
<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" [class.tab-disabled]="!t.enabled" [class.tab-hidden]="!t.show" role="tab" href="#" (ionSelect)="select($event)">
<ion-icon *ngIf="t.tabIcon" [name]="t.tabIcon" [isActive]="t.isSelected" class="tab-button-icon"></ion-icon>
<span *ngIf="t.tabTitle" class="tab-button-text">{{t.tabTitle}}</span>
<ion-badge *ngIf="t.tabBadge" class="tab-badge" [ngClass]="\'badge-\' + t.tabBadgeStyle">{{t.tabBadge}}</ion-badge>
<ion-button-effect></ion-button-effect>
</a>
<tab-highlight></tab-highlight>
</ion-tabbar>
<ng-content></ng-content>
<div #portal tab-portal></div>
`,
directives: [Badge, Icon, NgClass, NgFor, NgIf, TabButton, TabHighlight],
template:
'<div class="tabbar" role="tablist" #tabbar>' +
'<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" [class.tab-disabled]="!t.enabled" [class.tab-hidden]="!t.show" role="tab" href="#" (ionSelect)="select($event)">' +
'<ion-icon *ngIf="t.tabIcon" [name]="t.tabIcon" [isActive]="t.isSelected" class="tab-button-icon"></ion-icon>' +
'<span *ngIf="t.tabTitle" class="tab-button-text">{{t.tabTitle}}</span>' +
'<ion-badge *ngIf="t.tabBadge" class="tab-badge" [ngClass]="\'badge-\' + t.tabBadgeStyle">{{t.tabBadge}}</ion-badge>' +
'<div class="button-effect"></div>' +
'</a>' +
'<div class="tab-highlight"></div>' +
'</div>' +
'<ng-content></ng-content>' +
'<div #portal tab-portal></div>',
encapsulation: ViewEncapsulation.None,
})
export class Tabs extends Ion {
private _ids: number = -1;
private _tabs: Tab[] = [];
private _onReady: any = null;
private _sbPadding: boolean;
private _top: number;
private _bottom: number;
/**
* @private
*/
id: string;
/**
* @private
*/
selectHistory: string[] = [];
/**
* @private
*/
subPages: boolean;
export class Tabs extends Ion implements AfterViewInit {
/** @internal */
_color: string;
_ids: number = -1;
/** @internal */
_tabs: Tab[] = [];
/** @internal */
_sbPadding: boolean;
/** @internal */
_top: number;
/** @internal */
_bottom: number;
/** @internal */
id: string;
/** @internal */
_selectHistory: string[] = [];
/** @internal */
_subPages: boolean;
/**
* @input {string} The predefined color to use. For example: `"primary"`, `"secondary"`, `"danger"`.
*/
@Input()
get color(): string {
return this._color;
set color(value: string) {
this._setColor('tabs', value);
}
set color(value: string) {
this._updateColor(value);
/**
* @input {string} The mode to apply to this component.
*/
@Input()
set mode(val: string) {
this._setMode('tabs', val);
}
/**
* @input {number} The default selected tab index when first loaded. If a selected index isn't provided then it will use `0`, the first tab.
*/
@Input() selectedIndex: any;
@Input() selectedIndex: number;
/**
* @input {boolean} Set whether to preload all the tabs: `true`, `false`.
*/
@Input() preloadTabs: any;
/**
* @private DEPRECATED. Please use `tabsLayout` instead.
* @internal DEPRECATED. Please use `tabsLayout` instead.
*/
@Input() private tabbarLayout: string;
@@ -218,7 +204,7 @@ export class Tabs extends Ion {
@Input() tabsLayout: string;
/**
* @private DEPRECATED. Please use `tabsPlacement` instead.
* @internal DEPRECATED. Please use `tabsPlacement` instead.
*/
@Input() private tabbarPlacement: string;
@@ -238,17 +224,17 @@ export class Tabs extends Ion {
@Output() ionChange: EventEmitter<Tab> = new EventEmitter<Tab>();
/**
* @private
* @internal
*/
@ViewChild(TabHighlight) private _highlight: TabHighlight;
@ViewChild(TabHighlight) _highlight: TabHighlight;
/**
* @private
* @internal
*/
@ViewChild('tabbar') private _tabbar: ElementRef;
@ViewChild('tabbar') _tabbar: ElementRef;
/**
* @private
* @internal
*/
@ViewChild('portal', {read: ViewContainerRef}) portal: ViewContainerRef;
@@ -261,29 +247,31 @@ export class Tabs extends Ion {
@Optional() parent: NavController,
@Optional() public viewCtrl: ViewController,
private _app: App,
private _config: Config,
private _elementRef: ElementRef,
config: Config,
elementRef: ElementRef,
private _platform: Platform,
private _renderer: Renderer
renderer: Renderer,
private _linker: DeepLinker
) {
super(_elementRef);
super(config, elementRef, renderer);
this.mode = config.get('mode');
this.parent = <NavControllerBase>parent;
this.id = 't' + (++tabIds);
this._sbPadding = _config.getBoolean('statusbarPadding');
this.subPages = _config.getBoolean('tabsHideOnSubPages');
this.tabsHighlight = _config.getBoolean('tabsHighlight');
this._sbPadding = config.getBoolean('statusbarPadding');
this._subPages = config.getBoolean('tabsHideOnSubPages');
this.tabsHighlight = config.getBoolean('tabsHighlight');
// TODO deprecated 07-07-2016 beta.11
if (_config.get('tabSubPages') !== null) {
if (config.get('tabSubPages') !== null) {
console.warn('Config option "tabSubPages" has been deprecated. Please use "tabsHideOnSubPages" instead.');
this.subPages = _config.getBoolean('tabSubPages');
this._subPages = config.getBoolean('tabSubPages');
}
// TODO deprecated 07-07-2016 beta.11
if (_config.get('tabbarHighlight') !== null) {
if (config.get('tabbarHighlight') !== null) {
console.warn('Config option "tabbarHighlight" has been deprecated. Please use "tabsHighlight" instead.');
this.tabsHighlight = _config.getBoolean('tabbarHighlight');
this.tabsHighlight = config.getBoolean('tabbarHighlight');
}
if (this.parent) {
@@ -297,24 +285,20 @@ export class Tabs extends Ion {
} else if (this._app) {
// this is the root navcontroller for the entire app
this._app.setRootNav(this);
this._app._setRootNav(this);
}
// Tabs may also be an actual ViewController which was navigated to
// if Tabs is static and not navigated to within a NavController
// then skip this and don't treat it as it's own ViewController
if (viewCtrl) {
viewCtrl.setContent(this);
viewCtrl.setContentRef(_elementRef);
viewCtrl.loaded = (done) => {
this._onReady = done;
};
viewCtrl._setContent(this);
viewCtrl._setContentRef(elementRef);
}
}
/**
* @private
* @internal
*/
ngAfterViewInit() {
this._setConfig('tabsPlacement', 'bottom');
@@ -328,27 +312,27 @@ export class Tabs extends Ion {
// TODO deprecated 07-07-2016 beta.11
if (this.tabbarPlacement !== undefined) {
console.warn('Input "tabbarPlacement" has been deprecated. Please use "tabsPlacement" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsPlacement', this.tabbarPlacement);
this.setElementAttribute('tabsPlacement', this.tabbarPlacement);
this.tabsPlacement = this.tabbarPlacement;
}
// TODO deprecated 07-07-2016 beta.11
if (this._config.get('tabbarPlacement') !== null) {
console.warn('Config option "tabbarPlacement" has been deprecated. Please use "tabsPlacement" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsPlacement', this._config.get('tabbarPlacement'));
this.setElementAttribute('tabsPlacement', this._config.get('tabbarPlacement'));
}
// TODO deprecated 07-07-2016 beta.11
if (this.tabbarLayout !== undefined) {
console.warn('Input "tabbarLayout" has been deprecated. Please use "tabsLayout" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsLayout', this.tabbarLayout);
this.setElementAttribute('tabsLayout', this.tabbarLayout);
this.tabsLayout = this.tabbarLayout;
}
// TODO deprecated 07-07-2016 beta.11
if (this._config.get('tabbarLayout') !== null) {
console.warn('Config option "tabbarLayout" has been deprecated. Please use "tabsLayout" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsLayout', this._config.get('tabsLayout'));
this.setElementAttribute('tabsLayout', this._config.get('tabsLayout'));
}
if (this.tabsHighlight) {
@@ -361,12 +345,19 @@ export class Tabs extends Ion {
}
/**
* @private
* @internal
*/
initTabs() {
// get the selected index from the input
// otherwise default it to use the first index
let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(this.selectedIndex, 10));
let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(<any>this.selectedIndex, 10));
// now see if the deep linker can find a tab index
const tabsSegment = this._linker.initNav(this);
if (tabsSegment && isBlank(tabsSegment.component)) {
// we found a segment which probably represents which tab to select
selectedIndex = this._linker.getSelectedTabIndex(this, tabsSegment.name, selectedIndex);
}
// get the selectedIndex and ensure it isn't hidden or disabled
let selectedTab = this._tabs.find((t, i) => i === selectedIndex && t.enabled && t.show);
@@ -378,91 +369,73 @@ export class Tabs extends Ion {
if (selectedTab) {
// we found a tab to select
this.select(selectedTab);
}
// check if preloadTab is set as an input @Input
// otherwise check the preloadTabs config
let shouldPreloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs));
if (shouldPreloadTabs) {
// preload all the tabs which isn't the selected tab
this._tabs.filter((t) => t !== selectedTab).forEach((tab, index) => {
tab.preload(this._config.getNumber('tabsPreloadDelay', 1000) * index);
// get the segment the deep linker says this tab should load with
let pageId: string = null;
if (tabsSegment) {
let selectedTabSegment = this._linker.initNav(selectedTab);
if (selectedTabSegment && selectedTabSegment.component) {
selectedTab.root = selectedTabSegment.component;
selectedTab.rootParams = selectedTabSegment.data;
pageId = selectedTabSegment.id;
}
}
this.select(selectedTab, {
id: pageId
});
}
// set the initial href attribute values for each tab
this._tabs.forEach(t => {
t.updateHref(t.root, t.rootParams);
});
}
/**
* @private
* @internal
*/
private _setConfig(attrKey: string, fallback: any) {
var val = (<any>this)[attrKey];
_setConfig(attrKey: string, fallback: any) {
let val = (<any>this)[attrKey];
if (isBlank(val)) {
val = this._config.get(attrKey, fallback);
}
this._renderer.setElementAttribute(this._elementRef.nativeElement, attrKey, val);
}
/**
* @internal
*/
_updateColor(newColor: string) {
this._setElementColor(this._color, false);
this._setElementColor(newColor, true);
this._color = newColor;
}
/**
* @internal
*/
_setElementColor(color: string, isAdd: boolean) {
if (color !== null && color !== '') {
this._renderer.setElementClass(this._elementRef.nativeElement, `tabs-${color}`, isAdd);
}
this.setElementAttribute(attrKey, val);
}
/**
* @private
*/
add(tab: Tab) {
tab.id = this.id + '-' + (++this._ids);
this._tabs.push(tab);
return this.id + '-' + (++this._ids);
}
/**
* @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
*/
select(tabOrIndex: number | Tab, opts: NavOptions = {}, done?: Function): Promise<any> {
let promise: Promise<any>;
if (!done) {
promise = new Promise(res => { done = res; });
}
let selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
select(tabOrIndex: number | Tab, opts: NavOptions = {}) {
const selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
if (isBlank(selectedTab)) {
return Promise.resolve();
return;
}
let deselectedTab = this.getSelected();
const deselectedTab = this.getSelected();
if (selectedTab === deselectedTab) {
// no change
this._touchActive(selectedTab);
return Promise.resolve();
return this._touchActive(selectedTab);
}
console.debug(`Tabs, select: ${selectedTab.id}`);
let deselectedPage: ViewController;
if (deselectedTab) {
deselectedPage = deselectedTab.getActive();
deselectedPage && deselectedPage.fireWillLeave();
deselectedPage && deselectedPage._willLeave();
}
opts.animate = false;
let selectedPage = selectedTab.getActive();
selectedPage && selectedPage.fireWillEnter();
const selectedPage = selectedTab.getActive();
selectedPage && selectedPage._willEnter();
selectedTab.load(opts, (initialLoad: boolean) => {
selectedTab.load(opts, (alreadyLoaded: boolean) => {
selectedTab.ionSelect.emit(selectedTab);
this.ionChange.emit(selectedTab);
@@ -478,27 +451,26 @@ export class Tabs extends Ion {
if (this.tabsHighlight) {
this._highlight.select(selectedTab);
}
if (opts.updateUrl !== false) {
this._linker.navChange(DIRECTION_SWITCH);
}
}
selectedPage && selectedPage.fireDidEnter();
deselectedPage && deselectedPage.fireDidLeave();
if (this._onReady) {
this._onReady();
this._onReady = null;
}
selectedPage && selectedPage._didEnter();
deselectedPage && deselectedPage._didLeave();
// track the order of which tabs have been selected, by their index
// do not track if the tab index is the same as the previous
if (this.selectHistory[this.selectHistory.length - 1] !== selectedTab.id) {
this.selectHistory.push(selectedTab.id);
if (this._selectHistory[this._selectHistory.length - 1] !== selectedTab.id) {
this._selectHistory.push(selectedTab.id);
}
// if this is not the Tab's initial load then we need
// to refresh the tabbar and content dimensions to be sure
// they're lined up correctly
if (!initialLoad && selectedPage) {
var content = <Content>selectedPage.getContent();
if (alreadyLoaded && selectedPage) {
let content = <Content>selectedPage.getContent();
if (content && content instanceof Content) {
nativeRaf(() => {
content.readDimensions();
@@ -506,11 +478,7 @@ export class Tabs extends Ion {
});
}
}
done();
});
return promise;
}
/**
@@ -521,12 +489,12 @@ export class Tabs extends Ion {
previousTab(trimHistory: boolean = true): Tab {
// walk backwards through the tab selection history
// and find the first previous tab that is enabled and shown
console.debug('run previousTab', this.selectHistory);
for (var i = this.selectHistory.length - 2; i >= 0; i--) {
var tab = this._tabs.find(t => t.id === this.selectHistory[i]);
console.debug('run previousTab', this._selectHistory);
for (var i = this._selectHistory.length - 2; i >= 0; i--) {
var tab = this._tabs.find(t => t.id === this._selectHistory[i]);
if (tab && tab.enabled && tab.show) {
if (trimHistory) {
this.selectHistory.splice(i + 1);
this._selectHistory.splice(i + 1);
}
return tab;
}
@@ -540,17 +508,14 @@ export class Tabs extends Ion {
* @returns {Tab} Returns the tab who's index matches the one passed
*/
getByIndex(index: number): Tab {
if (index < this._tabs.length && index > -1) {
return this._tabs[index];
}
return null;
return this._tabs[index];
}
/**
* @return {Tab} Returns the currently selected tab
*/
getSelected(): Tab {
for (let i = 0; i < this._tabs.length; i++) {
for (var i = 0; i < this._tabs.length; i++) {
if (this._tabs[i].isSelected) {
return this._tabs[i];
}
@@ -559,68 +524,57 @@ export class Tabs extends Ion {
}
/**
* @private
* @internal
*/
getActiveChildNav() {
return this.getSelected();
}
/**
* @private
* @internal
*/
getIndex(tab: Tab): number {
return this._tabs.indexOf(tab);
}
/**
* @private
* @internal
*/
length(): number {
return this._tabs.length;
}
/**
* @private
* "Touch" the active tab, going back to the root view of the tab
* or optionally letting the tab handle the event
*/
private _touchActive(tab: Tab) {
let active = tab.getActive();
const active = tab.getActive();
if (!active) {
return Promise.resolve();
if (active) {
if (active._cmp && active._cmp.instance.ionSelected) {
// if they have a custom tab selected handler, call it
active._cmp.instance.ionSelected();
} else if (tab.length() > 1) {
// if we're a few pages deep, pop to root
tab.popToRoot(null, null);
} else if (tab.root !== active.component) {
// Otherwise, if the page we're on is not our real root, reset it to our
// default root type
tab.setRoot(tab.root);
}
}
let instance = active.instance;
// If they have a custom tab selected handler, call it
if (instance.ionSelected) {
return instance.ionSelected();
}
// If we're a few pages deep, pop to root
if (tab.length() > 1) {
// Pop to the root view
return tab.popToRoot();
}
// Otherwise, if the page we're on is not our real root, reset it to our
// default root type
if (tab.root !== active.componentType) {
return tab.setRoot(tab.root);
}
// And failing all of that, we do something safe and secure
return Promise.resolve();
}
/**
* @private
* @internal
* DOM WRITE
*/
setTabbarPosition(top: number, bottom: number) {
if (this._top !== top || this._bottom !== bottom) {
let tabbarEle = <HTMLElement>this._tabbar.nativeElement;
const tabbarEle = <HTMLElement>this._tabbar.nativeElement;
tabbarEle.style.top = (top > -1 ? top + 'px' : '');
tabbarEle.style.bottom = (bottom > -1 ? bottom + 'px' : '');
tabbarEle.classList.add('show-tabbar');

View File

@@ -0,0 +1,959 @@
import { ComponentRef, ComponentFactoryResolver, ElementRef, EventEmitter, NgZone, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core';
import { AnimationOptions } from '../animations/animation';
import { App } from '../components/app/app';
import { Config } from '../config/config';
import { convertToView, convertToViews, NavOptions, DIRECTION_BACK, DIRECTION_FORWARD, INIT_ZINDEX,
TransitionResolveFn, TransitionRejectFn, TransitionInstruction, ViewState } from './nav-util';
import { setZIndex } from './nav-util';
import { DeepLinker } from './deep-linker';
import { GestureController } from '../gestures/gesture-controller';
import { isBlank, isNumber, isPresent, pascalCaseToDashCase } from '../util/util';
import { isViewController, ViewController } from './view-controller';
import { Ion } from '../components/ion';
import { Keyboard } from '../util/keyboard';
import { NavController } from './nav-controller';
import { NavParams } from './nav-params';
import { SwipeBackGesture } from './swipe-back';
import { Transition } from '../transitions/transition';
import { TransitionController } from '../transitions/transition-controller';
/**
* @private
* This class is for internal use only. It is not exported publicly.
*/
export class NavControllerBase extends Ion implements NavController {
_children: any[] = [];
_ids: number = -1;
_init = false;
_isPortal: boolean;
_queue: TransitionInstruction[] = [];
_sbEnabled: boolean;
_sbGesture: SwipeBackGesture;
_sbThreshold: number;
_sbTrns: Transition;
_trnsId: number = null;
_trnsTm: number = 0;
_viewport: ViewContainerRef;
_views: ViewController[] = [];
viewDidLoad: EventEmitter<any>;
viewWillEnter: EventEmitter<any>;
viewDidEnter: EventEmitter<any>;
viewWillLeave: EventEmitter<any>;
viewDidLeave: EventEmitter<any>;
viewWillUnload: EventEmitter<any>;
id: string;
parent: any;
config: Config;
constructor(
parent: any,
public _app: App,
config: Config,
public _keyboard: Keyboard,
elementRef: ElementRef,
public _zone: NgZone,
renderer: Renderer,
public _cfr: ComponentFactoryResolver,
public _gestureCtrl: GestureController,
public _trnsCtrl: TransitionController,
public _linker: DeepLinker
) {
super(config, elementRef, renderer);
this.parent = parent;
this.config = config;
this._sbEnabled = config.getBoolean('swipeBackEnabled');
this._sbThreshold = config.getNumber('swipeBackThreshold', 40);
this.id = 'n' + (++ctrlIds);
this.viewDidLoad = new EventEmitter();
this.viewWillEnter = new EventEmitter();
this.viewDidEnter = new EventEmitter();
this.viewWillLeave = new EventEmitter();
this.viewDidLeave = new EventEmitter();
this.viewWillUnload = new EventEmitter();
}
push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
insertStart: -1,
insertViews: [convertToView(this._linker, page, params)],
opts: opts,
}, done);
}
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
insertStart: insertIndex,
insertViews: [convertToView(this._linker, page, params)],
opts: opts,
}, done);
}
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
insertStart: insertIndex,
insertViews: convertToViews(this._linker, insertPages),
opts: opts,
}, done);
}
pop(opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
removeStart: -1,
removeCount: 1,
opts: opts,
}, done);
}
popTo(indexOrViewCtrl: any, opts?: NavOptions, done?: Function): Promise<any> {
const startIndex = isViewController(indexOrViewCtrl) ? this.indexOf(indexOrViewCtrl) : isNumber(indexOrViewCtrl) ? indexOrViewCtrl : -1;
return this._queueTrns({
removeStart: startIndex + 1,
removeCount: -1,
opts: opts,
}, done);
}
popToRoot(opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
removeStart: 1,
removeCount: -1,
opts: opts,
}, done);
}
popAll() {
let promises: any[] = [];
for (var i = this._views.length - 1; i >= 0; i--) {
promises.push(this.pop(null));
}
return Promise.all(promises);
}
remove(startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
removeStart: startIndex,
removeCount: removeCount,
opts: opts,
}, done);
}
setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
insertStart: 0,
insertViews: [convertToView(this._linker, pageOrViewCtrl, params)],
removeStart: 0,
removeCount: -1,
opts: opts
}, done);
}
setPages(pages: any[], opts?: NavOptions, done?: Function): Promise<any> {
return this._queueTrns({
insertStart: 0,
insertViews: convertToViews(this._linker, pages),
removeStart: 0,
removeCount: -1,
opts: opts
}, done);
}
_queueTrns(ti: TransitionInstruction, done: Function): Promise<any> {
let promise: Promise<any>;
let resolve: Function = done;
let reject: Function = done;
if (done === undefined) {
// only create a promise if a done callback wasn't provided
// done can be a null, which avoids any functions
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
}
ti.resolve = (hasCompleted: boolean, isAsync: boolean, enteringName: string, leavingName: string, direction: string) => {
// transition has successfully resolved
this._trnsId = null;
resolve && resolve(hasCompleted, isAsync, enteringName, leavingName, direction);
this._sbCheck();
// let's see if there's another to kick off
this.setTransitioning(false);
this._nextTrns();
};
ti.reject = (rejectReason: any, trns: Transition) => {
// rut row raggy, something rejected this transition
this._trnsId = null;
this._queue.length = 0;
while (trns) {
if (trns.enteringView && (trns.enteringView._state !== ViewState.LOADED)) {
// destroy the entering views and all of their hopes and dreams
trns.enteringView._destroy(this._renderer);
}
if (!trns.parent) break;
}
if (trns) {
this._trnsCtrl.destroy(trns.trnsId);
}
this._sbCheck();
reject && reject(false, false, rejectReason);
this.setTransitioning(false);
this._nextTrns();
};
if (ti.insertViews) {
// ensure we've got good views to insert
ti.insertViews = ti.insertViews.filter(v => v !== null);
if (!ti.insertViews.length) {
ti.reject('invalid views to insert');
return;
}
} else if (isPresent(ti.removeStart) && !this._views.length && !this._isPortal) {
ti.reject('no views in the stack to be removed');
return;
}
this._queue.push(ti);
// if there isn't a transitoin already happening
// then this will kick off this transition
this._nextTrns();
// promise is undefined if a done callbacks was provided
return promise;
}
_nextTrns(): boolean {
// this is the framework's bread 'n butta function
// only one transition is allowed at any given time
if (this.isTransitioning()) {
return false;
}
// there is no transition happening right now
// get the next instruction
const ti = this._queue.shift();
if (!ti) {
this.setTransitioning(false);
return false;
}
this.setTransitioning(true, ACTIVE_TRANSITION_MAX_TIME);
const viewsLength = this._views.length;
const activeView = this.getActive();
let enteringView: ViewController;
let leavingView: ViewController = activeView;
const destroyQueue: ViewController[] = [];
const opts = ti.opts || {};
const resolve = ti.resolve;
const reject = ti.reject;
let insertViews = ti.insertViews;
ti.resolve = ti.reject = ti.opts = ti.insertViews = null;
let enteringRequiresTransition = false;
let leavingRequiresTransition = false;
if (isPresent(ti.removeStart)) {
if (ti.removeStart < 0) {
ti.removeStart = (viewsLength - 1);
}
if (ti.removeCount < 0) {
ti.removeCount = (viewsLength - ti.removeStart);
}
leavingRequiresTransition = (ti.removeStart + ti.removeCount === viewsLength);
for (var i = ti.removeStart; i <= ti.removeCount; i++) {
destroyQueue.push(this._views[i]);
}
for (var i = viewsLength - 1; i >= 0; i--) {
var view = this._views[i];
if (destroyQueue.indexOf(view) < 0 && view !== leavingView) {
enteringView = view;
break;
}
}
// default the direction to "back"
opts.direction = opts.direction || DIRECTION_BACK;
}
if (insertViews) {
// allow -1 to be passed in to auto push it on the end
// and clean up the index if it's larger then the size of the stack
if (ti.insertStart < 0 || ti.insertStart > viewsLength) {
ti.insertStart = viewsLength;
}
// only requires a transition if it's going at the end
enteringRequiresTransition = (ti.insertStart === viewsLength);
// grab the very last view of the views to be inserted
// and initialize it as the new entering view
enteringView = insertViews[insertViews.length - 1];
// manually set the new view's id if an id was passed in the options
if (isPresent(opts.id)) {
enteringView.id = opts.id;
}
// add the views to the
for (var i = 0; i < insertViews.length; i++) {
var view = insertViews[i];
var existingIndex = this._views.indexOf(view);
if (existingIndex > -1) {
// this view is already in the stack!!
// move it to its new location
this._views.splice(ti.insertStart + i, 0, this._views.splice(existingIndex, 1)[0]);
} else {
// this is a new view to add to the stack
// create the new entering view
view._setNav(this);
// give this inserted view an ID
view.id = this.id + '-' + (++this._ids);
// insert the entering view into the correct index in the stack
this._views.splice(ti.insertStart + i, 0, view);
}
}
if (enteringRequiresTransition) {
// default to forward if not already set
opts.direction = opts.direction || DIRECTION_FORWARD;
}
}
// if the views to be removed are in the beginning or middle
// and there is not a view that needs to visually transition out
// then just destroy them and don't transition anything
for (var i = 0; i < destroyQueue.length; i++) {
// batch all of lifecycles together
var view = destroyQueue[i];
if (view && view !== enteringView && view !== leavingView) {
view._willLeave();
this.viewWillLeave.emit(view);
this._app.viewWillLeave.emit(view);
view._didLeave();
this.viewDidLeave.emit(view);
this._app.viewDidLeave.emit(view);
view._willUnload();
this.viewWillUnload.emit(view);
this._app.viewWillUnload.emit(view);
}
}
for (var i = 0; i < destroyQueue.length; i++) {
// batch all of the destroys together
var view = destroyQueue[i];
if (view && view !== enteringView && view !== leavingView) {
view._destroy(this._renderer);
}
}
destroyQueue.length = 0;
if (enteringRequiresTransition || leavingRequiresTransition && enteringView !== leavingView) {
// set which animation it should use if it wasn't set yet
if (!opts.animation) {
if (isPresent(ti.removeStart)) {
opts.animation = (leavingView || enteringView).getTransitionName(opts.direction);
} else {
opts.animation = (enteringView || leavingView).getTransitionName(opts.direction);
}
}
// huzzah! let us transition these views
this._transition(enteringView, leavingView, opts, resolve, reject);
} else {
// they're inserting/removing the views somewhere in the middle or
// beginning, so visually nothing needs to animate/transition
// resolve immediately because there's no animation that's happening
resolve(true, false);
}
return true;
}
_transition(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, resolve: TransitionResolveFn, reject: TransitionRejectFn): void {
// figure out if this transition is the root one or a
// child of a parent nav that has the root transition
this._trnsId = this._trnsCtrl.getRootTrnsId(this);
if (this._trnsId === null) {
// this is the root transition, meaning all child navs and their views
// should be added as a child transition to this one
this._trnsId = this._trnsCtrl.nextId();
}
// create the transition options
const animationOpts: AnimationOptions = {
animation: opts.animation,
direction: opts.direction,
duration: (opts.animate === false ? 0 : opts.duration),
easing: opts.easing,
isRTL: this.config.platform.isRTL(),
ev: opts.ev,
};
// create the transition animation from the TransitionController
// this will either create the root transition, or add it as a child transition
const trns = this._trnsCtrl.get(this._trnsId, enteringView, leavingView, animationOpts);
// ensure any swipeback transitions are cleared out
this._sbTrns && this._sbTrns.destroy();
if (trns.parent) {
// this is important for later to know if there
// are any more child tests to check for
trns.parent.hasChildTrns = true;
} else {
// this is the root transition
if (opts.progressAnimation) {
this._sbTrns = trns;
}
}
trns.registerStart(() => {
this._trnsStart(trns, enteringView, leavingView, opts, resolve);
if (trns.parent) {
trns.parent.start();
}
});
if (enteringView && isBlank(enteringView._state)) {
// render the entering view, and all child navs and views
// ******** DOM WRITE ****************
this._viewInit(trns, enteringView, opts);
}
// views have been initialized, now let's test
// to see if the transition is even allowed or not
const shouldContinue = this._viewTest(trns, enteringView, leavingView, opts, resolve, reject);
if (shouldContinue) {
// synchronous and all tests passed! let's continue
this._postViewInit(trns, enteringView, leavingView, opts, resolve);
}
}
/**
* DOM WRITE
*/
_viewInit(trns: Transition, enteringView: ViewController, opts: NavOptions) {
// entering view has not been initialized yet
const componentProviders = ReflectiveInjector.resolve([
{ provide: NavController, useValue: this },
{ provide: ViewController, useValue: enteringView },
{ provide: NavParams, useValue: enteringView.getNavParams() }
]);
const componentFactory = this._cfr.resolveComponentFactory(enteringView.component);
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector);
// create ComponentRef and set it to the entering view
enteringView.init(componentFactory.create(childInjector, []));
enteringView._state = ViewState.INITIALIZED;
}
_viewTest(trns: Transition, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, resolve: TransitionResolveFn, reject: TransitionRejectFn): boolean {
const promises: Promise<any>[] = [];
if (leavingView) {
const leavingTestResult = leavingView._lifecycleTest('Leave');
if (isPresent(leavingTestResult) && leavingTestResult !== true) {
if (leavingTestResult instanceof Promise) {
// async promise
promises.push(leavingTestResult);
} else {
// synchronous reject
reject((leavingTestResult !== false ? leavingTestResult : `ionViewCanLeave rejected`), trns);
return false;
}
}
}
if (enteringView) {
const enteringTestResult = enteringView._lifecycleTest('Enter');
if (isPresent(enteringTestResult) && enteringTestResult !== true) {
if (enteringTestResult instanceof Promise) {
// async promise
promises.push(enteringTestResult);
} else {
// synchronous reject
reject((enteringTestResult !== false ? enteringTestResult : `ionViewCanEnter rejected`), trns);
return false;
}
}
}
if (promises.length) {
// darn, async promises, gotta wait for them to resolve
Promise.all(promises).then(() => {
// all promises resolved! let's continue
this._postViewInit(trns, enteringView, leavingView, opts, resolve);
}, (rejectReason: any) => {
// darn, one of the promises was rejected!!
reject(rejectReason, trns);
}).catch((rejectReason) => {
// idk, who knows
reject(rejectReason, trns);
});
return false;
}
// synchronous and all tests passed! let's move on already
return true;
}
_postViewInit(trns: Transition, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, resolve: TransitionResolveFn): void {
// passed both the enter and leave tests
if (enteringView && enteringView._state === ViewState.INITIALIZED) {
// render the entering component in the DOM
// this would also render new child navs/views
// which may have their very own async canEnter/Leave tests
// ******** DOM WRITE ****************
this._viewInsert(enteringView, enteringView._cmp, this._viewport);
}
if (!trns.hasChildTrns) {
// lowest level transition, so kick it off and let it bubble up to start all of them
trns.start();
}
}
_viewInsert(view: ViewController, componentRef: ComponentRef<any>, viewport: ViewContainerRef) {
// successfully finished loading the entering view
// fire off the "loaded" lifecycle events
view._didLoad();
this.viewDidLoad.emit(view);
this._app.viewDidLoad.emit(view);
// render the component ref instance to the DOM
// ******** DOM WRITE ****************
viewport.insert(componentRef.hostView, viewport.length);
view._state = ViewState.PRE_RENDERED;
// the ElementRef of the actual ion-page created
const pageElement = componentRef.location.nativeElement;
if (view._cssClass) {
// ******** DOM WRITE ****************
this._renderer.setElementClass(pageElement, view._cssClass, true);
}
// auto-add page css className created from component JS class name
// ******** DOM WRITE ****************
const cssClassName = pascalCaseToDashCase(view.component.name);
this._renderer.setElementClass(pageElement, cssClassName, true);
componentRef.changeDetectorRef.detectChanges();
}
_trnsStart(trns: Transition, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, resolve: TransitionResolveFn) {
this._trnsId = null;
// set the correct zIndex for the entering and leaving views
// ******** DOM WRITE ****************
setZIndex(this, enteringView, leavingView, opts.direction, this._renderer);
// always ensure the entering view is viewable
// ******** DOM WRITE ****************
enteringView && enteringView._domShow(true, this._renderer);
if (leavingView) {
// always ensure the leaving view is viewable
// ******** DOM WRITE ****************
leavingView._domShow(true, this._renderer);
}
// initialize the transition
trns.init();
if ((!this._init && this._views.length === 1 && !this._isPortal) || this.config.get('animate') === false) {
// the initial load shouldn't animate, unless it's a portal
opts.animate = false;
}
if (opts.animate === false) {
// if it was somehow set to not animation, then make the duration zero
trns.duration(0);
}
// create a callback that needs to run within zone
// that will fire off the willEnter/Leave lifecycle events at the right time
trns.beforeAddRead(() => {
this._zone.run(this._viewsWillLifecycles.bind(this, enteringView, leavingView));
});
// create a callback for when the animation is done
trns.onFinish(() => {
// transition animation has ended
this._zone.run(this._trnsFinish.bind(this, trns, opts, resolve));
});
// get the set duration of this transition
const duration = trns.getDuration();
// set that this nav is actively transitioning
this.setTransitioning(true, duration);
if (!trns.parent) {
// this is the top most, or only active transition, so disable the app
// add XXms to the duration the app is disabled when the keyboard is open
if (duration > DISABLE_APP_MINIMUM_DURATION) {
// if this transition has a duration and this is the root transition
// then set that the app is actively disabled
this._app.setEnabled(false, duration);
}
// cool, let's do this, start the transition
if (opts.progressAnimation) {
// this is a swipe to go back, just get the transition progress ready
// kick off the swipe animation start
trns.progressStart();
} else {
// only the top level transition should actually start "play"
// kick it off and let it play through
// ******** DOM WRITE ****************
trns.play();
}
}
}
_viewsWillLifecycles(enteringView: ViewController, leavingView: ViewController) {
// call each view's lifecycle events
if (enteringView) {
enteringView._willEnter();
this.viewWillEnter.emit(enteringView);
this._app.viewWillEnter.emit(enteringView);
}
if (leavingView) {
leavingView._willLeave();
this.viewWillLeave.emit(leavingView);
this._app.viewWillLeave.emit(leavingView);
}
}
_trnsFinish(trns: Transition, opts: NavOptions, resolve: TransitionResolveFn) {
const hasCompleted = trns.hasCompleted;
// mainly for testing
let enteringName: string;
let leavingName: string;
if (hasCompleted) {
// transition has completed (went from 0 to 1)
if (trns.enteringView) {
enteringName = trns.enteringView.name;
trns.enteringView._didEnter();
this.viewDidEnter.emit(trns.enteringView);
this._app.viewDidEnter.emit(trns.enteringView);
}
if (trns.leavingView) {
leavingName = trns.leavingView.name;
trns.leavingView._didLeave();
this.viewDidLeave.emit(trns.leavingView);
this._app.viewDidLeave.emit(trns.leavingView);
}
this._cleanup(trns.enteringView);
}
if (!trns.parent) {
// this is the root transition
// it's save to destroy this transition
this._trnsCtrl.destroy(trns.trnsId);
// it's safe to enable the app again
this._app.setEnabled(true);
if (opts.updateUrl !== false) {
// notify deep linker of the nav change
// if a direction was provided and should update url
this._linker.navChange(opts.direction);
}
if (opts.keyboardClose !== false && this._keyboard.isOpen()) {
// the keyboard is still open!
// no problem, let's just close for them
this._keyboard.close();
}
}
// congrats, we did it!
resolve(hasCompleted, true, enteringName, leavingName, opts.direction);
}
/**
* DOM WRITE
*/
_cleanup(activeView: ViewController) {
// ok, cleanup time!! Destroy all of the views that are
// INACTIVE and come after the active view
const activeViewIndex = this.indexOf(activeView);
let reorderZIndexes = false;
for (var i = this._views.length - 1; i >= 0; i--) {
var view = this._views[i];
if (i > activeViewIndex) {
// this view comes after the active view
// let's unload it
view._willUnload();
this.viewWillUnload.emit(view);
this._app.viewWillUnload.emit(view);
view._destroy(this._renderer);
} else if (i < activeViewIndex && !this._isPortal) {
// this view comes before the active view
// and it is not a portal then ensure it is hidden
view._domShow(false, this._renderer);
}
if (view._zIndex <= 0) {
reorderZIndexes = true;
}
}
if (!this._isPortal) {
if (reorderZIndexes) {
this._views.forEach(view => {
// ******** DOM WRITE ****************
view._setZIndex(view._zIndex + INIT_ZINDEX + 1, this._renderer);
});
}
}
}
getActiveChildNav(): any {
return this._children[this._children.length - 1];
}
registerChildNav(nav: any) {
this._children.push(nav);
}
unregisterChildNav(nav: any) {
const index = this._children.indexOf(nav);
if (index > -1) {
this._children.splice(index, 1);
}
}
destroy() {
for (var i = this._views.length - 1; i >= 0; i--) {
this._views[i]._willUnload();
this._views[i]._destroy(this._renderer);
}
this._views.length = 0;
this._sbGesture && this._sbGesture.destroy();
this._sbTrns && this._sbTrns.destroy();
this._sbGesture = this._sbTrns = null;
if (this.parent && this.parent.unregisterChildNav) {
this.parent.unregisterChildNav(this);
}
}
swipeBackStart() {
if (this.isTransitioning() || this._queue.length > 0) return;
// default the direction to "back";
const opts: NavOptions = {
direction: DIRECTION_BACK,
progressAnimation: true
};
this._queueTrns({
removeStart: -1,
removeCount: 1,
opts: opts,
}, null);
}
swipeBackProgress(stepValue: number) {
if (this._sbTrns && this._sbGesture) {
// continue to disable the app while actively dragging
this._app.setEnabled(false, ACTIVE_TRANSITION_MAX_TIME);
this.setTransitioning(true, ACTIVE_TRANSITION_MAX_TIME);
// set the transition animation's progress
this._sbTrns.progressStep(stepValue);
}
}
swipeBackEnd(shouldComplete: boolean, currentStepValue: number) {
if (this._sbTrns && this._sbGesture) {
// the swipe back gesture has ended
this._sbTrns.progressEnd(shouldComplete, currentStepValue);
}
}
_sbCheck() {
if (this._sbEnabled && !this._isPortal) {
// this nav controller can have swipe to go back
if (!this._sbGesture) {
// create the swipe back gesture if we haven't already
const opts = {
edge: 'left',
threshold: this._sbThreshold
};
this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._gestureCtrl);
}
if (this.canSwipeBack()) {
// it is be possible to swipe back
if (!this._sbGesture.isListening) {
this._zone.runOutsideAngular(() => {
// start listening if it's not already
console.debug('swipeBack gesture, listen');
this._sbGesture.listen();
});
}
} else if (this._sbGesture.isListening) {
// it should not be possible to swipe back
// but the gesture is still listening
console.debug('swipeBack gesture, unlisten');
this._sbGesture.unlisten();
}
}
}
canSwipeBack(): boolean {
return (this._sbEnabled &&
!this._children.length &&
!this.isTransitioning() &&
this._app.isEnabled() &&
this.canGoBack());
}
canGoBack(): boolean {
const activeView = this.getActive();
return !!(activeView && activeView.enableBack()) || false;
}
isTransitioning(): boolean {
// using a timestamp instead of boolean incase something goes wrong
return (this._trnsTm > Date.now());
}
setTransitioning(isTransitioning: boolean, durationPadding: number = 2000) {
this._trnsTm = (isTransitioning ? Date.now() + durationPadding : 0);
}
getActive() {
return this._views[this._views.length - 1];
}
isActive(view: ViewController) {
return (view === this.getActive());
}
getByIndex(index: number): ViewController {
return this._views[index];
}
getPrevious(view?: ViewController): ViewController {
// returns the view controller which is before the given view controller.
if (!view) {
view = this.getActive();
}
return this._views[this.indexOf(view) - 1];
}
first(): ViewController {
// returns the first view controller in this nav controller's stack.
return this._views[0];
}
last(): ViewController {
// returns the last page in this nav controller's stack.
return this._views[this._views.length - 1];
}
indexOf(view: ViewController): number {
// returns the index number of the given view controller.
return this._views.indexOf(view);
}
length(): number {
return this._views.length;
}
isSwipeBackEnabled(): boolean {
return this._sbEnabled;
}
dismissPageChangeViews() {
for (var i = 0; i < this._views.length; i++) {
var view = this._views[i];
if (view.data && view.data.dismissOnPageChange) {
view.dismiss();
}
}
}
setViewport(val: ViewContainerRef) {
this._viewport = val;
}
/**
* @private
* DEPRECATED: Please use app.getRootNav() instead
*/
get rootNav(): NavController {
// deprecated 07-14-2016 beta.11
console.warn('nav.rootNav() has been deprecated, please use app.getRootNav() instead');
return this._app.getRootNav();
}
/**
* @private
* DEPRECATED: Please use inject the overlays controller and use the present method on the instance instead.
*/
present(): Promise<any> {
// deprecated warning: added beta.11 2016-06-27
console.warn('nav.present() has been deprecated.\n' +
'Please inject the overlay\'s controller and use the present method on the instance instead.');
return Promise.resolve();
}
}
let ctrlIds = -1;
const DISABLE_APP_MINIMUM_DURATION = 64;
const ACTIVE_TRANSITION_MAX_TIME = 5000;

View File

@@ -1,11 +1,7 @@
import { EventEmitter } from '@angular/core';
import { Config } from '../../config/config';
import { GestureController } from '../../gestures/gesture-controller';
import { Ion } from '../ion';
import { isBlank, pascalCaseToDashCase } from '../../util/util';
import { Keyboard } from '../../util/keyboard';
import { NavOptions } from './nav-interfaces';
import { Config } from '../config/config';
import { NavOptions } from './nav-util';
import { ViewController } from './view-controller';
@@ -96,7 +92,7 @@ import { ViewController } from './view-controller';
* template: '<ion-nav #myNav [root]="rootPage"></ion-nav>'
* })
* export class MyApp {
* @ViewChild('myNav') nav : NavController
* @ViewChild('myNav') nav: NavController
* private rootPage = TabsPage;
*
* // Wait for the components in MyApp's template to be initialized
@@ -199,11 +195,11 @@ import { ViewController } from './view-controller';
* <ion-content>I'm the other page!</ion-content>`
* })
* class OtherPage {
* constructor(private navController: NavController ){
* constructor(private navCtrl: NavController ){
* }
*
* popView(){
* this.navController.pop();
* this.navCtrl.pop();
* }
* }
* ```
@@ -219,7 +215,7 @@ import { ViewController } from './view-controller';
* template: 'Hello World'
* })
* class HelloWorld {
* ionViewLoaded() {
* ionViewDidLoad() {
* console.log("I'm alive!");
* }
* ionViewWillLeave() {
@@ -230,13 +226,12 @@ import { ViewController } from './view-controller';
*
* | Page Event | Description |
* |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
* | `ionViewLoaded` | Runs when the page has loaded. This event only happens once per page being created and added to the DOM. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The `ionViewLoaded` event is good place to put your setup code for the page. |
* | `ionViewDidLoad` | Runs when the page has loaded. This event only happens once per page being created. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The `ionViewDidLoad` event is good place to put your setup code for the page. |
* | `ionViewWillEnter` | Runs when the page is about to enter and become the active page. |
* | `ionViewDidEnter` | Runs when the page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. |
* | `ionViewWillLeave` | Runs when the page is about to leave and no longer be the active page. |
* | `ionViewDidLeave` | Runs when the page has finished leaving and is no longer the active page. |
* | `ionViewWillUnload` | Runs when the page is about to be destroyed and have its elements removed. |
* | `ionViewDidUnload` | Runs after the page has been destroyed and its elements have been removed.
* | `ionViewWillUnload` | Runs when the page is about to be destroyed and have its elements removed.
*
*
* ## Asynchronous Nav Transitions
@@ -254,14 +249,14 @@ import { ViewController } from './view-controller';
* operations in order. Navigation actions can be chained together very easily using promises.
*
* ```typescript
* let navTransitionPromise = this.navController.push(Page2);
* navTransitionPromise.then( () => {
* let navTransitionPromise = this.navCtrl.push(Page2);
* navTransitionPromise.then(() => {
* // the transition has completed, so I can push another page now
* return this.navController.push(Page3);
* }).then( () => {
* return this.navCtrl.push(Page3);
* }).then(() => {
* // the second transition has completed, so I can push yet another page
return this.navController.push(Page4);
* }).then( () => {
return this.navCtrl.push(Page4);
* }).then(() => {
* console.log('The transitions are complete!');
* })
* ```
@@ -322,12 +317,6 @@ export abstract class NavController {
*/
viewWillUnload: EventEmitter<any>;
/**
* Observable to be subscribed to when a component has fully been unloaded and destroyed.
* @returns {Observable} Returns an observable
*/
viewDidUnload: EventEmitter<any>;
/**
* @private
*/
@@ -345,27 +334,6 @@ export abstract class NavController {
*/
config: Config;
/**
* Set the root for the current navigation stack.
* @param {Page} page The name of the component you want to push on the navigation stack.
* @param {object} [params={}] Any nav-params you want to pass along to the next view.
* @param {object} [opts={}] Any options you want to use pass to transtion.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setRoot(page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Set the views of the current navigation stack and navigate to the
* last view. By default animations are disabled, but they can be enabled
* by passing options to the navigation controller.You can also pass any
* navigation params to the individual pages in the array.
*
* @param {array<Page>} pages An arry of page components and their params to load in the stack.
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Push a new component onto the current navication stack. Pass any aditional information
* along as an object. This additional information is acessible through NavParams
@@ -421,13 +389,20 @@ export abstract class NavController {
/**
* @private
* Pop to a specific view in the history stack.
* Pop to a specific view in the history stack. If an already created
* instance of the page is not found in the stack, then it'll `setRoot`
* to the nav stack by removing all current pages and pushing on a
* new instance of the given page. Note that any params passed to
* this method are not used when an existing page instance has already
* been found in the stack. Nav params are only used by this method
* when a new instance needs to be created.
*
* @param {ViewController} view to pop to
* @param {any} page A page can be a ViewController instance or string.
* @param {object} [params={}] Any nav-params to be used when a new view instance is created at the root.
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract popTo(view: ViewController, opts?: NavOptions, done?: Function): Promise<any>;
abstract popTo(page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Removes a page from the nav stack at the specified index.
@@ -439,6 +414,27 @@ export abstract class NavController {
*/
abstract remove(startIndex: number, removeCount?: number, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Set the root for the current navigation stack.
* @param {Page|ViewController} page The name of the component you want to push on the navigation stack.
* @param {object} [params={}] Any nav-params you want to pass along to the next view.
* @param {object} [opts={}] Any options you want to use pass to transtion.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Set the views of the current navigation stack and navigate to the
* last view. By default animations are disabled, but they can be enabled
* by passing options to the navigation controller.You can also pass any
* navigation params to the individual pages in the array.
*
* @param {array<Page>} pages An arry of page components and their params to load in the stack.
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setPages(pages: any[], opts?: NavOptions, done?: Function): Promise<any>;
/**
* @param {number} index The index of the page to get.
* @returns {ViewController} Returns the view controller that matches the given index.
@@ -448,7 +444,7 @@ export abstract class NavController {
/**
* @returns {ViewController} Returns the active page's view controller.
*/
abstract getActive(): ViewController;
abstract getActive(includeEntering?: boolean): ViewController;
/**
* Returns if the given view is the active view or not.
@@ -459,10 +455,11 @@ export abstract class NavController {
/**
* Returns the view controller which is before the given view controller.
* If no view controller is passed in, then it'll default to the active view.
* @param {ViewController} view
* @returns {viewController}
*/
abstract getPrevious(view: ViewController): ViewController;
abstract getPrevious(view?: ViewController): ViewController;
/**
* Returns the first view controller in this nav controller's stack.

176
src/navigation/nav-util.ts Normal file
View File

@@ -0,0 +1,176 @@
import { Renderer, TypeDecorator } from '@angular/core';
import { DeepLinker } from './deep-linker';
import { isPresent } from '../util/util';
import { isViewController, ViewController } from './view-controller';
import { NavControllerBase } from './nav-controller-base';
import { Transition } from '../transitions/transition';
export function convertToView(linker: DeepLinker, nameOrPageOrView: any, params: any): ViewController {
if (nameOrPageOrView) {
if (isViewController(nameOrPageOrView)) {
// is already a ViewController
return nameOrPageOrView;
}
if (typeof nameOrPageOrView === 'function') {
// is a page component, now turn it into a ViewController
return new ViewController(nameOrPageOrView, params);
}
if (typeof nameOrPageOrView === 'string') {
// is a string, see if it matches a
const component = linker.getComponentFromName(nameOrPageOrView);
if (component) {
// found a page component in the link config by name
return new ViewController(component, params);
}
}
}
console.error(`invalid page component: ${nameOrPageOrView}`);
return null;
}
export function convertToViews(linker: DeepLinker, pages: any[]): ViewController[] {
const views: ViewController[] = [];
if (pages) {
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
if (page) {
if (isViewController(page)) {
views.push(page);
} else {
views.push(convertToView(linker, page.page, page.params));
}
}
}
}
return views;
}
export function setZIndex(nav: NavControllerBase, enteringView: ViewController, leavingView: ViewController, direction: string, renderer: Renderer) {
if (enteringView) {
leavingView = leavingView || nav.getPrevious(enteringView);
if (leavingView && isPresent(leavingView._zIndex)) {
if (direction === DIRECTION_BACK) {
enteringView._setZIndex(leavingView._zIndex - 1, renderer);
} else {
enteringView._setZIndex(leavingView._zIndex + 1, renderer);
}
} else {
enteringView._setZIndex(nav._isPortal ? PORTAL_ZINDEX : INIT_ZINDEX, renderer);
}
}
}
export function isTabs(nav: any) {
// Tabs (ion-tabs)
return !!nav && !!nav.getSelected;
}
export function isTab(nav: any) {
// Tab (ion-tab)
return !!nav && isPresent(nav._tabId);
}
export function isNav(nav: any) {
// Nav (ion-nav), Tab (ion-tab), Portal (ion-portal)
return !!nav && !!nav.push;
}
// public link interface
export interface DeepLinkMetadataType {
name: string;
segment?: string;
defaultHistory?: any[];
}
export class DeepLinkMetadata implements DeepLinkMetadataType {
component: any;
name: string;
segment?: string;
defaultHistory?: any[];
}
export interface DeepLinkDecorator extends TypeDecorator {}
export interface DeepLinkMetadataFactory {
(obj: DeepLinkMetadataType): DeepLinkDecorator;
new (obj: DeepLinkMetadataType): DeepLinkMetadata;
}
export var DeepLink: DeepLinkMetadataFactory;
export interface DeepLinkConfig {
links: DeepLinkMetadata[];
}
// internal link interface, not exposed publicly
export interface NavLink {
component: any;
name?: string;
segment?: string;
parts?: string[];
partsLen?: number;
staticLen?: number;
dataLen?: number;
defaultHistory?: any[];
}
export interface NavSegment {
id: string;
name: string;
component: any;
data: any;
navId?: string;
defaultHistory?: NavSegment[];
}
export interface NavOptions {
animate?: boolean;
animation?: string;
direction?: string;
duration?: number;
easing?: string;
id?: string;
keyboardClose?: boolean;
progressAnimation?: boolean;
ev?: any;
updateUrl?: boolean;
isNavRoot?: boolean;
}
export interface TransitionResolveFn {
(hasCompleted: boolean, requiresTransition: boolean, enteringName?: string, leavingName?: string, direction?: string): void;
}
export interface TransitionRejectFn {
(rejectReason: any, transition?: Transition): void;
}
export interface TransitionInstruction {
opts: NavOptions;
insertStart?: number;
insertViews?: ViewController[];
removeStart?: number;
removeCount?: number;
resolve?: TransitionResolveFn;
reject?: TransitionRejectFn;
}
export enum ViewState {
INITIALIZED,
PRE_RENDERED,
LOADED,
}
export const INIT_ZINDEX = 100;
export const PORTAL_ZINDEX = 9999;
export const DIRECTION_BACK = 'back';
export const DIRECTION_FORWARD = 'forward';
export const DIRECTION_SWITCH = 'switch';

View File

@@ -1,9 +1,8 @@
import { assign } from '../../util/util';
import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller';
import { MenuController } from '../menu/menu-controller';
import { assign } from '../util/util';
import { GestureController, GesturePriority } from '../gestures/gesture-controller';
import { NavControllerBase } from './nav-controller-base';
import { SlideData } from '../../gestures/slide-gesture';
import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture';
import { SlideData } from '../gestures/slide-gesture';
import { SlideEdgeGesture } from '../gestures/slide-edge-gesture';
export class SwipeBackGesture extends SlideEdgeGesture {

View File

@@ -0,0 +1,994 @@
import { mockNavController, mockView, mockViews,
MockView1, MockView2, MockView3, MockView4, MockView5 } from '../../util/mock-providers';
import { NavControllerBase } from '../nav-controller-base';
import { NavOptions, DIRECTION_FORWARD, DIRECTION_BACK } from '../nav-util';
import { ViewController } from '../view-controller';
describe('NavController', () => {
describe('push and pop', () => {
it('should push multiple times and pop multiple times', () => {
let push1Done = jasmine.createSpy('PushDone');
let push2Done = jasmine.createSpy('PushDone');
let push3Done = jasmine.createSpy('PushDone');
let push4Done = jasmine.createSpy('PushDone');
let pop1Done = jasmine.createSpy('PopDone');
let pop2Done = jasmine.createSpy('PopDone');
let pop3Done = jasmine.createSpy('PopDone');
// Push 1
nav.push(MockView1, null, { animate: false }, push1Done);
let hasCompleted = true;
let requiresTransition = true;
expect(push1Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', undefined, DIRECTION_FORWARD
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
// Push 2
nav.push(MockView2, null, { animate: false }, push2Done);
expect(push2Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView1', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
// Push 3
nav.push(MockView3, null, { animate: false }, push3Done);
expect(push3Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView3', 'MockView2', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(3);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
expect(nav.getByIndex(2).component).toEqual(MockView3);
// Push 4
nav.push(MockView4, null, { animate: false }, push4Done);
expect(push4Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView4', 'MockView3', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(4);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
expect(nav.getByIndex(2).component).toEqual(MockView3);
expect(nav.getByIndex(3).component).toEqual(MockView4);
// Pop 1
nav.pop({ animate: false }, pop1Done);
expect(pop1Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView3', 'MockView4', DIRECTION_BACK
);
expect(nav.length()).toEqual(3);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
expect(nav.getByIndex(2).component).toEqual(MockView3);
// Pop 2
nav.pop({ animate: false }, pop2Done);
expect(pop2Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView3', DIRECTION_BACK
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
// Pop 3
nav.pop({ animate: false }, pop3Done);
expect(pop3Done).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', 'MockView2', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
});
});
describe('push', () => {
it('should push a component as the first view', () => {
nav.push(MockView1, null, null, trnsDone);
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', undefined, DIRECTION_FORWARD
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.isTransitioning()).toEqual(false);
});
it('should push a component as the second view at the end', () => {
mockViews(nav, [mockView(MockView1)]);
nav.push(MockView2, null, null, trnsDone);
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView1', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
expect(nav.isTransitioning()).toEqual(false);
});
it('should push a ViewController as the second view and fire lifecycles', () => {
let view1 = mockView();
let view2 = mockView();
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
mockViews(nav, [view1]);
nav.push(view2, null, null, trnsDone);
expect(instance1.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance1.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance1.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance1.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance1.ionViewCanLeave).toHaveBeenCalled();
expect(instance1.ionViewWillLeave).toHaveBeenCalled();
expect(instance1.ionViewDidLeave).toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).toHaveBeenCalled();
expect(instance2.ionViewCanEnter).toHaveBeenCalled();
expect(instance2.ionViewWillEnter).toHaveBeenCalled();
expect(instance2.ionViewDidEnter).toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance2.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillUnload).not.toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView', 'MockView', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(2);
});
});
describe('insert', () => {
it('should insert at the begining with no async transition', () => {
let view4 = mockView(MockView4);
let instance4 = spyOnLifecycles(view4);
let opts: NavOptions = {};
mockViews(nav, [mockView(MockView1), mockView(MockView2), mockView(MockView3)]);
nav.insert(0, view4, null, opts, trnsDone);
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance4.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillUnload).not.toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = false;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, undefined, undefined, undefined
);
expect(nav.length()).toEqual(4);
expect(nav.first().component).toEqual(MockView4);
expect(nav.last().component).toEqual(MockView3);
});
it('should insert at the end when given -1', () => {
let opts: NavOptions = {};
mockViews(nav, [mockView(MockView1)]);
nav.insert(-1, MockView2, null, opts, trnsDone);
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView1', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(2);
expect(nav.last().component).toEqual(MockView2);
});
it('should insert at the end when given a number greater than actual length', () => {
mockViews(nav, [mockView(MockView1)]);
nav.insert(9999, MockView2, null, null, trnsDone);
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView1', DIRECTION_FORWARD
);
expect(nav.length()).toEqual(2);
expect(nav.last().component).toEqual(MockView2);
});
it('should not insert if null view', () => {
mockViews(nav, [mockView(MockView1)]);
nav.insert(-1, null, null, null, trnsDone);
let hasCompleted = false;
let requiresTransition = false;
let rejectReason = 'invalid views to insert';
expect(trnsDone).toHaveBeenCalledWith(hasCompleted, requiresTransition, rejectReason);
expect(nav.length()).toEqual(1);
expect(nav.last().component).toEqual(MockView1);
});
});
describe('insertPages', () => {
it('should insert all pages in the middle', () => {
let view4 = mockView(MockView4);
let instance4 = spyOnLifecycles(view4);
mockViews(nav, [mockView(MockView1), mockView(MockView2), mockView(MockView3)]);
nav.insertPages(1, [view4, mockView(MockView5)], null, trnsDone);
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance4.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillUnload).not.toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = false;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, undefined, undefined, undefined
);
expect(nav.length()).toEqual(5);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView4);
expect(nav.getByIndex(2).component).toEqual(MockView5);
expect(nav.getByIndex(3).component).toEqual(MockView2);
expect(nav.getByIndex(4).component).toEqual(MockView3);
expect(nav.getByIndex(1)._nav).toEqual(nav);
expect(nav.getByIndex(2)._nav).toEqual(nav);
});
});
describe('pop', () => {
it('should not pop when no views in the stack', () => {
nav.pop(null, trnsDone);
let hasCompleted = false;
let requiresTransition = false;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'no views in the stack to be removed'
);
expect(nav.length()).toEqual(0);
expect(nav.isTransitioning()).toEqual(false);
});
it('should remove the last view and fire lifecycles', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
mockViews(nav, [view1, view2]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
nav.pop(null, trnsDone);
expect(instance1.ionViewDidLoad).toHaveBeenCalled();
expect(instance1.ionViewCanEnter).toHaveBeenCalled();
expect(instance1.ionViewWillEnter).toHaveBeenCalled();
expect(instance1.ionViewDidEnter).toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance1.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', 'MockView2', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.isTransitioning()).toEqual(false);
});
});
describe('popTo', () => {
it('should pop to a view', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
mockViews(nav, [view1, view2, view3]);
nav.popTo(view2, null, trnsDone);
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView3', DIRECTION_BACK
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
});
it('should pop to using an index number', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
let view4 = mockView(MockView4);
mockViews(nav, [view1, view2, view3, view4]);
nav.popTo(1, null, trnsDone);
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView4', DIRECTION_BACK
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
});
it('should pop to first using an index number', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
let view4 = mockView(MockView4);
mockViews(nav, [view1, view2, view3, view4]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
let instance4 = spyOnLifecycles(view4);
nav.popTo(0, null, trnsDone);
expect(instance1.ionViewDidLoad).toHaveBeenCalled();
expect(instance1.ionViewCanEnter).toHaveBeenCalled();
expect(instance1.ionViewWillEnter).toHaveBeenCalled();
expect(instance1.ionViewDidEnter).toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance1.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).toHaveBeenCalled();
expect(instance4.ionViewWillLeave).toHaveBeenCalled();
expect(instance4.ionViewDidLeave).toHaveBeenCalled();
expect(instance4.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', 'MockView4', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
});
});
describe('popToRoot', () => {
it('should pop to the first view', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
let view4 = mockView(MockView4);
mockViews(nav, [view1, view2, view3, view4]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
let instance4 = spyOnLifecycles(view4);
nav.popToRoot(null, trnsDone);
expect(instance1.ionViewDidLoad).toHaveBeenCalled();
expect(instance1.ionViewCanEnter).toHaveBeenCalled();
expect(instance1.ionViewWillEnter).toHaveBeenCalled();
expect(instance1.ionViewDidEnter).toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance1.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).toHaveBeenCalled();
expect(instance4.ionViewWillLeave).toHaveBeenCalled();
expect(instance4.ionViewDidLeave).toHaveBeenCalled();
expect(instance4.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', 'MockView4', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
});
});
describe('remove', () => {
it('should remove the first three views in the beginning, no last view transition', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
let view4 = mockView(MockView4);
mockViews(nav, [view1, view2, view3, view4]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
let instance4 = spyOnLifecycles(view4);
nav.remove(0, 3, null, trnsDone);
expect(instance1.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance1.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance1.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance1.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).toHaveBeenCalled();
expect(instance1.ionViewDidLeave).toHaveBeenCalled();
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance4.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillUnload).not.toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = false;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, undefined, undefined, undefined
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView4);
});
it('should remove two views in the middle', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
let view4 = mockView(MockView4);
mockViews(nav, [view1, view2, view3, view4]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
let instance4 = spyOnLifecycles(view4);
nav.remove(1, 2, null, trnsDone);
expect(instance1.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance1.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance1.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance1.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance1.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance4.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance4.ionViewWillUnload).not.toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = false;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, undefined, undefined, undefined
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView4);
});
it('should remove the last two views at the end', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
let view4 = mockView(MockView4);
mockViews(nav, [view1, view2, view3, view4]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
let instance4 = spyOnLifecycles(view4);
nav.remove(2, 2, null, trnsDone);
expect(instance1.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance1.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance1.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance1.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance1.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).toHaveBeenCalled();
expect(instance2.ionViewCanEnter).toHaveBeenCalled();
expect(instance2.ionViewWillEnter).toHaveBeenCalled();
expect(instance2.ionViewDidEnter).toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance2.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
expect(instance4.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance4.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance4.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance4.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance4.ionViewCanLeave).toHaveBeenCalled();
expect(instance4.ionViewWillLeave).toHaveBeenCalled();
expect(instance4.ionViewDidLeave).toHaveBeenCalled();
expect(instance4.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView4', DIRECTION_BACK
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2);
});
});
describe('setRoot', () => {
it('should set a ViewController as the root when its the last view, no transition', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
mockViews(nav, [view1, view2, view3]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
nav.setRoot(view3, null, null, trnsDone);
expect(instance1.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance1.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance1.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance1.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).toHaveBeenCalled();
expect(instance1.ionViewDidLeave).toHaveBeenCalled();
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance3.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance3.ionViewWillUnload).not.toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = false;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, undefined, undefined, undefined
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView3);
});
it('should set a ViewController as the root when its the middle view, with transition', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
mockViews(nav, [view1, view2, view3]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
nav.setRoot(view2, null, null, trnsDone);
expect(instance1.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance1.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance1.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance1.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).toHaveBeenCalled();
expect(instance1.ionViewDidLeave).toHaveBeenCalled();
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
expect(instance2.ionViewDidLoad).toHaveBeenCalled();
expect(instance2.ionViewCanEnter).toHaveBeenCalled();
expect(instance2.ionViewWillEnter).toHaveBeenCalled();
expect(instance2.ionViewDidEnter).toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance2.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView2', 'MockView3', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView2);
});
it('should set a ViewController as the root when its the first view, with transition', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
mockViews(nav, [view1, view2, view3]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
nav.setRoot(view1, null, null, trnsDone);
expect(instance1.ionViewDidLoad).toHaveBeenCalled();
expect(instance1.ionViewCanEnter).toHaveBeenCalled();
expect(instance1.ionViewWillEnter).toHaveBeenCalled();
expect(instance1.ionViewDidEnter).toHaveBeenCalled();
expect(instance1.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillLeave).not.toHaveBeenCalled();
expect(instance1.ionViewDidLeave).not.toHaveBeenCalled();
expect(instance1.ionViewWillUnload).not.toHaveBeenCalled();
expect(instance2.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance2.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance2.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance2.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance2.ionViewCanLeave).not.toHaveBeenCalled();
expect(instance2.ionViewWillLeave).toHaveBeenCalled();
expect(instance2.ionViewDidLeave).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewDidLoad).not.toHaveBeenCalled();
expect(instance3.ionViewCanEnter).not.toHaveBeenCalled();
expect(instance3.ionViewWillEnter).not.toHaveBeenCalled();
expect(instance3.ionViewDidEnter).not.toHaveBeenCalled();
expect(instance3.ionViewCanLeave).toHaveBeenCalled();
expect(instance3.ionViewWillLeave).toHaveBeenCalled();
expect(instance3.ionViewDidLeave).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView1', 'MockView3', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1);
});
it('should set a page component as the root, with transition', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
let view3 = mockView(MockView3);
mockViews(nav, [view1, view2, view3]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
let instance3 = spyOnLifecycles(view3);
nav.setRoot(MockView4, null, null, trnsDone);
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
expect(instance3.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView4', 'MockView3', DIRECTION_BACK
);
expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView4);
});
});
describe('setPages', () => {
it('should set the pages from an array, starting at the root, with transition', () => {
let view1 = mockView(MockView1);
let view2 = mockView(MockView2);
mockViews(nav, [view1, view2]);
let instance1 = spyOnLifecycles(view1);
let instance2 = spyOnLifecycles(view2);
nav.setPages([{page: MockView4}, {page: MockView5}], null, trnsDone);
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
let hasCompleted = true;
let requiresTransition = true;
expect(trnsDone).toHaveBeenCalledWith(
hasCompleted, requiresTransition, 'MockView5', 'MockView2', DIRECTION_BACK
);
expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView4);
expect(nav.getByIndex(1).component).toEqual(MockView5);
});
});
describe('_nextTrns', () => {
it('should not start next transition when already transitioning', () => {
nav.setTransitioning(true);
expect(nav._nextTrns()).toEqual(false);
});
it('should not start next transition nothing in the queue', () => {
expect(nav._nextTrns()).toEqual(false);
});
});
// describe('_cleanup', () => {
// it('should destroy views that are inactive after the active view', () => {
// let view1 = new ViewController(Page1);
// view1.state = STATE_INACTIVE;
// let view2 = new ViewController(Page2);
// view2.state = STATE_ACTIVE;
// let view3 = new ViewController(Page3);
// view3.state = STATE_INACTIVE;
// let view4 = new ViewController(Page4);
// view4.state = STATE_TRANS_ENTER;
// let view5 = new ViewController(Page5);
// view5.state = STATE_INACTIVE;
// nav._views = [view1, view2, view3, view4, view5];
// nav._cleanup();
// expect(nav.length()).toBe(3);
// expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
// expect(nav.getByIndex(0).component).toBe(Page1);
// expect(nav.getByIndex(1).state).toBe(STATE_ACTIVE);
// expect(nav.getByIndex(1).component).toBe(Page2);
// expect(nav.getByIndex(2).state).toBe(STATE_TRANS_ENTER);
// expect(nav.getByIndex(2).component).toBe(Page4);
// });
// it('should not destroy any views since the last is active', () => {
// let view1 = new ViewController(Page1);
// view1.state = STATE_INACTIVE;
// let view2 = new ViewController(Page2);
// view2.state = STATE_ACTIVE;
// nav._views = [view1, view2];
// nav._cleanup();
// expect(nav.length()).toBe(2);
// });
// it('should call destroy for each view to be destroyed', () => {
// let view1 = new ViewController(Page1);
// view1.state = STATE_ACTIVE;
// let view2 = new ViewController(Page2);
// view2.state = STATE_INACTIVE;
// let view3 = new ViewController(Page3);
// view3.state = STATE_INACTIVE;
// nav._views = [view1, view2, view3];
// spyOn(view1, 'destroy');
// spyOn(view2, 'destroy');
// spyOn(view3, 'destroy');
// nav._cleanup();
// expect(nav.length()).toBe(1);
// expect(view1._willUnload).not.toHaveBeenCalled();
// expect(view2._willUnload).toHaveBeenCalled();
// expect(view3._willUnload).toHaveBeenCalled();
// });
// it('should reset zIndexes if their is a negative zindex', () => {
// let view1 = new ViewController(Page1);
// view1._setPageElementRef( mockElementRef() );
// view1.state = STATE_INACTIVE;
// view1._zIndex = -1;
// let view2 = new ViewController(Page2);
// view2._setPageElementRef( mockElementRef() );
// view2.state = STATE_INACTIVE;
// view2._zIndex = 0;
// let view3 = new ViewController(Page3);
// view3._setPageElementRef( mockElementRef() );
// view3.state = STATE_ACTIVE;
// view3._zIndex = 1;
// nav._views = [view1, view2, view3];
// nav._cleanup();
// expect(view1._zIndex).toEqual(100);
// expect(view2._zIndex).toEqual(101);
// expect(view3._zIndex).toEqual(102);
// });
// });
let nav: NavControllerBase;
let trnsDone: jasmine.Spy;
function spyOnLifecycles(view: ViewController) {
let instance = view.instance = {
ionViewDidLoad: () => {},
ionViewCanEnter: () => { return true; },
ionViewWillEnter: () => {},
ionViewDidEnter: () => {},
ionViewCanLeave: () => {},
ionViewWillLeave: () => { return true; },
ionViewDidLeave: () => {},
ionViewWillUnload: () => {},
};
spyOn(instance, 'ionViewDidLoad');
spyOn(instance, 'ionViewCanEnter');
spyOn(instance, 'ionViewWillEnter');
spyOn(instance, 'ionViewDidEnter');
spyOn(instance, 'ionViewCanLeave');
spyOn(instance, 'ionViewWillLeave');
spyOn(instance, 'ionViewDidLeave');
spyOn(instance, 'ionViewWillUnload');
return instance;
}
beforeEach(() => {
trnsDone = jasmine.createSpy('TransitionDone');
nav = mockNavController();
});
});

View File

@@ -0,0 +1,129 @@
import { convertToView, convertToViews, DIRECTION_BACK, DIRECTION_FORWARD, setZIndex } from '../nav-util';
import { mockDeepLinker, mockNavController, MockView, mockRenderer, mockView, mockViews } from '../../util/mock-providers';
import { ViewController } from '../view-controller';
describe('NavUtil', () => {
describe('convertToViews', () => {
it('should convert all page components', () => {
let linker = mockDeepLinker();
let pages = [{ page: MockView }, { page: MockView }, { page: MockView }];
let views = convertToViews(linker, pages);
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
});
it('should convert all string names', () => {
let linker = mockDeepLinker({
links: [{ component: MockView, name: 'someName' }]
});
let pages = [{ page: 'someName' }, { page: 'someName' }, { page: 'someName' }];
let views = convertToViews(linker, pages);
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
});
it('should convert all ViewControllers', () => {
let pages = [mockView(MockView), mockView(MockView), mockView(MockView)];
let linker = mockDeepLinker();
let views = convertToViews(linker, pages);
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
});
});
describe('convertToView', () => {
it('should return new ViewController instance from page component link config name', () => {
let linker = mockDeepLinker({
links: [{ component: MockView, name: 'someName' }]
});
let outputView = convertToView(linker, 'someName', null);
expect(outputView.component).toEqual(MockView);
});
it('should return new ViewController instance from page component', () => {
let linker = mockDeepLinker();
let outputView = convertToView(linker, MockView, null);
expect(outputView.component).toEqual(MockView);
});
it('should return existing ViewController instance', () => {
let linker = mockDeepLinker();
let inputView = new ViewController(MockView);
let outputView = convertToView(linker, inputView, null);
expect(outputView).toEqual(inputView);
});
it('should return null for null/undefined/number', () => {
let linker = mockDeepLinker();
let outputView = convertToView(linker, null, null);
expect(outputView).toEqual(null);
outputView = convertToView(linker, undefined, undefined);
expect(outputView).toEqual(null);
outputView = convertToView(linker, 8675309, null);
expect(outputView).toEqual(null);
});
});
describe('setZIndex', () => {
it('should set zIndex 100 when leaving view doesnt have a zIndex', () => {
let leavingView = mockView();
let enteringView = mockView();
let nav = mockNavController();
mockViews(nav, [leavingView, enteringView]);
setZIndex(nav, enteringView, leavingView, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(100);
});
it('should set zIndex 100 on first entering view', () => {
let enteringView = mockView();
let nav = mockNavController();
setZIndex(nav, enteringView, null, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(100);
});
it('should set zIndex 101 on second entering view', () => {
let leavingView = mockView();
leavingView._zIndex = 100;
let enteringView = mockView();
let nav = mockNavController();
setZIndex(nav, enteringView, leavingView, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(101);
});
it('should set zIndex 100 on entering view going back', () => {
let leavingView = mockView();
leavingView._zIndex = 101;
let enteringView = mockView();
let nav = mockNavController();
setZIndex(nav, enteringView, leavingView, DIRECTION_BACK, mockRenderer());
expect(enteringView._zIndex).toEqual(100);
});
it('should set zIndex 9999 on first entering portal view', () => {
let enteringView = mockView();
let nav = mockNavController();
nav._isPortal = true;
setZIndex(nav, enteringView, null, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(9999);
});
});
});

View File

@@ -0,0 +1,98 @@
import { mockView } from '../../util/mock-providers';
describe('ViewController', () => {
describe('willEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.willEnter.subscribe((event) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._willEnter();
}, 10000);
});
describe('didEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.didEnter.subscribe((event) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._didEnter();
}, 10000);
});
describe('willLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.willLeave.subscribe((event) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._willLeave();
}, 10000);
});
describe('didLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.didLeave.subscribe((event) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._didLeave();
}, 10000);
});
describe('willUnload', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.willUnload.subscribe((event) => {
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._willUnload();
}, 10000);
});
afterEach(() => {
if (subscription) {
subscription.unsubscribe();
}
});
let subscription: any = null;
});

View File

@@ -1,10 +1,10 @@
import { ChangeDetectorRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core';
import { ComponentRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core';
import { Footer, Header } from '../toolbar/toolbar';
import { isPresent, merge } from '../../util/util';
import { Navbar } from '../navbar/navbar';
import { NavController } from './nav-controller';
import { NavOptions } from './nav-interfaces';
import { Footer, Header } from '../components/toolbar/toolbar';
import { isPresent, merge } from '../util/util';
import { Navbar } from '../components/navbar/navbar';
import { NavControllerBase } from './nav-controller-base';
import { NavOptions, ViewState } from './nav-util';
import { NavParams } from './nav-params';
@@ -28,19 +28,14 @@ import { NavParams } from './nav-params';
export class ViewController {
private _cntDir: any;
private _cntRef: ElementRef;
private _tbRefs: ElementRef[] = [];
private _hdrDir: Header;
private _ftrDir: Footer;
private _destroyFn: Function;
private _hdAttr: string = null;
private _leavingOpts: NavOptions = null;
private _loaded: boolean = false;
private _nbDir: Navbar;
private _onDidDismiss: Function = null;
private _onWillDismiss: Function = null;
private _pgRef: ElementRef;
private _cd: ChangeDetectorRef;
protected _nav: NavController;
private _hidden: string;
private _leavingOpts: NavOptions;
private _nb: Navbar;
private _onDidDismiss: Function;
private _onWillDismiss: Function;
private _detached: boolean;
/**
* Observable to be subscribed to when the current component will become active
@@ -67,69 +62,75 @@ export class ViewController {
didLeave: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component will be destroyed
* Observable to be subscribed to when the current component has been destroyed
* @returns {Observable} Returns an observable
*/
willUnload: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component has been destroyed
* @returns {Observable} Returns an observable
*/
didUnload: EventEmitter<any>;
/**
* @internal
*/
/** @private */
data: any;
/**
* @private
*/
/** @private */
instance: any;
/** @private */
id: string;
/**
* @private
*/
instance: any = {};
/**
* @private
*/
state: number = 0;
/**
* @private
* If this is currently the active view, then set to false
* if it does not want the other views to fire their own lifecycles.
*/
fireOtherLifecycles: boolean = true;
/**
* @private
*/
/** @private */
isOverlay: boolean = false;
/**
* @private
*/
zIndex: number;
/** @private */
_cmp: ComponentRef<any>;
/**
* @private
*/
/** @private */
_nav: NavControllerBase;
/** @private */
_zIndex: number;
/** @private */
_state: ViewState;
/** @private */
_cssClass: string;
/** @private */
@Output() private _emitter: EventEmitter<any> = new EventEmitter();
constructor(public componentType?: any, data?: any) {
constructor(public component?: any, data?: any, rootCssClass: string = DEFAULT_CSS_CLASS) {
// passed in data could be NavParams, but all we care about is its data object
this.data = (data instanceof NavParams ? data.data : (isPresent(data) ? data : {}));
this._cssClass = rootCssClass;
this.willEnter = new EventEmitter();
this.didEnter = new EventEmitter();
this.willLeave = new EventEmitter();
this.didLeave = new EventEmitter();
this.willUnload = new EventEmitter();
this.didUnload = new EventEmitter();
}
/**
* @private
*/
init(componentRef: ComponentRef<any>) {
this._cmp = componentRef;
this.instance = this.instance || componentRef.instance;
this._detached = false;
}
/**
* @private
*/
_setNav(navCtrl: NavControllerBase) {
this._nav = navCtrl;
}
/**
* @private
*/
_setInstance(instance: any) {
this.instance = instance;
}
/**
@@ -150,28 +151,28 @@ export class ViewController {
* @private
* onDismiss(..) has been deprecated. Please use onDidDismiss(..) instead
*/
private onDismiss(callback: Function) {
onDismiss(callback: Function) {
// deprecated warning: added beta.11 2016-06-30
console.warn('onDismiss(..) has been deprecated. Please use onDidDismiss(..) instead');
this.onDidDismiss(callback);
}
/**
* @private
* onDidDismiss
*/
onDidDismiss(callback: Function) {
this._onDidDismiss = callback;
}
/**
* @private
* onWillDismiss
*/
onWillDismiss(callback: Function) {
this._onWillDismiss = callback;
}
/**
* @private
* dismiss
*/
dismiss(data?: any, role?: any, navOptions: NavOptions = {}) {
let options = merge({}, this._leavingOpts, navOptions);
@@ -182,13 +183,6 @@ export class ViewController {
});
}
/**
* @private
*/
setNav(navCtrl: NavController) {
this._nav = navCtrl;
}
/**
* @private
*/
@@ -233,25 +227,11 @@ export class ViewController {
return false;
}
/**
* @private
*/
setChangeDetector(cd: ChangeDetectorRef) {
this._cd = cd;
}
/**
* @private
*/
setInstance(instance: any) {
this.instance = instance;
}
/**
* @private
*/
get name(): string {
return this.componentType ? this.componentType['name'] : '';
return this.component ? this.component.name : '';
}
/**
@@ -278,57 +258,68 @@ export class ViewController {
/**
* @private
* DOM WRITE
*/
domShow(shouldShow: boolean, renderer: Renderer) {
_domShow(shouldShow: boolean, renderer: Renderer) {
// using hidden element attribute to display:none and not render views
// renderAttr of '' means the hidden attribute will be added
// renderAttr of null means the hidden attribute will be removed
// doing checks to make sure we only make an update to the element when needed
if (this._pgRef &&
(shouldShow && this._hdAttr === '' ||
!shouldShow && this._hdAttr !== '')) {
this._hdAttr = (shouldShow ? null : '');
renderer.setElementAttribute(this._pgRef.nativeElement, 'hidden', this._hdAttr);
// _hidden value of '' means the hidden attribute will be added
// _hidden value of null means the hidden attribute will be removed
// doing checks to make sure we only update the DOM when actually needed
if (this._cmp) {
// if it should render, then the hidden attribute should not be on the element
if (shouldShow && this._hidden === '' || !shouldShow && this._hidden !== '') {
this._hidden = (shouldShow ? null : '');
// ******** DOM WRITE ****************
renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', this._hidden);
}
}
}
/**
* @private
* DOM WRITE
*/
setZIndex(zIndex: number, renderer: Renderer) {
if (this._pgRef && zIndex !== this.zIndex) {
this.zIndex = zIndex;
renderer.setElementStyle(this._pgRef.nativeElement, 'z-index', zIndex.toString());
_setZIndex(zIndex: number, renderer: Renderer) {
if (zIndex !== this._zIndex) {
this._zIndex = zIndex;
const pageRef = this.pageRef();
if (pageRef) {
// ******** DOM WRITE ****************
renderer.setElementStyle(pageRef.nativeElement, 'z-index', (<any>zIndex));
}
}
}
/**
* @private
*/
setPageRef(elementRef: ElementRef) {
this._pgRef = elementRef;
}
/**
* @private
* @returns {elementRef} Returns the Page's ElementRef
* @returns {ElementRef} Returns the Page's ElementRef.
*/
pageRef(): ElementRef {
return this._pgRef;
return this._cmp && this._cmp.location;
}
/**
* @private
*/
setContentRef(elementRef: ElementRef) {
_setContent(directive: any) {
this._cntDir = directive;
}
/**
* @returns {component} Returns the Page's Content component reference.
*/
getContent() {
return this._cntDir;
}
/**
* @private
*/
_setContentRef(elementRef: ElementRef) {
this._cntRef = elementRef;
}
/**
* @private
* @returns {elementRef} Returns the Page's Content ElementRef
* @returns {ElementRef} Returns the Content's ElementRef.
*/
contentRef(): ElementRef {
return this._cntRef;
@@ -337,28 +328,7 @@ export class ViewController {
/**
* @private
*/
setContent(directive: any) {
this._cntDir = directive;
}
/**
* @private
*/
setToolbarRef(elementRef: ElementRef) {
this._tbRefs.push(elementRef);
}
/**
* @private
*/
toolbarRefs(): ElementRef[] {
return this._tbRefs;
}
/**
* @private
*/
setHeader(directive: Header) {
_setHeader(directive: Header) {
this._hdrDir = directive;
}
@@ -372,7 +342,7 @@ export class ViewController {
/**
* @private
*/
setFooter(directive: Footer) {
_setFooter(directive: Footer) {
this._ftrDir = directive;
}
@@ -385,83 +355,26 @@ export class ViewController {
/**
* @private
* @returns {component} Returns the Page's Content component reference.
*/
getContent() {
return this._cntDir;
_setNavbar(directive: Navbar) {
this._nb = directive;
}
/**
* @private
*/
setNavbar(directive: Navbar) {
this._nbDir = directive;
getNavbar(): Navbar {
return this._nb;
}
/**
* @private
*/
getNavbar() {
return this._nbDir;
}
/**
*
* Find out if the current component has a NavBar or not. Be sure
* to wrap this in an `ionViewWillEnter` method in order to make sure
* the view has rendered fully.
* @returns {boolean} Returns a boolean if this Page has a navbar or not.
*/
hasNavbar(): boolean {
return !!this.getNavbar();
}
/**
* @private
*/
navbarRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getElementRef();
}
/**
* @private
*/
titleRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getTitleRef();
}
/**
* @private
*/
navbarItemRefs(): Array<ElementRef> {
let navbar = this.getNavbar();
return navbar && navbar.getItemRefs();
}
/**
* @private
*/
backBtnRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getBackButtonRef();
}
/**
* @private
*/
backBtnTextRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getBackButtonTextRef();
}
/**
* @private
*/
navbarBgRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getBackgroundRef();
return !!this._nb;
}
/**
@@ -470,10 +383,7 @@ export class ViewController {
* @param {string} backButtonText Set the back button text.
*/
setBackButtonText(val: string) {
let navbar = this.getNavbar();
if (navbar) {
navbar.setBackButtonText(val);
}
this._nb && this._nb.setBackButtonText(val);
}
/**
@@ -482,31 +392,11 @@ export class ViewController {
* @param {boolean} Set if this Page's back button should show or not.
*/
showBackButton(shouldShow: boolean) {
let navbar = this.getNavbar();
if (navbar) {
navbar.hideBackButton = !shouldShow;
if (this._nb) {
this._nb.hideBackButton = !shouldShow;
}
}
/**
* @private
*/
isLoaded(): boolean {
return this._loaded;
}
/**
* The loaded method is used to load any dynamic content/components
* into the dom before proceeding with the transition. If a component
* needs dynamic component loading, extending ViewController and
* overriding this method is a good option
* @param {function} done is a callback that must be called when async
* loading/actions are completed
*/
loaded(done: (() => any)) {
done();
}
/**
* @private
* The view has loaded. This event only happens once per view being
@@ -515,23 +405,31 @@ export class ViewController {
* to put your setup code for the view; however, it is not the
* recommended method to use when a view becomes active.
*/
fireLoaded() {
this._loaded = true;
ctrlFn(this, 'Loaded');
_didLoad() {
// deprecated warning: added 2016-08-14, beta.12
if (this.instance && this.instance.ionViewLoaded) {
try {
console.warn('ionViewLoaded() has been deprecated. Please rename to ionViewDidLoad()');
this.instance.ionViewLoaded();
} catch (e) {
console.error(this.name + ' iionViewLoaded: ' + e.message);
}
}
ctrlFn(this, 'DidLoad');
}
/**
* @private
* The view is about to enter and become the active view.
*/
fireWillEnter() {
if (this._cd) {
_willEnter() {
if (this._detached && this._cmp) {
// ensure this has been re-attached to the change detector
this._cd.reattach();
// detect changes before we run any user code
this._cd.detectChanges();
this._cmp.changeDetectorRef.reattach();
this._detached = false;
}
this.willEnter.emit(null);
ctrlFn(this, 'WillEnter');
}
@@ -541,9 +439,8 @@ export class ViewController {
* The view has fully entered and is now the active view. This
* will fire, whether it was the first load or loaded from the cache.
*/
fireDidEnter() {
let navbar = this.getNavbar();
navbar && navbar.didEnter();
_didEnter() {
this._nb && this._nb.didEnter();
this.didEnter.emit(null);
ctrlFn(this, 'DidEnter');
}
@@ -552,7 +449,7 @@ export class ViewController {
* @private
* The view has is about to leave and no longer be the active view.
*/
fireWillLeave() {
_willLeave() {
this.willLeave.emit(null);
ctrlFn(this, 'WillLeave');
}
@@ -562,60 +459,87 @@ export class ViewController {
* The view has finished leaving and is no longer the active view. This
* will fire, whether it is cached or unloaded.
*/
fireDidLeave() {
_didLeave() {
this.didLeave.emit(null);
ctrlFn(this, 'DidLeave');
// when this is not the active page
// we no longer need to detect changes
this._cd && this._cd.detach();
if (!this._detached && this._cmp) {
this._cmp.changeDetectorRef.detach();
this._detached = true;
}
}
/**
* @private
* The view is about to be destroyed and have its elements removed.
*/
fireWillUnload() {
_willUnload() {
this.willUnload.emit(null);
ctrlFn(this, 'WillUnload');
}
/**
* @private
*/
onDestroy(destroyFn: Function) {
this._destroyFn = destroyFn;
}
/**
* @private
*/
destroy() {
this.didUnload.emit(null);
ctrlFn(this, 'DidUnload');
this._destroyFn && this._destroyFn();
this._destroyFn = null;
}
}
export interface LifeCycleEvent {
componentType?: any;
}
function ctrlFn(viewCtrl: ViewController, fnName: string) {
if (viewCtrl.instance) {
// deprecated warning: added 2016-06-01, beta.8
if (viewCtrl.instance['onPage' + fnName]) {
// deprecated warning: added 2016-08-14, beta.12
if (this.instance && this.instance.ionViewDidUnload) {
console.warn('ionViewDidUnload() has been deprecated. Please use ionViewWillUnload() instead');
try {
console.warn('onPage' + fnName + '() has been deprecated. Please rename to ionView' + fnName + '()');
viewCtrl.instance['onPage' + fnName]();
this.instance.ionViewDidUnload();
} catch (e) {
console.error(viewCtrl.name + ' onPage' + fnName + ': ' + e.message);
console.error(this.name + ' ionViewDidUnload: ' + e.message);
}
}
}
/**
* @private
* DOM WRITE
*/
_destroy(renderer: Renderer) {
if (this._cmp) {
if (renderer) {
// ensure the element is cleaned up for when the view pool reuses this element
// ******** DOM WRITE ****************
renderer.setElementAttribute(this._cmp.location.nativeElement, 'class', null);
renderer.setElementAttribute(this._cmp.location.nativeElement, 'style', null);
}
// completely destroy this component. boom.
this._cmp.destroy();
}
if (this._nav) {
// remove it from the nav
const index = this._nav.indexOf(this);
if (index > -1) {
this._nav._views.splice(index, 1);
}
}
this._nav = this._cmp = this.instance = this._cntDir = this._cntRef = this._hdrDir = this._ftrDir = this._nb = this._onDidDismiss = this._onWillDismiss = null;
}
/**
* @private
*/
_lifecycleTest(lifecycle: string): boolean | string | Promise<any> {
let result: any = true;
if (this.instance && this.instance['ionViewCan' + lifecycle]) {
try {
result = this.instance['ionViewCan' + lifecycle]();
} catch (e) {
console.error(`${this.name} ionViewCan${lifecycle} error: ${e}`);
result = false;
}
}
return result;
}
}
function ctrlFn(viewCtrl: ViewController, fnName: string) {
if (viewCtrl.instance) {
// fire off ionView lifecycle instance method
if (viewCtrl.instance['ionView' + fnName]) {
try {
@@ -626,3 +550,10 @@ function ctrlFn(viewCtrl: ViewController, fnName: string) {
}
}
}
export function isViewController(viewCtrl: any) {
return !!(viewCtrl && (<ViewController>viewCtrl)._didLoad && (<ViewController>viewCtrl)._willUnload);
}
const DEFAULT_CSS_CLASS = 'ion-page';