refactor(nav): simplify ViewController

This commit is contained in:
Manu Mtz.-Almeida
2018-04-01 18:04:36 +02:00
parent ff06dab3c0
commit 853e55388b
6 changed files with 199 additions and 306 deletions

View File

@ -3517,23 +3517,21 @@ declare global {
interface HTMLIonNavElement extends HTMLStencilElement { interface HTMLIonNavElement extends HTMLStencilElement {
'animated': boolean; 'animated': boolean;
'canGoBack': (view?: ViewController) => boolean; 'canGoBack': (view?: ViewController) => boolean;
'delegate': FrameworkDelegate; 'delegate': FrameworkDelegate|undefined;
'getActive': () => ViewController; 'getActive': () => ViewController;
'getByIndex': (index: number) => ViewController; 'getByIndex': (index: number) => ViewController;
'getPrevious': (view?: ViewController) => ViewController; 'getPrevious': (view?: ViewController) => ViewController;
'getRouteId': () => RouteID; 'getRouteId': () => RouteID;
'getViews': () => ViewController[];
'insert': (insertIndex: number, component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'insert': (insertIndex: number, component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'insertPages': (insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'insertPages': (insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'length': () => number;
'pop': (opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'pop': (opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'popAll': () => Promise<boolean[]>;
'popTo': (indexOrViewCtrl: number | ViewController, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'popTo': (indexOrViewCtrl: number | ViewController, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'popToRoot': (opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'popToRoot': (opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'push': (component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'push': (component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'removeIndex': (startIndex: number, removeCount?: number, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'removeIndex': (startIndex: number, removeCount?: number, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'removeView': (viewController: ViewController, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'root': NavComponent|undefined;
'root': NavComponent; 'rootParams': ComponentProps|undefined;
'rootParams': ComponentProps;
'setPages': (views: any[], opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'setPages': (views: any[], opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'setRoot': (component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>; 'setRoot': (component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn) => Promise<boolean>;
'setRouteId': (id: string, params: any, direction: number) => Promise<RouteWrite>; 'setRouteId': (id: string, params: any, direction: number) => Promise<RouteWrite>;
@ -3557,10 +3555,10 @@ declare global {
namespace JSXElements { namespace JSXElements {
export interface IonNavAttributes extends HTMLAttributes { export interface IonNavAttributes extends HTMLAttributes {
'animated'?: boolean; 'animated'?: boolean;
'delegate'?: FrameworkDelegate; 'delegate'?: FrameworkDelegate|undefined;
'onIonNavChanged'?: (event: CustomEvent<void>) => void; 'onIonNavChanged'?: (event: CustomEvent<void>) => void;
'root'?: NavComponent; 'root'?: NavComponent|undefined;
'rootParams'?: ComponentProps; 'rootParams'?: ComponentProps|undefined;
'swipeBackEnabled'?: boolean; 'swipeBackEnabled'?: boolean;
} }
} }
@ -4714,7 +4712,7 @@ declare global {
'commit': (enteringEl: HTMLElement, leavingEl: HTMLElement, opts?: RouterOutletOptions) => Promise<boolean>; 'commit': (enteringEl: HTMLElement, leavingEl: HTMLElement, opts?: RouterOutletOptions) => Promise<boolean>;
'delegate': FrameworkDelegate; 'delegate': FrameworkDelegate;
'getRouteId': () => RouteID; 'getRouteId': () => RouteID;
'setRoot': (component: string | HTMLElement, params?: { [key: string]: any; }, opts?: RouterOutletOptions) => Promise<boolean>; 'setRoot': (component: ComponentRef, params?: ComponentProps, opts?: RouterOutletOptions) => Promise<boolean>;
'setRouteId': (id: string, params: any, direction: number) => Promise<RouteWrite>; 'setRouteId': (id: string, params: any, direction: number) => Promise<RouteWrite>;
} }
var HTMLIonRouterOutletElement: { var HTMLIonRouterOutletElement: {

View File

@ -1,11 +1,11 @@
import { ViewController, isViewController } from './view-controller'; import { ViewController } from './view-controller';
import { Animation, ComponentRef, FrameworkDelegate } from '../..'; import { Animation, ComponentRef, FrameworkDelegate } from '../..';
export function convertToView(page: any, params: any): ViewController|null { export function convertToView(page: any, params: any): ViewController|null {
if (!page) { if (!page) {
return null; return null;
} }
if (isViewController(page)) { if (page instanceof ViewController) {
return page; return page;
} }
return new ViewController(page, params); return new ViewController(page, params);
@ -13,7 +13,7 @@ export function convertToView(page: any, params: any): ViewController|null {
export function convertToViews(pages: any[]): ViewController[] { export function convertToViews(pages: any[]): ViewController[] {
return pages.map(page => { return pages.map(page => {
if (isViewController(page)) { if (page instanceof ViewController) {
return page; return page;
} }
if ('page' in page) { if ('page' in page) {
@ -23,10 +23,6 @@ export function convertToViews(pages: any[]): ViewController[] {
}).filter(v => v !== null) as ViewController[]; }).filter(v => v !== null) as ViewController[];
} }
export function isPresent(val: any): val is any {
return val !== undefined && val !== null;
}
export const enum ViewState { export const enum ViewState {
New = 1, New = 1,
Attached, Attached,
@ -38,7 +34,7 @@ export const enum NavDirection {
Forward = 'forward' Forward = 'forward'
} }
export type NavComponent = ComponentRef | ViewController | Function; export type NavComponent = ComponentRef | ViewController;
export interface NavResult { export interface NavResult {
hasCompleted: boolean; hasCompleted: boolean;

View File

@ -8,10 +8,9 @@ import {
TransitionInstruction, TransitionInstruction,
ViewState, ViewState,
convertToViews, convertToViews,
isPresent,
} from './nav-util'; } from './nav-util';
import { ViewController, isViewController } from './view-controller'; import { ViewController, matches } from './view-controller';
import { Animation, ComponentProps, Config, DomController, FrameworkDelegate, GestureDetail, NavOutlet } from '../..'; import { Animation, ComponentProps, Config, DomController, FrameworkDelegate, GestureDetail, NavOutlet } from '../..';
import { RouteID, RouteWrite, RouterDirection } from '../router/utils/interfaces'; import { RouteID, RouteWrite, RouterDirection } from '../router/utils/interfaces';
import { AnimationOptions, ViewLifecycle, lifecycle, transition } from '../../utils/transition'; import { AnimationOptions, ViewLifecycle, lifecycle, transition } from '../../utils/transition';
@ -25,13 +24,13 @@ import mdTransitionAnimation from './animations/md.transition';
}) })
export class Nav implements NavOutlet { export class Nav implements NavOutlet {
private _init = false; private init = false;
private _queue: TransitionInstruction[] = []; private queue: TransitionInstruction[] = [];
private _sbTrns: Animation; private sbTrns: Animation|undefined;
private useRouter = false; private useRouter = false;
isTransitioning = false; private isTransitioning = false;
private _destroyed = false; private destroyed = false;
private _views: ViewController[] = []; private views: ViewController[] = [];
mode: string; mode: string;
@ -43,9 +42,9 @@ export class Nav implements NavOutlet {
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: HTMLIonAnimationControllerElement; @Prop({ connect: 'ion-animation-controller' }) animationCtrl: HTMLIonAnimationControllerElement;
@Prop({ mutable: true }) swipeBackEnabled: boolean; @Prop({ mutable: true }) swipeBackEnabled: boolean;
@Prop({ mutable: true }) animated: boolean; @Prop({ mutable: true }) animated: boolean;
@Prop() delegate: FrameworkDelegate; @Prop() delegate: FrameworkDelegate|undefined;
@Prop() rootParams: ComponentProps; @Prop() rootParams: ComponentProps|undefined;
@Prop() root: NavComponent; @Prop() root: NavComponent|undefined;
@Watch('root') @Watch('root')
rootChanged() { rootChanged() {
if (this.root) { if (this.root) {
@ -74,24 +73,20 @@ export class Nav implements NavOutlet {
} }
componentDidUnload() { componentDidUnload() {
const views = this._views; for (const view of this.views) {
let view: ViewController;
for (let i = 0; i < views.length; i++) {
view = views[i];
lifecycle(view.element, ViewLifecycle.WillUnload); lifecycle(view.element, ViewLifecycle.WillUnload);
view._destroy(); view._destroy();
} }
// release swipe back gesture and transition // release swipe back gesture and transition
this._sbTrns && this._sbTrns.destroy(); this.sbTrns && this.sbTrns.destroy();
this._queue = this._views = this._sbTrns = null; this.queue = this.views = this.sbTrns = null;
this.destroyed = true;
this._destroyed = true;
} }
@Method() @Method()
push(component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { push(component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({ return this.queueTrns({
insertStart: -1, insertStart: -1,
insertViews: [{ page: component, params: componentProps }], insertViews: [{ page: component, params: componentProps }],
opts: opts, opts: opts,
@ -100,7 +95,7 @@ export class Nav implements NavOutlet {
@Method() @Method()
insert(insertIndex: number, component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { insert(insertIndex: number, component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({ return this.queueTrns({
insertStart: insertIndex, insertStart: insertIndex,
insertViews: [{ page: component, params: componentProps }], insertViews: [{ page: component, params: componentProps }],
opts: opts, opts: opts,
@ -109,7 +104,7 @@ export class Nav implements NavOutlet {
@Method() @Method()
insertPages(insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { insertPages(insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({ return this.queueTrns({
insertStart: insertIndex, insertStart: insertIndex,
insertViews: insertComponents, insertViews: insertComponents,
opts: opts, opts: opts,
@ -118,7 +113,7 @@ export class Nav implements NavOutlet {
@Method() @Method()
pop(opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { pop(opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({ return this.queueTrns({
removeStart: -1, removeStart: -1,
removeCount: 1, removeCount: 1,
opts: opts, opts: opts,
@ -132,52 +127,33 @@ export class Nav implements NavOutlet {
removeCount: -1, removeCount: -1,
opts: opts opts: opts
}; };
if (isViewController(indexOrViewCtrl)) { if (indexOrViewCtrl instanceof ViewController) {
config.removeView = indexOrViewCtrl; config.removeView = indexOrViewCtrl;
config.removeStart = 1; config.removeStart = 1;
} else if (typeof indexOrViewCtrl === 'number') { } else if (typeof indexOrViewCtrl === 'number') {
config.removeStart = indexOrViewCtrl + 1; config.removeStart = indexOrViewCtrl + 1;
} }
return this._queueTrns(config, done); return this.queueTrns(config, done);
} }
@Method() @Method()
popToRoot(opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { popToRoot(opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({ return this.queueTrns({
removeStart: 1, removeStart: 1,
removeCount: -1, removeCount: -1,
opts: opts, opts: opts,
}, done); }, done);
} }
@Method()
popAll(): Promise<boolean[]> {
const promises: Promise<boolean>[] = [];
for (let i = this._views.length - 1; i >= 0; i--) {
promises.push(this.pop(undefined));
}
return Promise.all(promises);
}
@Method() @Method()
removeIndex(startIndex: number, removeCount = 1, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { removeIndex(startIndex: number, removeCount = 1, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({ return this.queueTrns({
removeStart: startIndex, removeStart: startIndex,
removeCount: removeCount, removeCount: removeCount,
opts: opts, opts: opts,
}, done); }, done);
} }
@Method()
removeView(viewController: ViewController, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this._queueTrns({
removeView: viewController,
removeStart: 0,
removeCount: 1,
opts: opts,
}, done);
}
@Method() @Method()
setRoot(component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> { setRoot(component: NavComponent, componentProps?: ComponentProps, opts?: NavOptions, done?: TransitionDoneFn): Promise<boolean> {
return this.setPages([{ page: component, params: componentProps }], opts, done); return this.setPages([{ page: component, params: componentProps }], opts, done);
@ -192,7 +168,7 @@ export class Nav implements NavOutlet {
if (opts.animate !== true) { if (opts.animate !== true) {
opts.animate = false; opts.animate = false;
} }
return this._queueTrns({ return this.queueTrns({
insertStart: 0, insertStart: 0,
insertViews: views, insertViews: views,
removeStart: 0, removeStart: 0,
@ -204,11 +180,11 @@ export class Nav implements NavOutlet {
@Method() @Method()
setRouteId(id: string, params: any, direction: number): Promise<RouteWrite> { setRouteId(id: string, params: any, direction: number): Promise<RouteWrite> {
const active = this.getActive(); const active = this.getActive();
if (active && active.matches(id, params)) { if (matches(active, id, params)) {
return Promise.resolve({changed: false, element: active.element}); return Promise.resolve({changed: false, element: active.element});
} }
const viewController = this._views.find(v => v.matches(id, params)); const viewController = this.views.find(v => matches(v, id, params));
let resolve: (result: RouteWrite) => void; let resolve: (result: RouteWrite) => void;
const promise = new Promise<RouteWrite>((r) => resolve = r); const promise = new Promise<RouteWrite>((r) => resolve = r);
@ -246,7 +222,7 @@ export class Nav implements NavOutlet {
const active = this.getActive(); const active = this.getActive();
return active ? { return active ? {
id: active.element.tagName, id: active.element.tagName,
params: active.data, params: active.params,
element: active.element element: active.element
} : undefined; } : undefined;
} }
@ -258,32 +234,24 @@ export class Nav implements NavOutlet {
@Method() @Method()
getActive(): ViewController|undefined { getActive(): ViewController|undefined {
return this._views[this._views.length - 1]; return this.views[this.views.length - 1];
} }
@Method() @Method()
getByIndex(index: number): ViewController|undefined { getByIndex(index: number): ViewController|undefined {
return this._views[index]; return this.views[index];
} }
@Method() @Method()
getPrevious(view = this.getActive()): ViewController|undefined { getPrevious(view = this.getActive()): ViewController|undefined {
const views = this._views; const views = this.views;
const index = views.indexOf(view); const index = views.indexOf(view);
return (index > 0) ? views[index - 1] : undefined; return (index > 0) ? views[index - 1] : undefined;
} }
@Method() @Method()
getViews(): ViewController[] {
return this._views.slice();
}
indexOf(viewController: ViewController) {
return this._views.indexOf(viewController);
}
length() { length() {
return this._views.length; return this.views.length;
} }
// _queueTrns() adds a navigation stack change to the queue and schedules it to run: // _queueTrns() adds a navigation stack change to the queue and schedules it to run:
@ -296,7 +264,7 @@ export class Nav implements NavOutlet {
// 7. _transitionStart(): called once the transition actually starts, it initializes the Animation underneath. // 7. _transitionStart(): called once the transition actually starts, it initializes the Animation underneath.
// 8. _transitionFinish(): called once the transition finishes // 8. _transitionFinish(): called once the transition finishes
// 9. _cleanup(): syncs the navigation internal state with the DOM. For example it removes the pages from the DOM or hides/show them. // 9. _cleanup(): syncs the navigation internal state with the DOM. For example it removes the pages from the DOM or hides/show them.
private _queueTrns(ti: TransitionInstruction, done: TransitionDoneFn|undefined): Promise<boolean> { private queueTrns(ti: TransitionInstruction, done: TransitionDoneFn|undefined): Promise<boolean> {
const promise = new Promise<boolean>((resolve, reject) => { const promise = new Promise<boolean>((resolve, reject) => {
ti.resolve = resolve; ti.resolve = resolve;
ti.reject = reject; ti.reject = reject;
@ -309,21 +277,21 @@ export class Nav implements NavOutlet {
} }
// Enqueue transition instruction // Enqueue transition instruction
this._queue.push(ti); this.queue.push(ti);
// if there isn't a transition already happening // if there isn't a transition already happening
// then this will kick off this transition // then this will kick off this transition
this._nextTrns(); this.nextTrns();
return promise; return promise;
} }
private _success(result: NavResult, ti: TransitionInstruction) { private success(result: NavResult, ti: TransitionInstruction) {
if (this._queue === null) { if (this.queue === null) {
this._fireError('nav controller was destroyed', ti); this.fireError('nav controller was destroyed', ti);
return; return;
} }
this._init = true; this.init = true;
if (ti.done) { if (ti.done) {
ti.done( ti.done(
@ -349,27 +317,27 @@ export class Nav implements NavOutlet {
this.ionNavChanged.emit(); this.ionNavChanged.emit();
} }
private _failed(rejectReason: any, ti: TransitionInstruction) { private failed(rejectReason: any, ti: TransitionInstruction) {
if (this._queue === null) { if (this.queue === null) {
this._fireError('nav controller was destroyed', ti); this.fireError('nav controller was destroyed', ti);
return; return;
} }
this._queue.length = 0; this.queue.length = 0;
this._fireError(rejectReason, ti); this.fireError(rejectReason, ti);
} }
private _fireError(rejectReason: any, ti: TransitionInstruction) { private fireError(rejectReason: any, ti: TransitionInstruction) {
if (ti.done) { if (ti.done) {
ti.done(false, false, rejectReason); ti.done(false, false, rejectReason);
} }
if (ti.reject && !this._destroyed) { if (ti.reject && !this.destroyed) {
ti.reject(rejectReason); ti.reject(rejectReason);
} else { } else {
ti.resolve(false); ti.resolve(false);
} }
} }
private _nextTrns(): boolean { private nextTrns(): boolean {
// this is the framework's bread 'n butta function // this is the framework's bread 'n butta function
// only one transition is allowed at any given time // only one transition is allowed at any given time
if (this.isTransitioning) { if (this.isTransitioning) {
@ -378,7 +346,7 @@ export class Nav implements NavOutlet {
// there is no transition happening right now // there is no transition happening right now
// get the next instruction // get the next instruction
const ti = this._queue.shift(); const ti = this.queue.shift();
if (!ti) { if (!ti) {
return false; return false;
} }
@ -391,10 +359,10 @@ export class Nav implements NavOutlet {
try { try {
// set that this nav is actively transitioning // set that this nav is actively transitioning
this.isTransitioning = true; this.isTransitioning = true;
this._prepareTI(ti); this.prepareTI(ti);
const leavingView = this.getActive(); const leavingView = this.getActive();
const enteringView = this._getEnteringView(ti, leavingView); const enteringView = this.getEnteringView(ti, leavingView);
if (!leavingView && !enteringView) { if (!leavingView && !enteringView) {
throw new Error('no views in the stack to be removed'); throw new Error('no views in the stack to be removed');
@ -403,23 +371,22 @@ export class Nav implements NavOutlet {
// Needs transition? // Needs transition?
ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView; ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView;
if (enteringView && enteringView._state === ViewState.New) { if (enteringView && enteringView.state === ViewState.New) {
await enteringView.init(this.el); await enteringView.init(this.el);
} }
this._postViewInit(enteringView, leavingView, ti); this.postViewInit(enteringView, leavingView, ti);
const result = await this._transition(enteringView, leavingView, ti);
this._success(result, ti); const result = await this.transition(enteringView, leavingView, ti);
this.success(result, ti);
} catch (rejectReason) { } catch (rejectReason) {
this._failed(rejectReason, ti); this.failed(rejectReason, ti);
} }
this.isTransitioning = false; this.isTransitioning = false;
this._nextTrns(); this.nextTrns();
} }
private _prepareTI(ti: TransitionInstruction) { private prepareTI(ti: TransitionInstruction) {
const viewsLength = this._views.length; const viewsLength = this.views.length;
ti.opts = ti.opts || {}; ti.opts = ti.opts || {};
@ -427,10 +394,10 @@ export class Nav implements NavOutlet {
ti.opts.delegate = this.delegate; ti.opts.delegate = this.delegate;
} }
if (ti.removeView != null) { if (ti.removeView != null) {
assert(isPresent(ti.removeStart), 'removeView needs removeStart'); assert(ti.removeStart != null, 'removeView needs removeStart');
assert(isPresent(ti.removeCount), 'removeView needs removeCount'); assert(ti.removeCount != null, 'removeView needs removeCount');
const index = this._views.indexOf(ti.removeView); const index = this.views.indexOf(ti.removeView);
if (index < 0) { if (index < 0) {
throw new Error('removeView was not found'); throw new Error('removeView was not found');
} }
@ -467,21 +434,20 @@ export class Nav implements NavOutlet {
} }
// Check all the inserted view are correct // Check all the inserted view are correct
for (let i = 0; i < viewControllers.length; i++) { for (const view of viewControllers) {
const view = viewControllers[i];
view.delegate = ti.opts.delegate; view.delegate = ti.opts.delegate;
const nav = view.nav; const nav = view.nav;
if (nav && nav !== this) { if (nav && nav !== this) {
throw new Error('inserted view was already inserted'); throw new Error('inserted view was already inserted');
} }
if (view._state === ViewState.Destroyed) { if (view.state === ViewState.Destroyed) {
throw new Error('inserted view was already destroyed'); throw new Error('inserted view was already destroyed');
} }
} }
ti.insertViews = viewControllers; ti.insertViews = viewControllers;
} }
private _getEnteringView(ti: TransitionInstruction, leavingView: ViewController): ViewController { private getEnteringView(ti: TransitionInstruction, leavingView: ViewController): ViewController|undefined {
const insertViews = ti.insertViews; const insertViews = ti.insertViews;
if (insertViews) { if (insertViews) {
// grab the very last view of the views to be inserted // grab the very last view of the views to be inserted
@ -490,8 +456,8 @@ export class Nav implements NavOutlet {
} }
const removeStart = ti.removeStart; const removeStart = ti.removeStart;
if (isPresent(removeStart)) { if (removeStart != null) {
const views = this._views; const views = this.views;
const removeEnd = removeStart + ti.removeCount; const removeEnd = removeStart + ti.removeCount;
for (let i = views.length - 1; i >= 0; i--) { for (let i = views.length - 1; i >= 0; i--) {
const view = views[i]; const view = views[i];
@ -500,10 +466,10 @@ export class Nav implements NavOutlet {
} }
} }
} }
return null; return undefined;
} }
private _postViewInit(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) { private postViewInit(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
assert(leavingView || enteringView, 'Both leavingView and enteringView are null'); assert(leavingView || enteringView, 'Both leavingView and enteringView are null');
assert(ti.resolve, 'resolve must be valid'); assert(ti.resolve, 'resolve must be valid');
assert(ti.reject, 'reject must be valid'); assert(ti.reject, 'reject must be valid');
@ -515,13 +481,13 @@ export class Nav implements NavOutlet {
let destroyQueue: ViewController[] = undefined; let destroyQueue: ViewController[] = undefined;
// there are views to remove // there are views to remove
if (isPresent(removeStart)) { if (removeStart != null) {
assert(removeStart >= 0, 'removeStart can not be negative'); assert(removeStart >= 0, 'removeStart can not be negative');
assert(removeCount >= 0, 'removeCount can not be negative'); assert(removeCount >= 0, 'removeCount can not be negative');
destroyQueue = []; destroyQueue = [];
for (let i = 0; i < removeCount; i++) { for (let i = 0; i < removeCount; i++) {
const view = this._views[i + removeStart]; const view = this.views[i + removeStart];
if (view && view !== enteringView && view !== leavingView) { if (view && view !== enteringView && view !== leavingView) {
destroyQueue.push(view); destroyQueue.push(view);
} }
@ -530,7 +496,7 @@ export class Nav implements NavOutlet {
opts.direction = opts.direction || NavDirection.Back; opts.direction = opts.direction || NavDirection.Back;
} }
const finalBalance = this._views.length + (insertViews ? insertViews.length : 0) - (removeCount ? removeCount : 0); const finalBalance = this.views.length + (insertViews ? insertViews.length : 0) - (removeCount ? removeCount : 0);
assert(finalBalance >= 0, 'final balance can not be negative'); assert(finalBalance >= 0, 'final balance can not be negative');
if (finalBalance === 0) { if (finalBalance === 0) {
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`, console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`,
@ -542,15 +508,11 @@ export class Nav implements NavOutlet {
// At this point the transition can not be rejected, any throw should be an error // At this point the transition can not be rejected, any throw should be an error
// there are views to insert // there are views to insert
if (insertViews) { if (insertViews) {
// 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 // add the views to the
for (let i = 0; i < insertViews.length; i++) { let insertIndex = ti.insertStart;
const view = insertViews[i]; for (const view of insertViews) {
this._insertViewAt(view, ti.insertStart + i); this.insertViewAt(view, insertIndex);
insertIndex++;
} }
if (ti.enteringRequiresTransition) { if (ti.enteringRequiresTransition) {
@ -565,22 +527,20 @@ export class Nav implements NavOutlet {
// batch all of lifecycles together // batch all of lifecycles together
// let's make sure, callbacks are zoned // let's make sure, callbacks are zoned
if (destroyQueue && destroyQueue.length > 0) { if (destroyQueue && destroyQueue.length > 0) {
for (let i = 0; i < destroyQueue.length; i++) { for (const view of destroyQueue) {
const view = destroyQueue[i];
lifecycle(view.element, ViewLifecycle.WillLeave); lifecycle(view.element, ViewLifecycle.WillLeave);
lifecycle(view.element, ViewLifecycle.DidLeave); lifecycle(view.element, ViewLifecycle.DidLeave);
lifecycle(view.element, ViewLifecycle.WillUnload); lifecycle(view.element, ViewLifecycle.WillUnload);
} }
// once all lifecycle events has been delivered, we can safely detroy the views // once all lifecycle events has been delivered, we can safely detroy the views
for (let i = 0; i < destroyQueue.length; i++) { for (const view of destroyQueue) {
this._destroyView(destroyQueue[i]); this.destroyView(view);
} }
} }
} }
private async _transition(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise<NavResult> { private async transition(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise<NavResult> {
if (!ti.requiresTransition) { if (!ti.requiresTransition) {
// transition is not required, so we are already done! // transition is not required, so we are already done!
// they're inserting/removing the views somewhere in the middle or // they're inserting/removing the views somewhere in the middle or
@ -591,9 +551,9 @@ export class Nav implements NavOutlet {
requiresTransition: false requiresTransition: false
}); });
} }
if (this._sbTrns) { if (this.sbTrns) {
this._sbTrns.destroy(); this.sbTrns.destroy();
this._sbTrns = null; this.sbTrns = null;
} }
// we should animate (duration > 0) if the pushed page is not the first one (startup) // we should animate (duration > 0) if the pushed page is not the first one (startup)
@ -602,7 +562,7 @@ export class Nav implements NavOutlet {
const animationBuilder = this.getAnimationBuilder(ti.opts); const animationBuilder = this.getAnimationBuilder(ti.opts);
const progressAnimation = ti.opts.progressAnimation const progressAnimation = ti.opts.progressAnimation
? (animation: Animation) => this._sbTrns = animation ? (animation: Animation) => this.sbTrns = animation
: undefined; : undefined;
const opts = ti.opts; const opts = ti.opts;
@ -625,17 +585,14 @@ export class Nav implements NavOutlet {
leavingEl leavingEl
}; };
const trns = await transition(animationOpts); const trns = await transition(animationOpts);
return this._transitionFinish(trns, enteringView, leavingView, ti.opts); return this.transitionFinish(trns, enteringView, leavingView, ti.opts);
} }
private _transitionFinish(transition: Animation|void, enteringView: ViewController, leavingView: ViewController, opts: NavOptions): NavResult { private transitionFinish(transition: Animation|void, enteringView: ViewController, leavingView: ViewController, opts: NavOptions): NavResult {
const hasCompleted = transition ? transition.hasCompleted : true; const hasCompleted = transition ? transition.hasCompleted : true;
if (hasCompleted) { const cleanupView = hasCompleted ? enteringView : leavingView;
this._cleanup(enteringView); this.cleanup(cleanupView);
} else {
this._cleanup(leavingView);
}
// this is the root transition // this is the root transition
// it's safe to destroy this transition // it's safe to destroy this transition
@ -651,20 +608,21 @@ export class Nav implements NavOutlet {
} }
private getAnimationBuilder(opts: NavOptions) { private getAnimationBuilder(opts: NavOptions) {
if (opts.duration === 0 || opts.animate === false || !this._init || this.animated === false || this._views.length <= 1) { if (opts.duration === 0 || opts.animate === false || !this.init || this.animated === false || this.views.length <= 1) {
return undefined; return undefined;
} }
const mode = opts.animation || this.config.get('pageTransition', this.mode); const mode = opts.animation || this.config.get('pageTransition', this.mode);
return mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation; return mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation;
} }
private _insertViewAt(view: ViewController, index: number) { private insertViewAt(view: ViewController, index: number) {
const existingIndex = this._views.indexOf(view); const views = this.views;
const existingIndex = views.indexOf(view);
if (existingIndex > -1) { if (existingIndex > -1) {
// this view is already in the stack!! // this view is already in the stack!!
// move it to its new location // move it to its new location
assert(view.nav === this, 'view is not part of the nav'); assert(view.nav === this, 'view is not part of the nav');
this._views.splice(index, 0, this._views.splice(existingIndex, 1)[0]); views.splice(index, 0, views.splice(existingIndex, 1)[0]);
} else { } else {
assert(!view.nav, 'nav is used'); assert(!view.nav, 'nav is used');
// this is a new view to add to the stack // this is a new view to add to the stack
@ -672,14 +630,14 @@ export class Nav implements NavOutlet {
view.nav = this; view.nav = this;
// insert the entering view into the correct index in the stack // insert the entering view into the correct index in the stack
this._views.splice(index, 0, view); views.splice(index, 0, view);
} }
} }
private _removeView(view: ViewController) { private removeView(view: ViewController) {
assert(view._state === ViewState.Attached || view._state === ViewState.Destroyed, 'view state should be loaded or destroyed'); assert(view.state === ViewState.Attached || view.state === ViewState.Destroyed, 'view state should be loaded or destroyed');
const views = this._views; const views = this.views;
const index = views.indexOf(view); const index = views.indexOf(view);
assert(index > -1, 'view must be part of the stack'); assert(index > -1, 'view must be part of the stack');
if (index >= 0) { if (index >= 0) {
@ -687,41 +645,42 @@ export class Nav implements NavOutlet {
} }
} }
private _destroyView(view: ViewController) { private destroyView(view: ViewController) {
view._destroy(); view._destroy();
this._removeView(view); this.removeView(view);
} }
/** /**
* DOM WRITE * DOM WRITE
*/ */
private _cleanup(activeView: ViewController) { private cleanup(activeView: ViewController) {
// ok, cleanup time!! Destroy all of the views that are // ok, cleanup time!! Destroy all of the views that are
// INACTIVE and come after the active view // INACTIVE and come after the active view
// only do this if the views exist, though // only do this if the views exist, though
if (!this._destroyed) { if (this.destroyed) {
const activeViewIndex = this._views.indexOf(activeView); return;
const views = this._views; }
const views = this.views;
const activeViewIndex = views.indexOf(activeView);
for (let i = views.length - 1; i >= 0; i--) { for (let i = views.length - 1; i >= 0; i--) {
const view = views[i]; const view = views[i];
if (i > activeViewIndex) { if (i > activeViewIndex) {
// this view comes after the active view // this view comes after the active view
// let's unload it // let's unload it
lifecycle(view.element, ViewLifecycle.WillUnload); lifecycle(view.element, ViewLifecycle.WillUnload);
this._destroyView(view); this.destroyView(view);
} else if (i < activeViewIndex) { } else if (i < activeViewIndex) {
// this view comes before the active view // this view comes before the active view
// and it is not a portal then ensure it is hidden // and it is not a portal then ensure it is hidden
view.element.hidden = true; view.element.hidden = true;
}
} }
} }
} }
private swipeBackStart() { private swipeBackStart() {
if (this.isTransitioning || this._queue.length > 0) { if (this.isTransitioning || this.queue.length > 0) {
return; return;
} }
@ -731,7 +690,7 @@ export class Nav implements NavOutlet {
progressAnimation: true progressAnimation: true
}; };
this._queueTrns({ this.queueTrns({
removeStart: -1, removeStart: -1,
removeCount: 1, removeCount: 1,
opts: opts, opts: opts,
@ -739,22 +698,20 @@ export class Nav implements NavOutlet {
} }
private swipeBackProgress(detail: GestureDetail) { private swipeBackProgress(detail: GestureDetail) {
if (this._sbTrns) { if (this.sbTrns) {
// continue to disable the app while actively dragging // continue to disable the app while actively dragging
// TODO
// this.app.setEnabled(false, ACTIVE_TRANSITION_DEFAULT);
this.isTransitioning = true; this.isTransitioning = true;
// set the transition animation's progress // set the transition animation's progress
const delta = detail.deltaX; const delta = detail.deltaX;
const stepValue = delta / window.innerWidth; const stepValue = delta / window.innerWidth;
// set the transition animation's progress // set the transition animation's progress
this._sbTrns.progressStep(stepValue); this.sbTrns.progressStep(stepValue);
} }
} }
private swipeBackEnd(detail: GestureDetail) { private swipeBackEnd(detail: GestureDetail) {
if (this._sbTrns) { if (this.sbTrns) {
// the swipe back gesture has ended // the swipe back gesture has ended
const delta = detail.deltaX; const delta = detail.deltaX;
const width = window.innerWidth; const width = window.innerWidth;
@ -772,11 +729,11 @@ export class Nav implements NavOutlet {
realDur = Math.min(dur, 300); realDur = Math.min(dur, 300);
} }
this._sbTrns.progressEnd(shouldComplete, stepValue, realDur); this.sbTrns.progressEnd(shouldComplete, stepValue, realDur);
} }
} }
canSwipeBack(): boolean { private canSwipeBack(): boolean {
return ( return (
this.swipeBackEnabled && this.swipeBackEnabled &&
!this.isTransitioning && !this.isTransitioning &&
@ -785,24 +742,21 @@ export class Nav implements NavOutlet {
} }
render() { render() {
const dom = []; return [
if (this.swipeBackEnabled) { this.swipeBackEnabled &&
dom.push(<ion-gesture <ion-gesture
canStart={this.canSwipeBack.bind(this)} canStart={this.canSwipeBack.bind(this)}
onStart={this.swipeBackStart.bind(this)} onStart={this.swipeBackStart.bind(this)}
onMove={this.swipeBackProgress.bind(this)} onMove={this.swipeBackProgress.bind(this)}
onEnd={this.swipeBackEnd.bind(this)} onEnd={this.swipeBackEnd.bind(this)}
gestureName='goback-swipe' gestureName='goback-swipe'
gesturePriority={10} gesturePriority={10}
type='pan' type='pan'
direction='x' direction='x'
threshold={10} threshold={10}
attachTo='body'/>); attachTo='body'/>,
} this.mode === 'ios' && <div class='nav-decor'/>,
if (this.mode === 'ios') { <slot></slot>
dom.push(<div class='nav-decor'/>); ];
}
dom.push(<slot></slot>);
return dom;
} }
} }

View File

@ -81,19 +81,16 @@ boolean
#### getRouteId() #### getRouteId()
#### getViews()
#### insert() #### insert()
#### insertPages() #### insertPages()
#### pop() #### length()
#### popAll() #### pop()
#### popTo() #### popTo()
@ -108,9 +105,6 @@ boolean
#### removeIndex() #### removeIndex()
#### removeView()
#### setPages() #### setPages()

View File

@ -112,7 +112,7 @@ describe('NavController', () => {
); );
expect(nav.length()).toEqual(1); expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1); expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.isTransitioning).toEqual(false); expect(nav['isTransitioning']).toEqual(false);
}, 10000); }, 10000);
@ -131,7 +131,7 @@ describe('NavController', () => {
expect(nav.length()).toEqual(2); expect(nav.length()).toEqual(2);
expect(nav.getByIndex(0).component).toEqual(MockView1); expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.getByIndex(1).component).toEqual(MockView2); expect(nav.getByIndex(1).component).toEqual(MockView2);
expect(nav.isTransitioning).toEqual(false); expect(nav['isTransitioning']).toEqual(false);
}, 10000); }, 10000);
@ -175,16 +175,6 @@ describe('NavController', () => {
describe('insert', () => { describe('insert', () => {
it('should not modify the view id', async () => {
const view = mockView(MockView4);
view.id = 'custom_id';
await nav.insert(0, view);
expect(view.id).toEqual('custom_id');
expect(view.id).toEqual('custom_id');
}, 10000);
it('should insert at the begining with no async transition', async () => { it('should insert at the begining with no async transition', async () => {
const view4 = mockView(MockView4); const view4 = mockView(MockView4);
const instance4 = spyOnLifecycles(view4); const instance4 = spyOnLifecycles(view4);
@ -366,7 +356,7 @@ describe('NavController', () => {
); );
expect(err).toEqual(rejectReason); expect(err).toEqual(rejectReason);
expect(nav.length()).toEqual(0); expect(nav.length()).toEqual(0);
expect(nav.isTransitioning).toEqual(false); expect(nav['isTransitioning']).toEqual(false);
done(); done();
}); });
}, 10000); }, 10000);
@ -403,7 +393,7 @@ describe('NavController', () => {
); );
expect(nav.length()).toEqual(1); expect(nav.length()).toEqual(1);
expect(nav.getByIndex(0).component).toEqual(MockView1); expect(nav.getByIndex(0).component).toEqual(MockView1);
expect(nav.isTransitioning).toEqual(false); expect(nav['isTransitioning']).toEqual(false);
}, 10000); }, 10000);
@ -1003,7 +993,7 @@ describe('NavController', () => {
const view2 = mockView(); const view2 = mockView();
mockViews(nav, [view1, view2]); mockViews(nav, [view1, view2]);
const result = nav.canSwipeBack(); const result = nav['canSwipeBack']();
expect(result).toEqual(false); expect(result).toEqual(false);
}); });
@ -1013,7 +1003,7 @@ describe('NavController', () => {
const view2 = mockView(); const view2 = mockView();
mockViews(nav, [view1, view2]); mockViews(nav, [view1, view2]);
const result = nav.canSwipeBack(); const result = nav['canSwipeBack']();
expect(result).toEqual(true); expect(result).toEqual(true);
}); });
}); });
@ -1092,7 +1082,7 @@ function mockView(component ?: any, data ?: any) {
} }
function mockViews(nav: Nav, views: ViewController[]) { function mockViews(nav: Nav, views: ViewController[]) {
nav['_views'] = views; nav['views'] = views;
views.forEach(v => { views.forEach(v => {
v.nav = nav; v.nav = nav;
}); });
@ -1111,7 +1101,7 @@ function mockNavController(): Nav {
? mockElement(enteringView.component) as HTMLElement ? mockElement(enteringView.component) as HTMLElement
: enteringView.element = enteringView.component as HTMLElement; : enteringView.element = enteringView.component as HTMLElement;
} }
enteringView._state = ViewState.Attached; enteringView.state = ViewState.Attached;
}; };
return nav; return nav;
} }

View File

@ -1,91 +1,31 @@
import { ViewState } from './nav-util';
import { NavOptions, ViewState } from './nav-util';
import { assert } from '../../utils/helpers'; import { assert } from '../../utils/helpers';
import { FrameworkDelegate, Nav } from '../..'; import { ComponentProps, FrameworkDelegate, Nav } from '../..';
import { attachComponent } from '../../utils/framework-delegate'; import { attachComponent } from '../../utils/framework-delegate';
/**
* @name ViewController
* @description
* Access various features and information about the current view.
* @usage
* ```ts
* import { Component } from '@angular/core';
* import { ViewController } from 'ionic-angular';
*
* @Component({...})
* export class MyPage{
*
* constructor(public viewCtrl: ViewController) {}
*
* }
* ```
*/
export class ViewController {
private _cntDir: any; export class ViewController {
private _leavingOpts: NavOptions;
nav: Nav; nav: Nav;
_state: ViewState = ViewState.New; state: ViewState = ViewState.New;
/** @hidden */
id: string;
element: HTMLElement; element: HTMLElement;
delegate: FrameworkDelegate; delegate: FrameworkDelegate;
constructor( constructor(
public component: any, public component: any,
public data: any public params: any
) {} ) {}
/** /**
* @hidden * @hidden
*/ */
async init(container: HTMLElement) { async init(container: HTMLElement) {
this._state = ViewState.Attached; this.state = ViewState.Attached;
if (!this.element) { if (!this.element) {
const component = this.component; const component = this.component;
this.element = await attachComponent(this.delegate, container, component, ['ion-page', 'hide-page'], this.data); this.element = await attachComponent(this.delegate, container, component, ['ion-page', 'hide-page'], this.params);
}
}
/**
* @hidden
*/
setLeavingOpts(opts: NavOptions) {
this._leavingOpts = opts;
}
matches(id: string, params: any): boolean {
if (this.component !== id) {
return false;
}
const currentParams = this.data;
const null1 = (currentParams == null);
const null2 = (params == null);
if (null1 !== null2) {
return false;
}
if (null1 && null2) {
return true;
}
const keysA = Object.keys(currentParams);
const keysB = Object.keys(params);
if (keysA.length !== keysB.length) {
return false;
} }
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (currentParams[key] !== params[key]) {
return false;
}
}
return true;
} }
/** /**
@ -93,7 +33,7 @@ export class ViewController {
* DOM WRITE * DOM WRITE
*/ */
_destroy() { _destroy() {
assert(this._state !== ViewState.Destroyed, 'view state must be ATTACHED'); assert(this.state !== ViewState.Destroyed, 'view state must be ATTACHED');
const element = this.element; const element = this.element;
if (element) { if (element) {
@ -103,19 +43,40 @@ export class ViewController {
element.remove(); element.remove();
} }
} }
this.nav = this._cntDir = this._leavingOpts = null; this.nav = null;
this._state = ViewState.Destroyed; this.state = ViewState.Destroyed;
} }
}
/** export function matches(view: ViewController|undefined, id: string, params: ComponentProps): boolean {
* Get the index of the current component in the current navigation stack. if (!view) {
* @returns {number} Returns the index of this page within its `NavController`. return false;
*/ }
get index(): number { if (view.component !== id) {
return (this.nav ? this.nav.indexOf(this) : -1); return false;
}
const currentParams = view.params;
const null1 = (currentParams == null);
const null2 = (params == null);
if (null1 !== null2) {
return false;
}
if (null1 && null2) {
return true;
} }
}
export function isViewController(viewCtrl: any): viewCtrl is ViewController { const keysA = Object.keys(currentParams);
return viewCtrl instanceof ViewController; const keysB = Object.keys(params);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (currentParams[key] !== params[key]) {
return false;
}
}
return true;
} }