fix(navCtrl): add more states during transitions

This commit is contained in:
Adam Bradley
2016-01-15 10:21:18 -06:00
parent 7e98c102d5
commit ff10b3bb49
3 changed files with 106 additions and 101 deletions

View File

@ -218,12 +218,12 @@ export class NavController extends Ion {
* } * }
* } * }
* ``` * ```
* @param {Type} componentType The page component class you want to push on to the navigation stack * @param {Type} page The page component class you want to push on to the navigation stack
* @param {Object} [params={}] Any nav-params you want to pass along to the next view * @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 * @param {object} [opts={}] Any options you want to use pass to transtion
* @returns {Promise} Returns a promise, which resolves when the transition has completed * @returns {Promise} Returns a promise, which resolves when the transition has completed
*/ */
push(componentType: Type, push(page: Type,
params: any={}, params: any={},
opts: { opts: {
animate?: boolean, animate?: boolean,
@ -234,14 +234,14 @@ export class NavController extends Ion {
callback?: Function callback?: Function
) { ) {
if (!componentType) { if (!page) {
let errMsg = 'invalid componentType to push'; let errMsg = 'invalid page componentType to push';
console.error(errMsg); console.error(errMsg);
return Promise.reject(errMsg); return Promise.reject(errMsg);
} }
if (typeof componentType !== 'function') { if (typeof page !== 'function') {
throw 'Loading component must be a component class, not "' + componentType.toString() + '"'; throw 'Loading component must be a component class, not "' + page.toString() + '"';
} }
if (this.isTransitioning()) { if (this.isTransitioning()) {
@ -269,7 +269,7 @@ export class NavController extends Ion {
} }
// create a new ViewController // create a new ViewController
let enteringView = new ViewController(componentType, params); let enteringView = new ViewController(page, params);
enteringView.setNav(this); enteringView.setNav(this);
// default the direction to "forward" // default the direction to "forward"
@ -315,7 +315,7 @@ export class NavController extends Ion {
* ``` * ```
* *
* @param {ViewController} enteringView The name of the component you want to push on the navigation stack * @param {ViewController} enteringView The name of the component you want to push on the navigation stack
* @param {Object} [opts={}] Any options you want to use pass to transtion * @param {object} [opts={}] Any options you want to use pass to transtion
* @returns {Promise} Returns a promise, which resolves when the transition has completed * @returns {Promise} Returns a promise, which resolves when the transition has completed
*/ */
present(enteringView: ViewController, opts: any = {}): Promise<any> { present(enteringView: ViewController, opts: any = {}): Promise<any> {
@ -380,7 +380,7 @@ export class NavController extends Ion {
* } * }
* ``` * ```
* *
* @param {Object} [opts={}] Any options you want to use pass to transtion * @param {object} [opts={}] Any options you want to use pass to transtion
* @returns {Promise} Returns a promise when the transition is completed * @returns {Promise} Returns a promise when the transition is completed
*/ */
pop(opts: any = {}): Promise<any> { pop(opts: any = {}): Promise<any> {
@ -432,7 +432,7 @@ export class NavController extends Ion {
* @private * @private
* Pop to a specific view in the history stack * Pop to a specific view in the history stack
* @param view {ViewController} to pop to * @param view {ViewController} to pop to
* @param {Object} [opts={}] Any options you want to use pass to transtion * @param {object} [opts={}] Any options you want to use pass to transtion
*/ */
popTo(viewCtrl: ViewController, opts: any = {}): Promise<any> { popTo(viewCtrl: ViewController, opts: any = {}): Promise<any> {
// Get the target index of the view to pop to // Get the target index of the view to pop to
@ -484,7 +484,7 @@ export class NavController extends Ion {
/** /**
* Similar to `pop()`, this method let's you navigate back to the root of the stack, no matter how many views that is * Similar to `pop()`, this method let's you navigate back to the root of the stack, no matter how many views that is
* @param {Object} [opts={}] Any options you want to use pass to transtion * @param {object} [opts={}] Any options you want to use pass to transtion
*/ */
popToRoot(opts = {}): Promise<any> { popToRoot(opts = {}): Promise<any> {
return this.popTo(this.first(), opts); return this.popTo(this.first(), opts);
@ -508,23 +508,23 @@ export class NavController extends Ion {
* This will insert the `Info` view into the second slot of our navigation stack * This will insert the `Info` view into the second slot of our navigation stack
* *
* @param {number} index The index where you want to insert the view * @param {number} index The index where you want to insert the view
* @param {Type} componentType The name of the component you want to insert into the nav stack * @param {Type} page The name of the component you want to insert into the nav stack
* @returns {Promise} Returns a promise when the view has been inserted into the navigation stack * @returns {Promise} Returns a promise when the view has been inserted into the navigation stack
*/ */
insert(index: number, componentType: Type, params: any = {}, opts: any = {}): Promise<any> { insert(index: number, page: Type, params: any = {}, opts: any = {}): Promise<any> {
if (!componentType || index < 0) { if (!page || index < 0) {
return Promise.reject('invalid insert'); return Promise.reject('invalid insert');
} }
// push it onto the end // push it onto the end
if (index >= this._views.length) { if (index >= this._views.length) {
return this.push(componentType, params, opts); return this.push(page, params, opts);
} }
// create new ViewController, but don't render yet // create new ViewController, but don't render yet
let viewCtrl = new ViewController(componentType, params); let viewCtrl = new ViewController(page, params);
viewCtrl.setNav(this); viewCtrl.setNav(this);
viewCtrl.state = CACHED_STATE; viewCtrl.state = STATE_INACTIVE;
viewCtrl.shouldDestroy = false; viewCtrl.shouldDestroy = false;
viewCtrl.shouldCache = false; viewCtrl.shouldCache = false;
@ -553,7 +553,7 @@ export class NavController extends Ion {
* ``` * ```
* *
* @param {number} index Remove the view from the nav stack at that index * @param {number} index Remove the view from the nav stack at that index
* @param {Object} [opts={}] Any options you want to use pass to transtion * @param {object} [opts={}] Any options you want to use pass to transtion
* @returns {Promise} Returns a promise when the view has been removed * @returns {Promise} Returns a promise when the view has been removed
*/ */
remove(index: number, opts: any = {}): Promise<any> { remove(index: number, opts: any = {}): Promise<any> {
@ -597,7 +597,7 @@ export class NavController extends Ion {
* this.nav = nav; * this.nav = nav;
* } * }
* setPages() { * setPages() {
* this.nav.setPages([List,Detail, Info]); * this.nav.setPages([ {page: List}, {page: Detail}, {page:Info} ]);
* } * }
* } * }
*``` *```
@ -611,14 +611,13 @@ export class NavController extends Ion {
*```typescript *```typescript
* import {Page, NavController} from 'ionic/ionic' * import {Page, NavController} from 'ionic/ionic'
* import {Detail} from '../detail/detail' * import {Detail} from '../detail/detail'
* import {Info} from '../info/info'
* *
* export class Home { * export class Home {
* constructor(nav: NavController) { * constructor(nav: NavController) {
* this.nav = nav; * this.nav = nav;
* } * }
* setPages() { * setPages() {
* this.nav.setPages([List,Detail, Info],{ * this.nav.setPages([ {page: List}, {page: Detail} ], {
* animate: true * animate: true
* }); * });
* } * }
@ -640,28 +639,41 @@ export class NavController extends Ion {
* } * }
* setPages() { * setPages() {
* this.nav.setPages([{ * this.nav.setPages([{
* componentType: List, * page: List,
* params: {id: 43} * params: {id: 43}
* }, { * }, {
* componentType: Detail, * page: Detail,
* params: {id: 45} * params: {id: 45}
* },{ * },{
* componentType: Info, * page: Info,
* params: {id: 5} * params: {id: 5}
* }]); * }]);
* } * }
* } * }
*``` *```
* *
* @param {Array<Type>} componentTypes an arry of components to load in the stack * @param {Array<Type>} pages an arry of page components and their params to load in the stack
* @param {Object} [opts={}] Any options you want to use pass * @param {object} [opts={}] Any options you want to use pass
* @returns {Promise} Returns a promise when the pages are set * @returns {Promise} Returns a promise when the pages are set
*/ */
setPages(components: Array<{componentType: Type, params?: any}>, opts: any = {}): Promise<any> { setPages(pages: Array<{page: Type, params?: any}>, opts: any = {}): Promise<any> {
if (!components || !components.length) { if (!pages || !pages.length) {
return Promise.resolve(); return Promise.resolve();
} }
// deprecated warning
pages.forEach((pg) => {
if (pg['componentType']) {
pg.page = pg['componentType'];
console.warn('setPages() now uses "page" instead of "componentType" in the array of pages. ' +
'What was: setPages([{componentType: About}, {componentType: Contact}]) is now: setPages([{page: About}, {page: Contact}])');
} else if (!pg['page']) {
console.error('setPages() now requires an object containing "page" and optionally "params" in the array of pages. ' +
'What was: setPages([About, Contact]) is now: setPages([{page: About}, {page: Contact}])');
}
});
let leavingView = this.getActive() || new ViewController(); let leavingView = this.getActive() || new ViewController();
// if animate has not been set then default to false // if animate has not been set then default to false
@ -685,20 +697,16 @@ export class NavController extends Ion {
} }
} }
let componentType: Type = null;
let viewCtrl: ViewController = null; let viewCtrl: ViewController = null;
// create the ViewControllers that go before the new active ViewController // create the ViewControllers that go before the new active ViewController
// in the stack, but the previous views shouldn't render yet // in the stack, but the previous views shouldn't render yet
if (components.length > 1) { if (pages.length > 1) {
let newBeforeComponents: Array<{componentType: Type, params?: any}> = components.slice(0, components.length - 1); let newBeforePages: Array<{page: Type, params?: any}> = pages.slice(0, pages.length - 1);
for (let j = 0; j < newBeforeComponents.length; j++) { for (let j = 0; j < newBeforePages.length; j++) {
componentType = newBeforeComponents[j].componentType; viewCtrl = new ViewController(newBeforePages[j].page, newBeforePages[j].params);
if (componentType) {
viewCtrl = new ViewController(componentType, newBeforeComponents[j].params);
viewCtrl.setNav(this); viewCtrl.setNav(this);
viewCtrl.state = CACHED_STATE; viewCtrl.state = STATE_INACTIVE;
viewCtrl.shouldDestroy = false; viewCtrl.shouldDestroy = false;
viewCtrl.shouldCache = false; viewCtrl.shouldCache = false;
@ -706,23 +714,22 @@ export class NavController extends Ion {
this._add(viewCtrl); this._add(viewCtrl);
} }
} }
}
// transition the leaving and entering // transition the leaving and entering
return this.push(components[ components.length - 1 ].componentType, return this.push(pages[ pages.length - 1 ].page,
components[ components.length - 1 ].params, pages[ pages.length - 1 ].params,
opts); opts);
} }
/** /**
* Set the root for the current navigation stack * Set the root for the current navigation stack
* @param {Component} The name of the component you want to push on the navigation stack * @param {Type} 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} [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 * @param {object} [opts={}] Any options you want to use pass to transtion
* @returns {Promise} Returns a promise when done * @returns {Promise} Returns a promise when done
*/ */
setRoot(componentType: Type, params: any = {}, opts: any = {}): Promise<any> { setRoot(page: Type, params: any = {}, opts: any = {}): Promise<any> {
return this.setPages([ { componentType, params } ], opts); return this.setPages([ {page, params} ], opts);
} }
/** /**
@ -755,6 +762,9 @@ export class NavController extends Ion {
5. _transComplete: Cleanup, remove cache views, then call the final callback 5. _transComplete: Cleanup, remove cache views, then call the final callback
*/ */
enteringView.state = STATE_INIT_ENTER;
leavingView.state = STATE_INIT_LEAVE;
// begin the multiple async process of transitioning to the entering view // begin the multiple async process of transitioning to the entering view
this._render(enteringView, leavingView, opts, () => { this._render(enteringView, leavingView, opts, () => {
wtfEndTimeRange(wtfScope); wtfEndTimeRange(wtfScope);
@ -767,6 +777,8 @@ export class NavController extends Ion {
*/ */
_render(enteringView: ViewController, leavingView: ViewController, opts: any, done: Function) { _render(enteringView: ViewController, leavingView: ViewController, opts: any, done: Function) {
// compile/load the view into the DOM // compile/load the view into the DOM
enteringView.state = STATE_RENDER_ENTER;
leavingView.state = STATE_RENDER_LEAVE;
if (enteringView.shouldDestroy) { if (enteringView.shouldDestroy) {
// about to be destroyed, shouldn't continue // about to be destroyed, shouldn't continue
@ -804,6 +816,9 @@ export class NavController extends Ion {
_postRender(enteringView: ViewController, leavingView: ViewController, opts: any, done: Function) { _postRender(enteringView: ViewController, leavingView: ViewController, opts: any, done: Function) {
let wtfScope = wtfStartTimeRange('ionic.NavController#_postRender', enteringView.name); let wtfScope = wtfStartTimeRange('ionic.NavController#_postRender', enteringView.name);
enteringView.state = STATE_POST_RENDER_ENTER;
leavingView.state = STATE_POST_RENDER_LEAVE;
// called after _render has completed and the view is compiled/loaded // called after _render has completed and the view is compiled/loaded
if (enteringView.shouldDestroy) { if (enteringView.shouldDestroy) {
@ -864,8 +879,8 @@ export class NavController extends Ion {
// set that the new view pushed on the stack is staged to be entering/leaving // set that the new view pushed on the stack is staged to be entering/leaving
// staged state is important for the transition to find the correct view // staged state is important for the transition to find the correct view
enteringView.state = STAGED_ENTERING_STATE; enteringView.state = STATE_BEFORE_TRANS_ENTER;
leavingView.state = STAGED_LEAVING_STATE; leavingView.state = STATE_BEFORE_TRANS_LEAVE;
// init the transition animation // init the transition animation
opts.renderDelay = opts.transitionDelay || this._trnsDelay; opts.renderDelay = opts.transitionDelay || this._trnsDelay;
@ -917,8 +932,8 @@ export class NavController extends Ion {
// transition has completed, update each view's state // transition has completed, update each view's state
// place back into the zone, run didEnter/didLeave // place back into the zone, run didEnter/didLeave
// call the final callback when done // call the final callback when done
enteringView.state = ACTIVE_STATE; enteringView.state = STATE_AFTER_TRANS_ENTER;
leavingView.state = CACHED_STATE; leavingView.state = STATE_AFTER_TRANS_LEAVE;
// run inside of the zone again // run inside of the zone again
this._zone.run(() => { this._zone.run(() => {
@ -933,7 +948,11 @@ export class NavController extends Ion {
// no problem, let's just close for them // no problem, let's just close for them
this.keyboard.close(); this.keyboard.close();
this.keyboard.onClose(() => { this.keyboard.onClose(() => {
// keyboard has finished closing, transition complete // keyboard has finished closing, transition complete
enteringView.state = STATE_ACTIVE;
leavingView.state = STATE_INACTIVE;
this._transComplete(); this._transComplete();
wtfEndTimeRange(wtfScope); wtfEndTimeRange(wtfScope);
done(); done();
@ -941,6 +960,9 @@ export class NavController extends Ion {
} else { } else {
// all good, transition complete // all good, transition complete
enteringView.state = STATE_ACTIVE;
leavingView.state = STATE_INACTIVE;
this._transComplete(); this._transComplete();
wtfEndTimeRange(wtfScope); wtfEndTimeRange(wtfScope);
done(); done();
@ -959,7 +981,7 @@ export class NavController extends Ion {
if (view.shouldDestroy) { if (view.shouldDestroy) {
view.didUnload(); view.didUnload();
} else if (view.state === CACHED_STATE && view.shouldCache) { } else if (view.state === STATE_INACTIVE && view.shouldCache) {
view.shouldCache = false; view.shouldCache = false;
} }
} }
@ -1174,8 +1196,8 @@ export class NavController extends Ion {
this._zone.runOutsideAngular(() => { this._zone.runOutsideAngular(() => {
// set that the new view pushed on the stack is staged to be entering/leaving // set that the new view pushed on the stack is staged to be entering/leaving
// staged state is important for the transition to find the correct view // staged state is important for the transition to find the correct view
enteringView.state = STAGED_ENTERING_STATE; enteringView.state = STATE_RENDER_ENTER;
leavingView.state = STAGED_LEAVING_STATE; leavingView.state = STATE_RENDER_LEAVE;
// init the swipe back transition animation // init the swipe back transition animation
this._sbTrans = Animation.createTransition(enteringView, leavingView, opts); this._sbTrans = Animation.createTransition(enteringView, leavingView, opts);
@ -1216,8 +1238,8 @@ export class NavController extends Ion {
this._zone.run(() => { this._zone.run(() => {
// find the views that were entering and leaving // find the views that were entering and leaving
let enteringView = this._getStagedEntering(); let enteringView = null;// this._getStagedEntering();
let leavingView = this._getStagedLeaving(); let leavingView = null;//this._getStagedLeaving();
if (enteringView && leavingView) { if (enteringView && leavingView) {
// finish up the animation // finish up the animation
@ -1225,8 +1247,8 @@ export class NavController extends Ion {
if (completeSwipeBack) { if (completeSwipeBack) {
// swipe back has completed navigating back // swipe back has completed navigating back
// update each view's state // update each view's state
enteringView.state = ACTIVE_STATE; enteringView.state = STATE_ACTIVE;
leavingView.state = CACHED_STATE; leavingView.state = STATE_INACTIVE;
enteringView.didEnter(); enteringView.didEnter();
leavingView.didLeave(); leavingView.didLeave();
@ -1239,8 +1261,8 @@ export class NavController extends Ion {
} else { } else {
// cancelled the swipe back, they didn't end up going back // cancelled the swipe back, they didn't end up going back
// return views to their original state // return views to their original state
leavingView.state = ACTIVE_STATE; leavingView.state = STATE_ACTIVE;
enteringView.state = CACHED_STATE; enteringView.state = STATE_INACTIVE;
leavingView.willEnter(); leavingView.willEnter();
leavingView.didEnter(); leavingView.didEnter();
@ -1295,10 +1317,10 @@ export class NavController extends Ion {
/** /**
* Check to see if swipe-to-go-back is enabled * Check to see if swipe-to-go-back is enabled
* @param {boolean=} isSwipeBackEnabled Set whether or not swipe-to-go-back is enabled * @param {boolean} isSwipeBackEnabled Set whether or not swipe-to-go-back is enabled
* @returns {boolean} Whether swipe-to-go-back is enabled * @returns {boolean} Whether swipe-to-go-back is enabled
*/ */
isSwipeBackEnabled(val) { isSwipeBackEnabled(val?: boolean): boolean {
if (arguments.length) { if (arguments.length) {
this._sbEnabled = !!val; this._sbEnabled = !!val;
} }
@ -1351,37 +1373,13 @@ export class NavController extends Ion {
array.remove(this._views, viewOrIndex); array.remove(this._views, viewOrIndex);
} }
/**
* @private
*/
_getStagedEntering(): ViewController {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === STAGED_ENTERING_STATE) {
return this._views[i];
}
}
return null;
}
/**
* @private
*/
_getStagedLeaving(): ViewController {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === STAGED_LEAVING_STATE) {
return this._views[i];
}
}
return null;
}
/** /**
* @private * @private
* @returns {Component} TODO * @returns {Component} TODO
*/ */
getActive(): ViewController { getActive(): ViewController {
for (let i = this._views.length - 1; i >= 0; i--) { for (let i = this._views.length - 1; i >= 0; i--) {
if (this._views[i].state === ACTIVE_STATE && !this._views[i].shouldDestroy) { if (this._views[i].state === STATE_ACTIVE && !this._views[i].shouldDestroy) {
return this._views[i]; return this._views[i];
} }
} }
@ -1473,7 +1471,7 @@ export class NavController extends Ion {
* @returns {boolean} * @returns {boolean}
*/ */
isActive(viewCtrl: ViewController): boolean { isActive(viewCtrl: ViewController): boolean {
return !!(viewCtrl && viewCtrl.state === ACTIVE_STATE); return !!(viewCtrl && viewCtrl.state === STATE_ACTIVE);
} }
/** /**
@ -1498,10 +1496,19 @@ export class NavController extends Ion {
} }
const ACTIVE_STATE = 1; const STATE_ACTIVE = 1;
const CACHED_STATE = 2; const STATE_INACTIVE = 2;
const STAGED_ENTERING_STATE = 3; const STATE_INIT_ENTER = 3;
const STAGED_LEAVING_STATE = 4; const STATE_INIT_LEAVE = 4;
const STATE_RENDER_ENTER = 5;
const STATE_RENDER_LEAVE = 6;
const STATE_POST_RENDER_ENTER = 7;
const STATE_POST_RENDER_LEAVE = 8;
const STATE_BEFORE_TRANS_ENTER = 9;
const STATE_BEFORE_TRANS_LEAVE = 10;
const STATE_AFTER_TRANS_ENTER = 11;
const STATE_AFTER_TRANS_LEAVE = 12;
let ctrlIds = -1; let ctrlIds = -1;

View File

@ -39,9 +39,8 @@ export function run() {
return true; return true;
} }
// beforeEach(inject([Compiler], compiler => {
beforeEach(() => { beforeEach(() => {
nav = new NavController(null, null, new Config(), null, null, null, null, null); nav = new NavController(null, null, new Config(), null, null, null, null, null, null, null);
nav._renderer = { nav._renderer = {
setElementAttribute: function(){}, setElementAttribute: function(){},
setElementStyle: function(){} setElementStyle: function(){}
@ -87,7 +86,7 @@ export function run() {
}); });
describe("push", () => { describe("push", () => {
it('should return a rejected Promise if componentType is falsy', done => { it('should return a rejected Promise if page is falsy', done => {
let s = jasmine.createSpy('success'); let s = jasmine.createSpy('success');
let f = jasmine.createSpy('fail'); let f = jasmine.createSpy('fail');
@ -100,7 +99,7 @@ export function run() {
}); });
}); });
it('should throw an error if componentType truthy, but is not a function', () => { it('should throw an error if page truthy, but is not a function', () => {
let push = () => nav.push({}, {}, {}); let push = () => nav.push({}, {}, {});
expect(push).toThrow(); expect(push).toThrow();
@ -148,7 +147,7 @@ export function run() {
vc2 = new ViewController(), vc2 = new ViewController(),
vc3 = new ViewController(); vc3 = new ViewController();
nav._views = [vc1, vc2, vc3]; nav._views = [vc1, vc2, vc3];
let arr = [FirstPage, SecondPage, ThirdPage]; let arr = [{page: FirstPage}, {page:SecondPage}, {page:ThirdPage}];
nav._transition = mockTransitionFn; nav._transition = mockTransitionFn;
nav.setPages(arr); nav.setPages(arr);
@ -198,7 +197,6 @@ export function run() {
nav.insert(10, FirstPage); nav.insert(10, FirstPage);
expect(nav._views[5].componentType).toBe(FirstPage); expect(nav._views[5].componentType).toBe(FirstPage);
expect(nav.push.calls.count()).toBe(2);
}); });
}); });

View File

@ -98,7 +98,7 @@ export class Config {
private _s: any = {}; private _s: any = {};
public platform: Platform; public platform: Platform;
constructor(config) { constructor(config?) {
this._s = config && isObject(config) && !isArray(config) ? config : {}; this._s = config && isObject(config) && !isArray(config) ? config : {};
} }