refactor(NavController): adds better error handling

fixes #10090
This commit is contained in:
Manu Mtz.-Almeida
2017-02-25 22:29:24 +01:00
parent beed9989ed
commit 5a4c6093a7
10 changed files with 85 additions and 45 deletions

View File

@ -202,8 +202,8 @@ export class NavControllerBase extends Ion implements NavController {
});
}
// ti.resolve() is called when the navigation transition is finished successfully
ti.resolve = (hasCompleted: boolean, isAsync: boolean, enteringName: string, leavingName: string, direction: string) => {
// transition has successfully resolved
this._trnsId = null;
this._init = true;
resolve && resolve(hasCompleted, isAsync, enteringName, leavingName, direction);
@ -214,23 +214,22 @@ export class NavControllerBase extends Ion implements NavController {
this._nextTrns();
};
ti.reject = (rejectReason: any, trns: Transition) => {
// rut row raggy, something rejected this transition
// ti.reject() is called when the navigation transition fails. ie. it is rejected at some point.
ti.reject = (rejectReason: any, transition: 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
this._destroyView(trns.enteringView);
// walk through the transition views so they are destroyed
while (transition) {
var enteringView = transition.enteringView;
if (enteringView && (enteringView._state === ViewState.ATTACHED)) {
this._destroyView(enteringView);
}
if (!trns.parent) {
if (transition.isRoot()) {
this._trnsCtrl.destroy(transition.trnsId);
break;
}
}
if (trns) {
this._trnsCtrl.destroy(trns.trnsId);
transition = transition.parent;
}
reject && reject(false, false, rejectReason);
@ -278,7 +277,19 @@ export class NavControllerBase extends Ion implements NavController {
return false;
}
// Get entering and leaving views
// ensure any of the inserted view are used
const insertViews = ti.insertViews;
if (insertViews) {
for (var i = 0; i < insertViews.length; i++) {
var nav = insertViews[i]._nav;
if (nav && nav !== this || insertViews[i]._state === ViewState.DESTROYED) {
ti.reject('leavingView and enteringView are null. stack is already empty');
return false;
}
}
}
// get entering and leaving views
const leavingView = this.getActive();
const enteringView = this._getEnteringView(ti, leavingView);
@ -291,7 +302,7 @@ export class NavControllerBase extends Ion implements NavController {
this.setTransitioning(true);
// Initialize enteringView
if (enteringView && isBlank(enteringView._state)) {
if (enteringView && enteringView._state === ViewState.NEW) {
// render the entering view, and all child navs and views
// ******** DOM WRITE ****************
this._viewInit(enteringView);
@ -303,7 +314,6 @@ export class NavControllerBase extends Ion implements NavController {
// views have been initialized, now let's test
// to see if the transition is even allowed or not
return this._viewTest(enteringView, leavingView, ti);
} else {
return this._postViewInit(enteringView, leavingView, ti);
}
@ -419,7 +429,6 @@ export class NavControllerBase extends Ion implements NavController {
// add the views to the
for (i = 0; i < insertViews.length; i++) {
view = insertViews[i];
assert(view, 'view must be non null');
this._insertViewAt(view, ti.insertStart + i);
}
@ -477,6 +486,9 @@ export class NavControllerBase extends Ion implements NavController {
* DOM WRITE
*/
_viewInit(enteringView: ViewController) {
assert(enteringView, 'enteringView must be non null');
assert(enteringView._state === ViewState.NEW, 'enteringView state must be NEW');
// entering view has not been initialized yet
const componentProviders = ReflectiveInjector.resolve([
{ provide: NavController, useValue: this },
@ -501,7 +513,7 @@ export class NavControllerBase extends Ion implements NavController {
// render the component ref instance to the DOM
// ******** DOM WRITE ****************
viewport.insert(componentRef.hostView, viewport.length);
view._state = ViewState.PRE_RENDERED;
view._state = ViewState.ATTACHED;
if (view._cssClass) {
// the ElementRef of the actual ion-page created
@ -604,14 +616,12 @@ export class NavControllerBase extends Ion implements NavController {
}
});
if (enteringView && enteringView._state === ViewState.INITIALIZED) {
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._viewAttachToDOM(enteringView, enteringView._cmp, this._viewport);
} else {
console.debug('enteringView state is not INITIALIZED', enteringView);
}
if (!transition.hasChildren) {
@ -762,9 +772,10 @@ export class NavControllerBase extends Ion implements NavController {
if (existingIndex > -1) {
// this view is already in the stack!!
// move it to its new location
assert(view._nav === this, 'view is not part of the nav');
this._views.splice(index, 0, this._views.splice(existingIndex, 1)[0]);
} else {
assert(!view._nav, 'nav is used');
// this is a new view to add to the stack
// create the new entering view
view._setNav(this);
@ -781,6 +792,8 @@ export class NavControllerBase extends Ion implements NavController {
}
_removeView(view: ViewController) {
assert(view._state === ViewState.ATTACHED || view._state === ViewState.DESTROYED, 'view state should be loaded or destroyed');
const views = this._views;
const index = views.indexOf(view);
assert(index > -1, 'view must be part of the stack');
@ -1056,7 +1069,7 @@ export class NavControllerBase extends Ion implements NavController {
dismissPageChangeViews() {
for (let view of this._views) {
if (view.data && view.data.dismissOnPageChange) {
view.dismiss();
view.dismiss().catch(null);
}
}
}

View File

@ -193,9 +193,10 @@ export interface TransitionInstruction {
}
export enum ViewState {
INITIALIZED,
PRE_RENDERED,
LOADED,
NEW, // New created ViewController
INITIALIZED, // Initialized by the NavController
ATTACHED, // Loaded to the DOM
DESTROYED // Destroyed by the NavController
}
export const INIT_ZINDEX = 100;

View File

@ -1,5 +1,5 @@
import { mockNavController, mockView, mockViews } from '../../util/mock-providers';
import { ViewState } from '../nav-util';
describe('ViewController', () => {
@ -16,6 +16,7 @@ describe('ViewController', () => {
});
// act
viewController._state = ViewState.ATTACHED;
viewController._willEnter();
}, 10000);
});
@ -33,6 +34,7 @@ describe('ViewController', () => {
});
// act
viewController._state = ViewState.ATTACHED;
viewController._didEnter();
}, 10000);
});
@ -50,6 +52,7 @@ describe('ViewController', () => {
});
// act
viewController._state = ViewState.ATTACHED;
viewController._willLeave(false);
}, 10000);
});

View File

@ -1,7 +1,7 @@
import { ComponentRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core';
import { Footer, Header } from '../components/toolbar/toolbar';
import { isPresent } from '../util/util';
import { isPresent, assert } from '../util/util';
import { Navbar } from '../components/navbar/navbar';
import { NavController } from './nav-controller';
import { NavOptions, ViewState } from './nav-util';
@ -46,7 +46,7 @@ export class ViewController {
_cmp: ComponentRef<any>;
_nav: NavController;
_zIndex: number;
_state: ViewState;
_state: ViewState = ViewState.NEW;
_cssClass: string;
/**
@ -227,7 +227,7 @@ export class ViewController {
* @private
*/
get name(): string {
return this.component ? this.component.name : '';
return (this.component ? this.component.name : '');
}
/**
@ -261,14 +261,12 @@ export class ViewController {
// _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._isHidden) {
this._isHidden = !shouldShow;
let value = (shouldShow ? null : '');
// ******** DOM WRITE ****************
renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', value);
}
// if it should render, then the hidden attribute should not be on the element
if (this._cmp && shouldShow === this._isHidden) {
this._isHidden = !shouldShow;
let value = (shouldShow ? null : '');
// ******** DOM WRITE ****************
renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', value);
}
}
@ -411,6 +409,7 @@ export class ViewController {
}
_preLoad() {
assert(this._state === ViewState.INITIALIZED, 'view state must be INITIALIZED');
this._lifecycle('PreLoad');
}
@ -420,6 +419,7 @@ export class ViewController {
* This event is fired before the component and his children have been initialized.
*/
_willLoad() {
assert(this._state === ViewState.INITIALIZED, 'view state must be INITIALIZED');
this._lifecycle('WillLoad');
}
@ -432,6 +432,7 @@ export class ViewController {
* recommended method to use when a view becomes active.
*/
_didLoad() {
assert(this._state === ViewState.ATTACHED, 'view state must be ATTACHED');
this._lifecycle('DidLoad');
}
@ -440,6 +441,8 @@ export class ViewController {
* The view is about to enter and become the active view.
*/
_willEnter() {
assert(this._state === ViewState.ATTACHED, 'view state must be ATTACHED');
if (this._detached && this._cmp) {
// ensure this has been re-attached to the change detector
this._cmp.changeDetectorRef.reattach();
@ -456,6 +459,8 @@ export class ViewController {
* will fire, whether it was the first load or loaded from the cache.
*/
_didEnter() {
assert(this._state === ViewState.ATTACHED, 'view state must be ATTACHED');
this._nb && this._nb.didEnter();
this.didEnter.emit(null);
this._lifecycle('DidEnter');
@ -510,6 +515,8 @@ export class ViewController {
* DOM WRITE
*/
_destroy(renderer: Renderer) {
assert(this._state !== ViewState.DESTROYED, 'view state must be ATTACHED');
if (this._cmp) {
if (renderer) {
// ensure the element is cleaned up for when the view pool reuses this element
@ -524,6 +531,7 @@ export class ViewController {
}
this._nav = this._cmp = this.instance = this._cntDir = this._cntRef = this._leavingOpts = this._hdrDir = this._ftrDir = this._nb = this._onDidDismiss = this._onWillDismiss = null;
this._state = ViewState.DESTROYED;
}
/**
@ -534,7 +542,7 @@ export class ViewController {
const methodName = 'ionViewCan' + lifecycle;
if (instance && instance[methodName]) {
try {
let result = instance[methodName]();
var result = instance[methodName]();
if (result === false) {
return false;
} else if (result instanceof Promise) {