refactor(NavController): improve transitions, view stages

This refactor made it so view transitions do no step on one another when a new transition happens
during an active transition.
This commit is contained in:
Adam Bradley
2016-01-19 14:24:49 -06:00
parent 332c761b9e
commit 3213d02375
12 changed files with 1431 additions and 1013 deletions

View File

@@ -554,7 +554,7 @@ export class Animation {
/*
STATIC CLASSES
*/
static create(element, name) {
static create(name) {
let AnimationClass = AnimationRegistry[name];
if (!AnimationClass) {
@@ -562,7 +562,7 @@ export class Animation {
// fallback to just the base Animation class
AnimationClass = Animation;
}
return new AnimationClass(element);
return new AnimationClass();
}
static createTransition(enteringView: ViewController, leavingView: ViewController, opts: any = {}) {

View File

@@ -16,8 +16,8 @@ class IOSTransition extends Animation {
constructor(enteringView, leavingView, opts) {
super(null, opts);
this.duration(DURATION);
this.easing(EASING);
this.duration(opts.duration || DURATION);
this.easing(opts.easing || EASING);
// what direction is the transition going
let backDirection = (opts.direction === 'back');

View File

@@ -24,11 +24,11 @@ class MDTransition extends Animation {
this.add(enteringPage);
if (backDirection) {
this.duration(200).easing('cubic-bezier(0.47,0,0.745,0.715)');
this.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)');
enteringPage.fromTo(TRANSLATEY, CENTER, CENTER);
} else {
this.duration(280).easing('cubic-bezier(0.36,0.66,0.04,1)');
this.duration(opts.duration || 280).easing('cubic-bezier(0.36,0.66,0.04,1)');
enteringPage
.fromTo(TRANSLATEY, OFF_BOTTOM, CENTER)
.fadeIn();
@@ -51,7 +51,7 @@ class MDTransition extends Animation {
// setup leaving view
if (leavingView && backDirection) {
// leaving content
this.duration(200).easing('cubic-bezier(0.47,0,0.745,0.715)');
this.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)');
let leavingPage = new Animation(leavingView.pageRef());
this.add(leavingPage.fromTo(TRANSLATEY, CENTER, OFF_BOTTOM).fadeOut());
}

View File

@@ -21,6 +21,7 @@ export * from './components/modal/modal'
export * from './components/nav/nav'
export * from './components/nav/nav-controller'
export * from './components/nav/view-controller'
export * from './components/nav/nav-params'
export * from './components/nav/nav-push'
export * from './components/nav/nav-router'
export * from './components/navbar/navbar'

View File

@@ -315,7 +315,7 @@ class AlertCmp {
if (this.d.message) {
this.descId = this.msgId;
} else if (this.d.subTitle) {
this.descId = this.subHdrId;
}
@@ -379,7 +379,7 @@ class AlertCmp {
if (activeElement) {
activeElement.blur();
}
if (this.d.inputs.length) {
let firstInput = this._elementRef.nativeElement.querySelector('input');
if (firstInput) {

View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import {
Location} from 'angular2/router';
import {Nav} from './nav';
import {ViewController} from './view-controller';
/**
* @private
@@ -16,11 +17,11 @@ import {Nav} from './nav';
})
export class NavRouter extends RouterOutlet {
private _activeViewId;
constructor(
_elementRef: ElementRef,
_elementRef: ElementRef,
_loader: DynamicComponentLoader,
_parentRouter: Router,
_parentRouter: Router,
@Attribute('name') nameAttr: string,
private _nav: Nav
) {
@@ -38,10 +39,10 @@ export class NavRouter extends RouterOutlet {
* @param {ComponentInstruction} instruction TODO
*/
activate(nextInstruction: ComponentInstruction): Promise<any> {
var previousInstruction = this._currentInstruction;
this._currentInstruction = nextInstruction;
var previousInstruction = this['_currentInstruction'];
this['_currentInstruction'] = nextInstruction;
var componentType = nextInstruction.componentType;
var childRouter = this._parentRouter.childRouter(componentType);
var childRouter = this['_parentRouter'].childRouter(componentType);
// prevent double navigations to the same view
var lastView = this._nav.last();
@@ -58,11 +59,11 @@ export class NavRouter extends RouterOutlet {
}
/**
* TODO
* @param {TODO} type TODO
* @param {TODO} viewCtrl TODO
* Called by Ionic after a transition has completed.
* @param {string} direction The direction of the state change
* @param {ViewController} viewCtrl The entering ViewController
*/
stateChange(type, viewCtrl) {
stateChange(direction: string, viewCtrl: ViewController) {
// stateChange is called by Ionic's NavController
// type could be "push" or "pop"
// viewCtrl is Ionic's ViewController class, which has the properties "componentType" and "params"
@@ -79,9 +80,9 @@ export class NavRouter extends RouterOutlet {
let componentInstruction = pathRecognizer.generate(viewCtrl.data);
// create a ResolvedInstruction from the componentInstruction
let instruction = new ResolvedInstruction(componentInstruction, null);
let instruction = new ResolvedInstruction(componentInstruction, null, null);
this._parentRouter.navigateByInstruction(instruction);
this['_parentRouter'].navigateByInstruction(instruction);
}
}
@@ -92,7 +93,7 @@ export class NavRouter extends RouterOutlet {
*/
getPathRecognizerByComponent(componentType) {
// given a componentType, figure out the best PathRecognizer to use
let rules = this._parentRouter.registry._rules;
let rules = this['_parentRouter'].registry._rules;
let pathRecognizer = null;
rules.forEach((rule) => {

View File

@@ -1,4 +1,4 @@
import {Component} from 'angular2/core';
import {Component, Type} from 'angular2/core';
import {App, NavController} from 'ionic/ionic';
import {Page, Config, IonicApp} from 'ionic/ionic';
import {NavParams, NavController, ViewController, IONIC_DIRECTIVES} from 'ionic/ionic';
@@ -37,7 +37,7 @@ class MyCmpTest{}
<ion-label>Text Input</ion-label>
<textarea></textarea>
</ion-input>
<button ion-item [navPush]="[pushPage, {id: 42}]">Push FullPage w/ [navPush] array</button>
<button ion-item [navPush]="pushPage" [navParams]="{id:40}">Push w/ [navPush] and [navParams]</button>
<button ion-item [navPush]="[\'FirstPage\', {id: 22}]">Push w/ [navPush] array and string view name</button>
@@ -45,6 +45,8 @@ class MyCmpTest{}
<button ion-item (click)="setPages()">setPages() (Go to PrimaryHeaderPage)</button>
<button ion-item (click)="setRoot()">setRoot(PrimaryHeaderPage) (Go to PrimaryHeaderPage)</button>
<button ion-item (click)="nav.pop()">Pop</button>
<button ion-item (click)="quickPush()">New push during transition</button>
<button ion-item (click)="quickPop()">New pop during transition</button>
<button ion-item (click)="reload()">Reload</button>
<button *ngFor="#i of pages" ion-item (click)="pushPrimaryHeaderPage()">Page {{i}}</button>
@@ -54,17 +56,17 @@ class MyCmpTest{}
directives: [MyCmpTest]
})
class FirstPage {
pushPage;
title = 'First Page';
pages: Array<number> = [];
constructor(
nav: NavController,
private nav: NavController,
app: IonicApp,
config: Config
) {
this.nav = nav;
this.title = 'First Page';
this.pushPage = FullPage;
this.pages = [];
for (var i = 1; i <= 50; i++) {
this.pages.push(i);
}
@@ -72,7 +74,7 @@ class FirstPage {
setPages() {
let items = [
PrimaryHeaderPage
{page: PrimaryHeaderPage}
];
this.nav.setPages(items);
@@ -94,6 +96,20 @@ class FirstPage {
this.nav.push(AnotherPage);
}
quickPush() {
this.nav.push(AnotherPage);
setTimeout(() => {
this.nav.push(PrimaryHeaderPage);
}, 150);
}
quickPop() {
this.nav.push(AnotherPage);
setTimeout(() => {
this.nav.remove(1, 1);
}, 250);
}
reload() {
window.location.reload();
}
@@ -116,17 +132,14 @@ class FirstPage {
})
class FullPage {
constructor(
nav: NavController,
params: NavParams
) {
this.nav = nav;
this.params = params;
}
private nav: NavController,
private params: NavParams
) {}
setPages() {
let items = [
FirstPage,
PrimaryHeaderPage
{page: FirstPage},
{page: PrimaryHeaderPage}
];
this.nav.setPages(items);
@@ -157,6 +170,7 @@ class FullPage {
<p><button (click)="pushAnother()">Push to AnotherPage</button></p>
<p><button (click)="pushFullPage()">Push to FullPage</button></p>
<p><button (click)="setRoot()">setRoot(AnotherPage)</button></p>
<p><button (click)="nav.popToRoot()">Pop to root</button></p>
<p><button id="insert" (click)="insert()">Insert first page into history before this</button></p>
<p><button id="remove" (click)="removeSecond()">Remove second page in history</button></p>
<div class="yellow"><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f></div>
@@ -165,12 +179,9 @@ class FullPage {
})
class PrimaryHeaderPage {
constructor(
nav: NavController,
viewCtrl: ViewController
) {
this.nav = nav;
this.viewCtrl = viewCtrl;
}
private nav: NavController,
private viewCtrl: ViewController
) {}
onPageWillEnter() {
this.viewCtrl.setBackButtonText('Previous');
@@ -225,15 +236,13 @@ class PrimaryHeaderPage {
`
})
class AnotherPage {
bbHideToggleVal = false;
bbCount = 0;
constructor(
nav: NavController,
viewCtrl: ViewController
) {
this.nav = nav;
this.viewCtrl = viewCtrl;
this.bbHideToggleVal = false;
this._bbCount = 0;
}
private nav: NavController,
private viewCtrl: ViewController
) {}
pushFullPage() {
this.nav.push(FullPage);
@@ -259,12 +268,12 @@ class AnotherPage {
setBackButtonText() {
let backButtonText = 'Messages';
if (this._bbCount > 0) {
backButtonText += ` (${this._bbCount})`;
if (this.bbCount > 0) {
backButtonText += ` (${this.bbCount})`;
}
this.viewCtrl.setBackButtonText(backButtonText);
++this._bbCount;
++this.bbCount;
}
}
@@ -277,6 +286,8 @@ class AnotherPage {
}
})
class E2EApp {
root;
constructor() {
this.root = FirstPage;
}

View File

@@ -1,301 +1,268 @@
import {
NavController,
Config,
Page,
ViewController
} from 'ionic/ionic';
import {NavController, Config, ViewController} from 'ionic/ionic';
export function run() {
describe("NavController", () => {
let nav;
describe('NavController', () => {
class FirstPage {}
class SecondPage {}
class ThirdPage {}
describe('popToRoot', () => {
function mockTransitionFn(enteringView, leavingView, opts, cb) {
let destroys = [];
it('should go back to root', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INACTIVE;
let view3 = new ViewController(Page3);
view3.state = STATE_INACTIVE;
let view4 = new ViewController(Page4);
view4.state = STATE_ACTIVE;
nav._views = [view1, view2, view3, view4];
nav._views.forEach(view => {
if (view) {
if (view.shouldDestroy) {
destroys.push(view);
nav.popToRoot();
expect(nav.length()).toBe(2);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(1).componentType).toBe(Page4);
} else if (view.state === 2 && view.shouldCache) {
view.shouldCache = false;
}
}
});
destroys.forEach(view => {
nav._remove(view);
view.destroy();
});
cb();
}
function mockCanGoBackFn() {
return true;
}
beforeEach(() => {
nav = new NavController(null, null, new Config(), null, null, null, null, null, null, null);
nav._renderer = {
setElementAttribute: function(){},
setElementStyle: function(){}
};
});
it('should exist', () => {
expect(nav).not.toBeUndefined();
});
describe("getActive", () => {
it('should return null if there is no active view', () => {
var active = nav.getActive();
expect(active).toBe(null);
});
it('should return the last active page', () => {
let activeView = new ViewController();
activeView.state = 1; // ACTIVE_STATE
nav._add(activeView);
expect(nav.getActive()).toBe(activeView);
let secondActiveView = new ViewController();
secondActiveView.state = 1; // ACTIVE_STATE
nav._add(secondActiveView);
expect(nav.getActive()).toBe(secondActiveView);
});
it('should return the last active page thats not shouldDestroy', () => {
let view1 = new ViewController();
view1.state = 1; // ACTIVE_STATE
nav._add(view1);
expect(nav.getActive()).toBe(view1);
let view2 = new ViewController();
view2.state = 1; // ACTIVE_STATE
view2.shouldDestroy = true;
nav._add(view2);
expect(nav.getActive()).toBe(view1);
});
});
describe("push", () => {
it('should return a rejected Promise if page is falsy', done => {
let s = jasmine.createSpy('success');
let f = jasmine.createSpy('fail');
let promise = nav.push(undefined, {}, {});
promise.then(s, f).then(() => {
expect(s).not.toHaveBeenCalled();
expect(f).toHaveBeenCalled();
done();
});
});
it('should throw an error if page truthy, but is not a function', () => {
let push = () => nav.push({}, {}, {});
expect(push).toThrow();
push = () => nav.push("string", {}, {});
expect(push).toThrow();
push = () => nav.push(42, {}, {});
expect(push).toThrow();
push = () => nav.push(true, {}, {});
expect(push).toThrow();
});
it('to add the pushed page to the nav stack', (done) => {
expect(FirstPage).toBeDefined();
expect(nav._views.length).toBe(0);
spyOn(nav, '_add').and.callThrough();
nav._transition = mockTransitionFn;
nav.push(FirstPage, {}, {}).then(() => {
expect(nav._add).toHaveBeenCalled();
expect(nav._views.length).toBe(1);
done();
});
});
});
describe("setPages", () => {
it('should return a resolved Promise if components is falsy', done => {
let s = jasmine.createSpy('success');
let f = jasmine.createSpy('fail');
let promise = nav.setPages();
promise.then(s, f).then(() => {
expect(s).toHaveBeenCalled();
expect(f).not.toHaveBeenCalled();
done();
});
});
it('replace views with the supplied views', () => {
let vc1 = new ViewController(),
vc2 = new ViewController(),
vc3 = new ViewController();
nav._views = [vc1, vc2, vc3];
let arr = [{page: FirstPage}, {page:SecondPage}, {page:ThirdPage}];
nav._transition = mockTransitionFn;
nav.setPages(arr);
//_views[0] will be transitioned out of
expect(nav._views[1].componentType).toBe(FirstPage);
expect(nav._views[2].componentType).toBe(SecondPage);
expect(nav._views[3].componentType).toBe(ThirdPage);
expect(view2.state).toBe(STATE_REMOVE);
expect(view3.state).toBe(STATE_REMOVE);
});
});
describe("insert", () => {
it('insert page at the specified index', () => {
let view1 = new ViewController();
view1._loaded = true;
let view2 = new ViewController();
view2._loaded = true;
let view3 = new ViewController();
view3._loaded = true;
describe('popTo', () => {
it('should go back two views', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INACTIVE;
let view3 = new ViewController(Page3);
view3.state = STATE_INACTIVE;
let view4 = new ViewController(Page4);
view4.state = STATE_ACTIVE;
nav._views = [view1, view2, view3, view4];
nav.popTo(view2);
expect(nav.length()).toBe(3);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(view3.state).toBe(STATE_REMOVE);
expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(2).componentType).toBe(Page4);
});
});
describe('_remove', () => {
it('should reassign activily transitioning leave that isnt getting removed, to become force active', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_TRANS_LEAVE;
let view3 = new ViewController(Page3);
view3.state = STATE_TRANS_ENTER;
nav._views = [view1, view2, view3];
expect(nav._views[2].componentType).toBeUndefined();
nav.insert(2, FirstPage);
expect(nav._views[2].componentType).toBe(FirstPage);
nav._remove(2, 1);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_FORCE_ACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS);
expect(nav.getByIndex(2).componentType).toBe(Page3);
});
it('push page if index >= _views.length', () => {
let view1 = new ViewController();
view1._loaded = true;
let view2 = new ViewController();
view2._loaded = true;
let view3 = new ViewController();
view3._loaded = true;
it('should reassign activily transitioning views that should be removed to STATE_REMOVE_AFTER_TRANS', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_TRANS_ENTER;
let view3 = new ViewController(Page3);
view3.state = STATE_TRANS_LEAVE;
nav._views = [view1, view2, view3];
spyOn(nav, 'push').and.callThrough();
nav.insert(2, FirstPage);
expect(nav.push).not.toHaveBeenCalled();
nav._transition = mockTransitionFn;
nav.insert(4, FirstPage);
expect(nav._views[4].componentType).toBe(FirstPage);
expect(nav.push).toHaveBeenCalled();
nav.setTransitioning(false);
nav.insert(10, FirstPage);
expect(nav._views[5].componentType).toBe(FirstPage);
nav._remove(1, 2);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_REMOVE_AFTER_TRANS);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS);
expect(nav.getByIndex(2).componentType).toBe(Page3);
});
it('another insert happened before last insert rendered, abort previous insert enter', () => {
let view1 = new ViewController();
view1._loaded = true;
view1.state = NavController.STATE_ABORT;
let view2 = new ViewController();
view2._loaded = true;
it('should keep same init leave, but set previous init enter to inactive', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INIT_ENTER;
let view3 = new ViewController(Page3);
view3.state = STATE_INIT_LEAVE;
nav._views = [view1, view2, view3];
nav._remove(1, 1);
expect(nav.length()).toBe(2);
expect(view1.state).toBe(STATE_INIT_ENTER);
expect(view2.state).toBe(STATE_REMOVE);
expect(view3.state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(1).componentType).toBe(Page3);
});
it('should set to pop the active and enter the previous', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_ACTIVE;
nav._views = [view1, view2];
nav._remove(1, 1);
expect(view1.state).toBe(STATE_INIT_ENTER);
expect(view2.state).toBe(STATE_INIT_LEAVE);
});
});
it('should set to remove 2 views before active one, active stays the same', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INACTIVE;
let view3 = new ViewController(Page3);
view3.state = STATE_INACTIVE;
let view4 = new ViewController(Page4);
view4.state = STATE_INACTIVE;
let view5 = new ViewController(Page5);
view5.state = STATE_ACTIVE;
nav._views = [view1, view2, view3, view4, view5];
describe("setRoot", () => {
it('remove previous views and set root', () => {
let vc1 = new ViewController(),
vc2 = new ViewController(),
vc3 = new ViewController();
nav._views = [vc1, vc2, vc3];
expect(nav._views.length).toBe(3);
nav._remove(2, 2);
expect(nav.length()).toBe(3);
expect(view1.state).toBe(STATE_INACTIVE);
expect(view2.state).toBe(STATE_INACTIVE);
expect(view3.state).toBe(STATE_REMOVE);
expect(view4.state).toBe(STATE_REMOVE);
expect(view5.state).toBe(STATE_ACTIVE);
nav._transition = mockTransitionFn;
nav.setRoot(FirstPage);
//_views[0] will be transitioned out of
expect(nav._views.length).toBe(2);
expect(nav._views[1].componentType).toBe(FirstPage);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_ACTIVE);
expect(nav.getByIndex(2).componentType).toBe(Page5);
});
it('should set to remove all views other than the first', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INACTIVE;
let view3 = new ViewController(Page3);
view3.state = STATE_INACTIVE;
let view4 = new ViewController(Page4);
view4.state = STATE_ACTIVE;
nav._views = [view1, view2, view3, view4];
nav._remove(1, 9999);
expect(nav.length()).toBe(2);
expect(view1.state).toBe(STATE_INIT_ENTER);
expect(view2.state).toBe(STATE_REMOVE);
expect(view3.state).toBe(STATE_REMOVE);
expect(view4.state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(1).componentType).toBe(Page4);
});
it('should set to remove 3 views and enter the first inactive one, remove includes active one', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INACTIVE;
let view3 = new ViewController(Page3);
view3.state = STATE_INACTIVE;
let view4 = new ViewController(Page4);
view4.state = STATE_ACTIVE;
nav._views = [view1, view2, view3, view4];
nav._remove(1, 3);
expect(nav.length()).toBe(2);
expect(view1.state).toBe(STATE_INIT_ENTER);
expect(view2.state).toBe(STATE_REMOVE);
expect(view3.state).toBe(STATE_REMOVE);
expect(view4.state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(1).componentType).toBe(Page4);
});
it('should set to remove the active and enter the previous', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_ACTIVE;
nav._views = [view1, view2];
nav._remove(1, 1);
expect(view1.state).toBe(STATE_INIT_ENTER);
expect(view2.state).toBe(STATE_INIT_LEAVE);
});
it('should set to remove the only view in the stack', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_ACTIVE;
nav._views = [view1];
nav._remove(0, 1);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
});
});
describe("first", () => {
it('should get the first item', () => {
let vc1 = new ViewController(),
vc2 = new ViewController(FirstPage),
vc3 = new ViewController(SecondPage);
nav._views = [vc1, vc2, vc3];
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.first()).toBe(vc1);
expect(nav.length()).toBe(3);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_ACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_TRANS_ENTER);
expect(nav.getByIndex(2).componentType).toBe(Page4);
});
it('should get the first item that isnt being destroyed', () => {
let vc1 = new ViewController(),
vc2 = new ViewController(),
vc3 = new ViewController();
vc1.shouldDestroy = true;
nav._views = [vc1, vc2, vc3];
expect(nav.first()).toBe(vc2);
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);
});
});
describe("popTo", () => {
it('should popTo 1st view', () => {
let vc1 = new ViewController(FirstPage),
vc2 = new ViewController(SecondPage),
vc3 = new ViewController(ThirdPage);
nav._views = [vc1, vc2, vc3];
nav._transition = mockTransitionFn;
nav.popTo(vc1);
expect(nav._views.length).toBe(1);
expect(nav._views[0].componentType).toBe(FirstPage);
});
});
describe("remove", () => {
it('should remove the view at the specified index', () => {
let vc1 = new ViewController(),
vc2 = new ViewController(FirstPage),
vc3 = new ViewController(SecondPage);
nav._views = [vc1, vc2, vc3];
expect(nav._views.length).toBe(3);
expect(nav._views[1].componentType).toBe(FirstPage);
nav.remove(1);
expect(nav._views.length).toBe(2);
expect(nav._views[1].componentType).toBe(SecondPage);
});
it('should pop if index is of active view', () => {
let vc1 = new ViewController(),
vc2 = new ViewController(FirstPage),
vc3 = new ViewController(SecondPage);
vc3.state = 1; //ACTIVE_STATE
nav._views = [vc1, vc2, vc3];
spyOn(nav, 'pop').and.callThrough();
nav.remove(1);
expect(nav.pop).not.toHaveBeenCalled();
nav.remove(1);
expect(nav.pop).toHaveBeenCalled();
});
});
describe("_setZIndex", () => {
describe('_setZIndex', () => {
it('should set zIndex 10 on first entering view', () => {
let enteringView = new ViewController();
enteringView.setPageRef({});
@@ -324,5 +291,360 @@ export function run() {
});
});
describe('_transComplete', () => {
it('should not entering/leaving state, after transition that isnt the most recent, and state already changed', () => {
let enteringView = new ViewController(Page1);
enteringView.state = 'somethingelse';
let leavingView = new ViewController(Page2);
leavingView.state = 'somethingelse';
nav._transIds = 2;
nav._transComplete(1, enteringView, leavingView);
expect(enteringView.state).toBe('somethingelse');
expect(leavingView.state).toBe('somethingelse');
});
it('should set entering/leaving to inactive, after transition that isnt the most recent', () => {
let enteringView = new ViewController(Page1);
enteringView.state = STATE_TRANS_ENTER;
let leavingView = new ViewController(Page2);
leavingView.state = STATE_TRANS_LEAVE;
nav._transIds = 2;
nav._transComplete(1, enteringView, leavingView);
expect(enteringView.state).toBe(STATE_INACTIVE);
expect(leavingView.state).toBe(STATE_INACTIVE);
});
it('should set entering active, leaving inactive, after transition', () => {
let enteringView = new ViewController(Page1);
enteringView.state = STATE_TRANS_ENTER;
let leavingView = new ViewController(Page2);
leavingView.state = STATE_TRANS_LEAVE;
nav._transIds = 1;
nav._transComplete(1, enteringView, leavingView);
expect(enteringView.state).toBe(STATE_ACTIVE);
expect(leavingView.state).toBe(STATE_INACTIVE);
});
});
describe('_insert', () => {
it('should push page when previous transition is still actively transitioning', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_TRANS_ENTER;
let view2 = new ViewController(Page2);
view2.state = STATE_TRANS_LEAVE;
nav._views = [view1, view2];
let view3 = new ViewController(Page3);
nav._insert(-1, [view3]);
expect(nav.getByIndex(0).state).toBe(STATE_TRANS_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_TRANS_LEAVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(2).componentType).toBe(Page3);
});
it('should push page when previous transition views init, but havent transitioned yet', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INIT_LEAVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INIT_ENTER;
nav._views = [view1, view2];
let view3 = new ViewController(Page3);
nav._insert(-1, [view3]);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(2).componentType).toBe(Page3);
});
it('should insert multiple pages, back to back, with a starting active page', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_ACTIVE;
nav._views = [view1];
let view2 = new ViewController(Page2);
nav._insert(-1, [view2]);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(1).componentType).toBe(Page2);
let view3 = new ViewController(Page3);
nav._insert(-1, [view3]);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(2).componentType).toBe(Page3);
});
it('should insert multiple pages, back to back, no starting active page', () => {
let view1 = new ViewController(Page1);
nav._insert(-1, [view1]);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page1);
let view2 = new ViewController(Page2);
nav._insert(-1, [view2]);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(1).componentType).toBe(Page2);
let view3 = new ViewController(Page3);
nav._insert(1, [view3]);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(1).componentType).toBe(Page3);
expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(2).componentType).toBe(Page2);
});
it('should push a page, and abort previous init', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INIT_LEAVE;
let view2 = new ViewController(Page2);
view2.state = STATE_INIT_ENTER;
nav._views = [view1, view2];
let view3 = new ViewController(Page3);
nav._insert(-1, [view3]);
expect(nav.length()).toBe(3);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(2).componentType).toBe(Page3);
});
it('should insert a page between the first and second', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_INACTIVE;
let view2 = new ViewController(Page2);
view2.state = STATE_ACTIVE;
nav._views = [view1, view2];
let view3 = new ViewController(Page3);
nav._insert(1, [view3]);
expect(nav.length()).toBe(3);
expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(1).componentType).toBe(Page3);
expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(2).componentType).toBe(Page2);
});
it('should insert a page before the first', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_ACTIVE;
nav._views = [view1];
let view2 = new ViewController(Page2);
nav._insert(0, [view2]);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(0).componentType).toBe(Page2);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(1).componentType).toBe(Page1);
});
it('should insert 3 pages', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_ACTIVE;
nav._views = [view1];
let insertViews = [
new ViewController(Page2),
new ViewController(Page3),
new ViewController(Page4)
];
nav._insert(-1, insertViews);
expect(nav.length()).toBe(4);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(1).componentType).toBe(Page2);
expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE);
expect(nav.getByIndex(2).componentType).toBe(Page3);
expect(nav.getByIndex(3).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(3).componentType).toBe(Page4);
});
it('should push the second page', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_ACTIVE;
nav._views = [view1];
let view2 = new ViewController(Page2)
nav._insert(-1, [view2]);
expect(nav.length()).toBe(2);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER);
expect(nav.getByIndex(1).componentType).toBe(Page2);
});
it('should push the first page, using a number greater than the length', () => {
let view1 = new ViewController(Page1)
nav._insert(8675309, [view1]);
expect(nav.length()).toBe(1);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
});
it('should push the first page, using -1', () => {
let view1 = new ViewController(Page1)
nav._insert(-1, [view1]);
expect(nav.getByIndex(0).id).toBeDefined();
expect(nav.length()).toBe(1);
expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER);
});
});
it('should getActive()', () => {
expect(nav.getActive()).toBe(null);
let view1 = new ViewController(Page1);
view1.state = STATE_INIT_ENTER;
nav._views = [view1];
expect(nav.getActive()).toBe(null);
view1.state = STATE_ACTIVE;
expect(nav.getActive()).toBe(view1);
});
it('should getByState()', () => {
expect(nav.getByState()).toBe(null);
let view1 = new ViewController(Page1);
view1.state = STATE_INIT_ENTER;
let view2 = new ViewController(Page2);
view2.state = STATE_INIT_ENTER;
nav._views = [view1, view2];
expect(nav.getByState('whatever')).toBe(null);
expect(nav.getByState(STATE_INIT_ENTER)).toBe(view2);
view2.state = STATE_INACTIVE;
expect(nav.getByState(STATE_INIT_ENTER)).toBe(view1);
view2.state = STATE_ACTIVE;
expect(nav.getActive()).toBe(view2);
});
it('should getPrevious()', () => {
expect(nav.getPrevious(null)).toBe(null);
let view1 = new ViewController(Page1);
let view2 = new ViewController(Page2);
nav._views = [view1, view2];
expect(nav.getPrevious(view1)).toBe(null);
expect(nav.getPrevious(view2)).toBe(view1);
});
it('should get first()', () => {
expect(nav.first()).toBe(null);
let view1 = new ViewController(Page1);
let view2 = new ViewController(Page2);
nav._views = [view1];
expect(nav.first()).toBe(view1);
nav._views = [view1, view2];
expect(nav.first()).toBe(view1);
});
it('should get last()', () => {
expect(nav.last()).toBe(null);
let view1 = new ViewController(Page1);
let view2 = new ViewController(Page2);
nav._views = [view1];
expect(nav.last()).toBe(view1);
nav._views = [view1, view2];
expect(nav.last()).toBe(view2);
});
it('should get indexOf()', () => {
let view1 = new ViewController(Page1);
let view2 = new ViewController(Page2);
expect(nav.length()).toBe(0);
expect(nav.indexOf(view1)).toBe(-1);
nav._views = [view1, view2];
expect(nav.indexOf(view1)).toBe(0);
expect(nav.indexOf(view2)).toBe(1);
expect(nav.length()).toBe(2);
});
it('should get getByIndex()', () => {
expect(nav.getByIndex(-99)).toBe(null);
expect(nav.getByIndex(99)).toBe(null);
let view1 = new ViewController(Page1);
let view2 = new ViewController(Page2);
nav._views = [view1, view2];
expect(nav.getByIndex(-1)).toBe(null);
expect(nav.getByIndex(0)).toBe(view1);
expect(nav.getByIndex(1)).toBe(view2);
expect(nav.getByIndex(2)).toBe(null);
});
// setup stuff
let nav;
let config = new Config();
class Page1 {}
class Page2 {}
class Page3 {}
class Page4 {}
class Page5 {}
beforeEach(() => {
nav = new NavController(null, null, config, null, null, null, null, null, null, null);
nav._renderer = {
setElementAttribute: function(){},
setElementClass: function(){},
setElementStyle: function(){}
};
});
});
}
const STATE_ACTIVE = 'active';
const STATE_INACTIVE = 'inactive';
const STATE_INIT_ENTER = 'init_enter';
const STATE_INIT_LEAVE = 'init_leave';
const STATE_TRANS_ENTER = 'trans_enter';
const STATE_TRANS_LEAVE = 'trans_leave';
const STATE_REMOVE = 'remove';
const STATE_REMOVE_AFTER_TRANS = 'remove_after_trans';
const STATE_FORCE_ACTIVE = 'force_active';

View File

@@ -1,4 +1,4 @@
import {Output, EventEmitter, Type, TemplateRef, ViewContainerRef, ElementRef} from 'angular2/core';
import {Output, EventEmitter, Type, TemplateRef, ViewContainerRef, ElementRef, Renderer} from 'angular2/core';
import {Navbar} from '../navbar/navbar';
import {NavController} from './nav-controller';
@@ -21,26 +21,26 @@ import {NavParams} from './nav-params';
* ```
*/
export class ViewController {
private _cntDir: any;
private _cntDir;
private _cntRef: ElementRef;
private _destroys: Array<Function> = [];
private _hdAttr = null;
private _leavingOpts = null;
private _loaded: boolean = false;
private _leavingOpts: any = null;
private _nbDir: Navbar;
private _nbTmpRef: TemplateRef;
private _nbVwRef: ViewContainerRef;
private _onDismiss: Function = null;
private _pgRef: ElementRef;
protected _nav: NavController;
id: string;
instance: any = {};
state: number = 0;
shouldDestroy: boolean = false;
shouldCache: boolean = false;
state: string = '';
viewType: string = '';
onReady: any;
zIndex: number;
@Output() private _emitter: EventEmitter<any> = new EventEmitter();
constructor(public componentType?: Type, public data: any = {}) {}
@@ -59,7 +59,7 @@ export class ViewController {
dismiss(data) {
this._onDismiss && this._onDismiss(data);
return this._nav.remove(this._nav.indexOf(this), this._leavingOpts);
return this._nav.remove(this._nav.indexOf(this), 1, this._leavingOpts);
}
setNav(navCtrl) {
@@ -89,7 +89,7 @@ export class ViewController {
let previousItem = this._nav.getPrevious(this);
// the previous view may exist, but if it's about to be destroyed
// it shouldn't be able to go back to
return !!(previousItem && !previousItem.shouldDestroy);
return !!(previousItem);
}
return false;
}
@@ -151,6 +151,36 @@ export class ViewController {
this._destroys = [];
}
/**
* @private
*/
domCache(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, 'hidden', this._hdAttr);
let navbarRef = this.navbarRef();
if (navbarRef) {
renderer.setElementAttribute(navbarRef, 'hidden', this._hdAttr);
}
}
}
setZIndex(zIndex: number, renderer: Renderer) {
if (this._pgRef && zIndex !== this.zIndex) {
this.zIndex = zIndex;
renderer.setElementStyle(this._pgRef, 'z-index', zIndex.toString());
}
}
/**
* @private
*/
@@ -358,9 +388,7 @@ export class ViewController {
*/
loaded() {
this._loaded = true;
if (!this.shouldDestroy) {
ctrlFn(this, 'onPageLoaded');
}
ctrlFn(this, 'onPageLoaded');
}
/**
@@ -368,9 +396,7 @@ export class ViewController {
* The view is about to enter and become the active view.
*/
willEnter() {
if (!this.shouldDestroy) {
ctrlFn(this, 'onPageWillEnter');
}
ctrlFn(this, 'onPageWillEnter');
}
/**

View File

@@ -153,7 +153,9 @@ export class Tab extends NavController {
*/
load(opts, done?: Function) {
if (!this._loaded && this.root) {
this.push(this.root, null, opts, done);
this.push(this.root, null, opts).then(() => {
done();
});
this._loaded = true;
} else {

View File

@@ -17,7 +17,7 @@ module.exports = function(config) {
'node_modules/rxjs/bundles/Rx.min.js',
'dist/bundles/ionic.system.js',
//'node_modules/angular2/bundles/test_lib.js',
{ pattern: 'dist/tests/**/*.spec.js', included: false },
{ pattern: 'dist/tests/**/nav-controller.spec.js', included: false },
'scripts/karma/test-main.js'
],