refactor(navigation): get rid of ion-nav-controller, get nav working correctly in DOM, angular, react

This commit is contained in:
Dan Bucholtz
2017-12-08 14:45:13 -06:00
parent b0cd97b623
commit c30337bf8c
29 changed files with 1255 additions and 1191 deletions

View File

@ -1,17 +1,23 @@
import {
ChangeDetectorRef,
Component,
OnInit,
ComponentFactoryResolver,
ComponentRef,
ElementRef,
Injector,
ReflectiveInjector,
Type,
ViewContainerRef,
ViewChild
} from '@angular/core';
import { PublicNav } from '@ionic/core';
import { FrameworkDelegate } from '@ionic/core';
import { getProviders } from '../di/di';
import { AngularFrameworkDelegate } from '../providers/angular-framework-delegate';
import { AngularViewController } from '../types/angular-view-controller';
import { AngularComponentMounter } from '../providers/angular-component-mounter';
import { AngularMountingData } from '../types/interfaces';
const elementToComponentRefMap = new Map<HTMLElement, ComponentRef<any>>();
@Component({
selector: 'ion-nav',
@ -19,41 +25,41 @@ import { AngularViewController } from '../types/angular-view-controller';
<div #viewport class="ng-nav-viewport"></div>
`
})
export class IonNavDelegate implements OnInit {
export class IonNavDelegate implements FrameworkDelegate {
@ViewChild('viewport', { read: ViewContainerRef}) viewport: ViewContainerRef;
constructor(private changeDetection: ChangeDetectorRef, private angularFrameworkDelegate: AngularFrameworkDelegate) {
constructor(private elementRef: ElementRef, private changeDetection: ChangeDetectorRef, private angularComponentMounter: AngularComponentMounter, private injector: Injector, private componentResolveFactory: ComponentFactoryResolver) {
this.elementRef.nativeElement.delegate = this;
}
ngOnInit() {
const controllerElement = document.querySelector('ion-nav-controller') as any;
controllerElement.delegate = this;
async attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>,
_propsOrDataObj?: any, _classesToAdd?: string[]): Promise<AngularMountingData> {
const componentProviders = ReflectiveInjector.resolve(getProviders(elementOrContainerToMountTo));
console.log('componentProviders: ', componentProviders);
const element = document.createElement('ion-page');
for (const clazz of _classesToAdd) {
element.classList.add(clazz);
}
elementOrContainerToMountTo.appendChild(element);
const mountingData = await this.angularComponentMounter.attachViewToDom(element, elementOrComponentToMount, [], this.changeDetection, this.componentResolveFactory, this.injector);
mountingData.element = element;
elementToComponentRefMap.set(mountingData.angularHostElement, mountingData.componentRef);
return mountingData;
}
attachViewToDom(nav: PublicNav, enteringView: AngularViewController): Promise<any> {
const componentProviders = ReflectiveInjector.resolve(getProviders(nav.element as HTMLIonNavElement));
return this.angularFrameworkDelegate.attachViewToDom(enteringView.component, this.viewport, componentProviders, this.changeDetection).then((angularMountingData) => {
enteringView.componentFactory = angularMountingData.componentFactory;
enteringView.injector = angularMountingData.childInjector;
enteringView.componentRef = angularMountingData.componentRef;
enteringView.instance = angularMountingData.componentRef.instance;
enteringView.angularHostElement = angularMountingData.componentRef.location.nativeElement;
enteringView.element = angularMountingData.componentRef.location.nativeElement.querySelector('ion-page');
});
}
removeViewFromDom(_nav: PublicNav, viewController: AngularViewController) {
return this.angularFrameworkDelegate.removeViewFromDom((viewController as any).componentRef).then(() => {
viewController.componentFactory = null;
viewController.injector = null;
viewController.componentRef = null;
viewController.instance = null;
viewController.angularHostElement = null;
viewController.element = null;
});
async removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
const componentRef = elementToComponentRefMap.get(childElement);
if (componentRef) {
return this.angularComponentMounter.removeViewFromDom(componentRef);
}
return Promise.resolve();
}
}

View File

@ -16,7 +16,7 @@ import { IonNavDelegate } from './components/ion-nav';
/* Providers */
import { ActionSheetController } from './providers/action-sheet-controller';
import { AlertController } from './providers/alert-controller';
import { AngularFrameworkDelegate } from './providers/angular-framework-delegate';
import { AngularComponentMounter } from './providers/angular-component-mounter';
import { LoadingController } from './providers/loading-controller';
import { ToastController } from './providers/toast-controller';
@ -48,7 +48,7 @@ export class IonicAngularModule {
providers: [
AlertController,
ActionSheetController,
AngularFrameworkDelegate,
AngularComponentMounter,
LoadingController,
ToastController
]

View File

@ -0,0 +1,59 @@
import {
ChangeDetectorRef,
ComponentFactoryResolver,
ComponentRef,
Injectable,
Injector,
NgZone,
ReflectiveInjector,
Type,
} from '@angular/core';
import { AngularMountingData } from '../types/interfaces';
@Injectable()
export class AngularComponentMounter {
constructor(private defaultCfr: ComponentFactoryResolver, private zone: NgZone) {
}
attachViewToDom(parentElement: HTMLElement, componentToMount: Type<any>, providers: any[], changeDetection: ChangeDetectorRef, componentResolveFactory: ComponentFactoryResolver, injector: Injector): Promise<AngularMountingData> {
return new Promise((resolve) => {
this.zone.run(() => {
console.log('parentElement: ', parentElement);
const crf = componentResolveFactory ? componentResolveFactory : this.defaultCfr;
const mountingData = attachViewToDom(crf, componentToMount, parentElement, providers, changeDetection, injector);
resolve(mountingData);
});
});
}
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> {
return new Promise((resolve) => {
this.zone.run(() => {
componentRef.destroy();
resolve();
});
});
}
}
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, element: HTMLElement, providers: any, changeDetection: ChangeDetectorRef, injector: Injector): AngularMountingData {
const componentFactory = crf.resolveComponentFactory(componentToMount);
const componentProviders = ReflectiveInjector.resolve(providers);
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector);
const componentRef = componentFactory.create(childInjector, [], element);
changeDetection.detectChanges();
return {
componentFactory,
childInjector: childInjector,
componentRef: componentRef,
instance: componentRef.instance,
angularHostElement: componentRef.location.nativeElement,
element: componentRef.location.nativeElement,
};
}

View File

@ -1,60 +0,0 @@
import {
ChangeDetectorRef,
ComponentFactoryResolver,
ComponentRef,
Injectable,
Injector,
NgZone,
ReflectiveInjector,
Type,
ViewContainerRef,
} from '@angular/core';
import { ComponentFactory } from '@angular/core/src/linker/component_factory';
@Injectable()
export class AngularFrameworkDelegate {
constructor(private crf: ComponentFactoryResolver, private zone: NgZone) {
}
attachViewToDom(componentToMount: Type<any>, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): Promise<AngularMountingData> {
return new Promise((resolve) => {
this.zone.run(() => {
const mountingData = attachViewToDom(this.crf, componentToMount, viewport, providers, changeDetection);
resolve(mountingData);
});
});
}
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> {
return new Promise((resolve) => {
this.zone.run(() => {
componentRef.destroy();
resolve();
});
});
}
}
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): AngularMountingData{
const componentFactory = crf.resolveComponentFactory(componentToMount);
const componentProviders = ReflectiveInjector.resolve(providers);
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, viewport.parentInjector);
const componentRef = componentFactory.create(childInjector, []);
viewport.insert(componentRef.hostView, viewport.length);
changeDetection.detectChanges();
return {
componentFactory,
childInjector: childInjector,
componentRef: componentRef
}
}
export interface AngularMountingData {
componentFactory: ComponentFactory<any>;
childInjector: Injector;
componentRef: ComponentRef<any>;
}

View File

@ -4,12 +4,12 @@ import {
Injector
} from '@angular/core';
import { PublicViewController } from '@ionic/core';
import { FrameworkMountingData } from '@ionic/core';
export interface AngularViewController extends PublicViewController {
export interface AngularMountingData extends FrameworkMountingData {
componentFactory?: ComponentFactory<any>;
injector?: Injector;
childInjector?: Injector;
componentRef?: ComponentRef<any>;
instance?: any;
angularHostElement?: HTMLElement;
}
}

View File

@ -1635,36 +1635,6 @@ declare global {
}
import {
NavController as IonNavController
} from './components/nav-controller/nav-controller';
declare global {
interface HTMLIonNavControllerElement extends IonNavController, HTMLElement {
}
var HTMLIonNavControllerElement: {
prototype: HTMLIonNavControllerElement;
new (): HTMLIonNavControllerElement;
};
interface HTMLElementTagNameMap {
"ion-nav-controller": HTMLIonNavControllerElement;
}
interface ElementTagNameMap {
"ion-nav-controller": HTMLIonNavControllerElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-nav-controller": JSXElements.IonNavControllerAttributes;
}
}
namespace JSXElements {
export interface IonNavControllerAttributes extends HTMLAttributes {
delegate?: FrameworkDelegate;
}
}
}
import {
Nav as IonNav
} from './components/nav/nav';

View File

@ -1,126 +0,0 @@
import { Component, Element, Method, Prop } from '@stencil/core';
import { Animation, AnimationController, ComponentDataPair, FrameworkDelegate, Nav, NavOptions, ViewController } from '../../index';
import { DomFrameworkDelegate } from './dom-framework-delegate';
import {
insert as insertImpl,
insertPages as insertPagesImpl,
pop as popImpl,
popTo as popToImpl,
popToRoot as popToRootImpl,
push as pushImpl,
remove as removeImpl,
removeView as removeViewImpl,
setPages as setPagesImpl,
setRoot as setRootImpl,
} from '../../navigation/nav-controller-functions';
let defaultDelegate: FrameworkDelegate = null;
@Component({
tag: 'ion-nav-controller',
})
export class NavController {
@Element() element: HTMLElement;
@Prop() delegate: FrameworkDelegate;
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
constructor() {
}
@Method()
push(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return pushImpl(nav, delegate, animation, component, data, opts);
});
}
@Method()
pop(nav: Nav, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return popImpl(nav, delegate, animation, opts);
});
}
@Method()
setRoot(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return setRootImpl(nav, delegate, animation, component, data, opts);
});
}
@Method()
insert(nav: Nav, insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return insertImpl(nav, delegate, animation, insertIndex, page, params, opts);
});
}
@Method()
insertPages(nav: Nav, insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return insertPagesImpl(nav, delegate, animation, insertIndex, insertPages, opts);
});
}
@Method()
popToRoot(nav: Nav, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return popToRootImpl(nav, delegate, animation, opts);
});
}
@Method()
popTo(nav: Nav, indexOrViewCtrl: any, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return popToImpl(nav, delegate, animation, indexOrViewCtrl, opts);
});
}
@Method()
removeIndex(nav: Nav, startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return removeImpl(nav, delegate, animation, startIndex, removeCount, opts);
});
}
@Method()
removeView(nav: Nav, viewController: ViewController, opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return removeViewImpl(nav, delegate, animation, viewController, opts);
});
}
@Method()
setPages(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions): Promise<any> {
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
return setPagesImpl(nav, delegate, animation, componentDataPairs, opts);
});
}
render() {
return <slot></slot>;
}
}
export function hydrateDelegateAndAnimation(navController: NavController): Promise<any> {
return Promise.all([hydrateDelegate(navController), hydrateAnimationController(navController.animationCtrl)]);
}
export function hydrateDelegate(navController: NavController): Promise<FrameworkDelegate> {
if (navController.delegate) {
return Promise.resolve(navController.delegate);
}
// no delegate is set, so fall back to using the DomFrameworkDelegate
defaultDelegate = new DomFrameworkDelegate();
return Promise.resolve(defaultDelegate);
}
export function hydrateAnimationController(animationController: AnimationController): Promise<Animation> {
return animationController.create();
}

View File

@ -1,57 +0,0 @@
# ion-nav-controller
<!-- Auto Generated Below -->
## Properties
#### delegate
any
## Attributes
#### delegate
any
## Methods
#### insert()
#### insertPages()
#### pop()
#### popTo()
#### popToRoot()
#### push()
#### removeIndex()
#### removeView()
#### setPages()
#### setRoot()
----------------------------------------------
*Built by [StencilJS](https://stenciljs.com/)*

View File

@ -1,24 +0,0 @@
/* it is very important to keep this interface in sync with ./nav */
import { NavOptions, PublicViewController } from '../../index';
export interface PublicNav {
push(component: any, data?: any, opts?: NavOptions): Promise<any>;
pop(opts?: NavOptions): Promise<any>;
setRoot(component: any, data?: any, opts?: NavOptions): Promise<any>;
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any>;
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any>;
popToRoot(opts?: NavOptions): Promise<any>;
popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise<any>;
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any>;
removeView(viewController: PublicViewController, opts?: NavOptions): Promise<any>;
setPages(componentDataPairs: any[], opts?: NavOptions): Promise<any>;
getActive?(): PublicViewController;
getPrevious?(view?: PublicViewController): PublicViewController;
canGoBack?(): boolean;
canSwipeBack?(): boolean;
getFirstView?(): PublicViewController;
element?: HTMLElement;
}

View File

@ -1,18 +1,34 @@
/* it is very important to keep this interface in sync with ./nav */
import {
Animation,
AnimationOptions,
FrameworkDelegate,
Nav,
NavOptions,
PublicViewController,
ViewController
} from '../index';
} from '../../index';
export interface FrameworkDelegate {
export interface PublicNav {
push(component: any, data?: any, opts?: NavOptions): Promise<any>;
pop(opts?: NavOptions): Promise<any>;
setRoot(component: any, data?: any, opts?: NavOptions): Promise<any>;
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any>;
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any>;
popToRoot(opts?: NavOptions): Promise<any>;
popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise<any>;
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any>;
removeView(viewController: PublicViewController, opts?: NavOptions): Promise<any>;
setPages(componentDataPairs: any[], opts?: NavOptions): Promise<any>;
attachViewToDom(elementOrContainerToMountTo: any, elementOrComponentToMount: any, propsOrDataObj?: any, classesToAdd?: string[]): Promise<FrameworkMountingData>;
removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any): Promise<FrameworkMountingData>;
}
getActive?(): PublicViewController;
getPrevious?(view?: PublicViewController): PublicViewController;
canGoBack?(): boolean;
canSwipeBack?(): boolean;
getFirstView?(): PublicViewController;
export interface FrameworkMountingData {
element: HTMLElement;
element?: HTMLElement;
}
export interface NavContainer {

View File

@ -1,6 +1,6 @@
import { Transition } from './nav-interfaces';
import { Animation, AnimationOptions, Config, Nav, RouterEntry, TransitionBuilder, ViewController } from '..';
import { isDef } from '../utils/helpers';
import { Animation, AnimationOptions, Config, Nav, RouterEntry, TransitionBuilder, ViewController } from '../../index';
import { isDef } from '../../utils/helpers';
export const STATE_NEW = 1;
export const STATE_INITIALIZED = 2;

View File

@ -1,25 +1,59 @@
import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import {
Animation,
AnimationController,
AnimationOptions,
ComponentDataPair,
Config,
FrameworkDelegate,
NavController,
NavOptions,
NavResult,
NavState,
PublicNav,
PublicViewController,
RouterEntries,
ViewController
Transition,
TransitionInstruction,
} from '../../index';
import { ViewController } from './view-controller';
import {
DIRECTION_BACK,
DIRECTION_FORWARD,
STATE_ATTACHED,
STATE_DESTROYED,
STATE_NEW,
VIEW_ID_START,
destroyTransition,
getActiveImpl,
getFirstView,
getHydratedTransition,
getNextNavId,
getNextTransitionId,
getParentTransitionId,
getPreviousImpl,
getViews,
resolveRoute
} from '../../navigation/nav-utils';
import { assert, isReady } from '../../utils/helpers';
isViewController,
resolveRoute,
setZIndex,
toggleHidden,
transitionFactory
} from './nav-utils';
import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate';
import {
assert,
focusOutActiveElement,
isDef,
isNumber,
} from '../../utils/helpers';
import { buildIOSTransition } from './transitions/transition.ios';
import { buildMdTransition } from './transitions/transition.md';
const queueMap = new Map<number, TransitionInstruction[]>();
/* it is very important to keep this class in sync with ./nav-interface interface */
@Component({
@ -44,12 +78,12 @@ export class Nav implements PublicNav {
isPortal: boolean;
swipeToGoBackTransition: any; // TODO Transition
childNavs?: Nav[];
navController?: NavController;
@Prop() mode: string;
@Prop() root: any;
@Prop() delegate: FrameworkDelegate;
@Prop({ context: 'config' }) config: Config;
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
constructor() {
this.navId = getNextNavId();
@ -212,72 +246,54 @@ export function componentDidLoadImpl(nav: Nav) {
}
}
export function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.push(nav, component, data, opts);
});
export async function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return push(nav, nav.delegate, animation, component, data, opts);
}
export function popImpl(nav: Nav, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.pop(nav, opts);
});
export async function popImpl(nav: Nav, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return pop(nav, nav.delegate, animation, opts);
}
export function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.setRoot(nav, component, data, opts);
});
export async function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return setRoot(nav, nav.delegate, animation, component, data, opts);
}
export function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.insert(nav, insertIndex, page, params, opts);
});
export async function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return insert(nav, nav.delegate, animation, insertIndex, page, params, opts);
}
export function insertPagesImpl(nav: Nav, insertIndex: number, insertPages: any[], opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.insertPages(nav, insertIndex, insertPages, opts);
});
export async function insertPagesImpl(nav: Nav, insertIndex: number, pagesToInsert: any[], opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return insertPages(nav, nav.delegate, animation, insertIndex, pagesToInsert, opts);
}
export function popToRootImpl(nav: Nav, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.popToRoot(nav, opts);
});
export async function popToRootImpl(nav: Nav, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return popToRoot(nav, nav.delegate, animation, opts);
}
export function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.popTo(nav, indexOrViewCtrl, opts);
});
export async function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return popTo(nav, nav.delegate, animation, indexOrViewCtrl, opts);
}
export function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.removeIndex(nav, startIndex, removeCount, opts);
});
export async function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return remove(nav, nav.delegate, animation, startIndex, removeCount, opts);
}
export function removeViewImpl(nav: Nav, viewController: PublicViewController, opts?: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.removeView(nav, viewController as ViewController, opts);
});
export async function removeViewImpl(nav: Nav, viewController: PublicViewController, opts?: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return removeView(nav, nav.delegate, animation, viewController as ViewController, opts);
}
export function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions) {
return getNavController(nav).then(() => {
return nav.navController.setPages(nav, componentDataPairs, opts);
});
}
export function getNavController(nav: Nav): Promise<any> {
if (nav.navController) {
return Promise.resolve();
}
nav.navController = document.querySelector('ion-nav-controller') as any as NavController;
return isReady(nav.navController as any as HTMLElement);
export async function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions) {
const animation = await hydrateAnimationController(nav.animationCtrl);
return setPages(nav, nav.delegate, animation, componentDataPairs, opts);
}
export function canGoBackImpl(nav: Nav) {
@ -296,3 +312,770 @@ export function navInitializedImpl(potentialParent: Nav, event: CustomEvent) {
event.stopPropagation();
}
}
export function hydrateAnimationController(animationController: AnimationController): Promise<Animation> {
return animationController.create();
}
// public api
export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data?: any, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
insertStart: -1,
insertViews: [{page: component, params: data}],
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
insertStart: insertIndex,
insertViews: [{ page: page, params: params }],
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, insertPages: any[], opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
insertStart: insertIndex,
insertViews: insertPages,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeStart: -1,
removeCount: 1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeStart: 1,
removeCount: -1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animation, indexOrViewCtrl: any, opts?: NavOptions, done?: () => void): Promise<any> {
const config: TransitionInstruction = {
removeStart: -1,
removeCount: -1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
};
if (isViewController(indexOrViewCtrl)) {
config.removeView = indexOrViewCtrl;
config.removeStart = 1;
} else if (isNumber(indexOrViewCtrl)) {
config.removeStart = indexOrViewCtrl + 1;
}
return queueTransaction(config, done);
}
export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animation, startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeStart: startIndex,
removeCount: removeCount,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Animation, viewController: ViewController, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeView: viewController,
removeStart: 0,
removeCount: 1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function setRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise<any> {
return setPages(nav, delegate, animation, [{ page: page, params: params }], opts, done);
}
export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, componentDataPars: ComponentDataPair[], opts?: NavOptions, done?: () => void): Promise<any> {
if (!isDef(opts)) {
opts = {};
}
if (opts.animate !== true) {
opts.animate = false;
}
return queueTransaction({
insertStart: 0,
insertViews: componentDataPars,
removeStart: 0,
removeCount: -1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
// private api, exported for testing
export function queueTransaction(ti: TransitionInstruction, done: () => void): Promise<boolean> {
const promise = new Promise<boolean>((resolve, reject) => {
ti.resolve = resolve;
ti.reject = reject;
});
ti.done = done;
if (!ti.delegate) {
ti.delegate = new DomFrameworkDelegate();
}
// Normalize empty
if (ti.insertViews && ti.insertViews.length === 0) {
ti.insertViews = undefined;
}
// Normalize empty
if (ti.insertViews && ti.insertViews.length === 0) {
ti.insertViews = undefined;
}
// Enqueue transition instruction
addToQueue(ti);
// if there isn't a transition already happening
// then this will kick off this transition
nextTransaction(ti.nav);
return promise;
}
export function nextTransaction(nav: Nav): Promise<any> {
if (nav.transitioning) {
return Promise.resolve();
}
const topTransaction = getTopTransaction(nav.navId);
if (!topTransaction) {
return Promise.resolve();
}
let enteringView: ViewController;
let leavingView: ViewController;
return initializeViewBeforeTransition(nav, topTransaction).then(([_enteringView, _leavingView]) => {
enteringView = _enteringView;
leavingView = _leavingView;
return attachViewToDom(nav, enteringView, topTransaction.delegate);
}).then(() => {
return loadViewAndTransition(nav, enteringView, leavingView, topTransaction);
}).then((result: NavResult) => {
nav.ionNavChanged.emit({ isPop: false });
return successfullyTransitioned(result, topTransaction);
}).catch((err: Error) => {
return transitionFailed(err, topTransaction);
});
}
export function successfullyTransitioned(result: NavResult, ti: TransitionInstruction) {
const queue = getQueue(ti.id);
if (!queue) {
// TODO, make throw error in the future
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
}
ti.nav.isViewInitialized = true;
ti.nav.transitionId = null;
ti.nav.transitioning = false;
// TODO - check if it's a swipe back
// kick off next transition for this nav I guess
nextTransaction(ti.nav);
if (ti.done) {
ti.done(
result.hasCompleted,
result.requiresTransition,
result.enteringName,
result.leavingName,
result.direction
);
}
ti.resolve(result.hasCompleted);
}
export function transitionFailed(error: Error, ti: TransitionInstruction) {
const queue = getQueue(ti.nav.navId);
if (!queue) {
// TODO, make throw error in the future
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
}
ti.nav.transitionId = null;
resetQueue(ti.nav.navId);
ti.nav.transitioning = false;
// TODO - check if it's a swipe back
// kick off next transition for this nav I guess
nextTransaction(ti.nav);
fireError(error, ti);
}
export function fireError(error: Error, ti: TransitionInstruction) {
if (ti.done) {
ti.done(false, false, error.message);
}
if (ti.reject && !ti.nav.destroyed) {
ti.reject(error);
} else {
ti.resolve(false);
}
}
export function loadViewAndTransition(nav: Nav, enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
if (!ti.requiresTransition) {
// transition is not required, so we are already done!
// they're inserting/removing the views somewhere in the middle or
// beginning, so visually nothing needs to animate/transition
// resolve immediately because there's no animation that's happening
return Promise.resolve({
hasCompleted: true,
requiresTransition: false
});
}
let transition: Transition = null;
const transitionId = getParentTransitionId(nav);
nav.transitionId = transitionId >= 0 ? transitionId : getNextTransitionId();
// create the transition options
const animationOpts: AnimationOptions = {
animation: ti.opts.animation,
direction: ti.opts.direction,
duration: (ti.opts.animate === false ? 0 : ti.opts.duration),
easing: ti.opts.easing,
isRTL: false, // TODO
ev: ti.opts.event,
};
const emptyTransition = transitionFactory(ti.animation);
transition = getHydratedTransition(animationOpts.animation, nav.config, nav.transitionId, emptyTransition, enteringView, leavingView, animationOpts, getDefaultTransition(nav.config));
if (nav.swipeToGoBackTransition) {
nav.swipeToGoBackTransition.destroy();
nav.swipeToGoBackTransition = null;
}
// it's a swipe to go back transition
if (transition.isRoot() && ti.opts.progressAnimation) {
nav.swipeToGoBackTransition = transition;
}
transition.start();
return executeAsyncTransition(nav, transition, enteringView, leavingView, ti.delegate, ti.opts, ti.nav.config.getBoolean('animate'));
}
// TODO - transition type
export function executeAsyncTransition(nav: Nav, transition: Transition, enteringView: ViewController, leavingView: ViewController, delegate: FrameworkDelegate, opts: NavOptions, configShouldAnimate: boolean): Promise<NavResult> {
assert(nav.transitioning, 'must be transitioning');
nav.transitionId = null;
setZIndex(nav, enteringView, leavingView, opts.direction);
// always ensure the entering view is viewable
// ******** DOM WRITE ****************
// TODO, figure out where we want to read this data from
enteringView && toggleHidden(enteringView.element, true, true);
// always ensure the leaving view is viewable
// ******** DOM WRITE ****************
leavingView && toggleHidden(leavingView.element, true, true);
const isFirstPage = !nav.isViewInitialized && nav.views.length === 1;
const shouldNotAnimate = isFirstPage && !nav.isPortal;
if (configShouldAnimate || shouldNotAnimate) {
opts.animate = false;
}
if (opts.animate === false) {
// if it was somehow set to not animation, then make the duration zero
transition.duration(0);
}
transition.beforeAddRead(() => {
fireViewWillLifecycles(enteringView, leavingView);
});
// get the set duration of this transition
const duration = transition.getDuration();
// create a callback for when the animation is done
const transitionCompletePromise = new Promise(resolve => {
transition.onFinish(resolve);
});
if (transition.isRoot()) {
if (duration > DISABLE_APP_MINIMUM_DURATION && opts.disableApp !== false) {
// if this transition has a duration and this is the root transition
// then set that the app is actively disabled
// this._app.setEnabled(false, duration + ACTIVE_TRANSITION_OFFSET, opts.minClickBlockDuration);
// TODO - figure out how to disable the app
}
if (opts.progressAnimation) {
// this is a swipe to go back, just get the transition progress ready
// kick off the swipe animation start
transition.progressStart();
} else {
// only the top level transition should actually start "play"
// kick it off and let it play through
// ******** DOM WRITE ****************
transition.play();
}
}
return transitionCompletePromise.then(() => {
return transitionFinish(nav, transition, delegate, opts);
});
}
export function transitionFinish(nav: Nav, transition: Transition, delegate: FrameworkDelegate, opts: NavOptions): Promise<NavResult> {
let promise: Promise<any> = null;
if (transition.hasCompleted) {
transition.enteringView && transition.enteringView.didEnter();
transition.leavingView && transition.leavingView.didLeave();
promise = cleanUpView(nav, delegate, transition.enteringView);
} else {
promise = cleanUpView(nav, delegate, transition.leavingView);
}
return promise.then(() => {
if (transition.isRoot()) {
destroyTransition(transition.transitionId);
// TODO - enable app
nav.transitioning = false;
// TODO - navChange on the deep linker used to be called here
if (opts.keyboardClose !== false) {
focusOutActiveElement();
}
}
return {
hasCompleted: transition.hasCompleted,
requiresTransition: true,
direction: opts.direction
};
});
}
export function cleanUpView(nav: Nav, delegate: FrameworkDelegate, activeViewController: ViewController): Promise<any> {
if (nav.destroyed) {
return Promise.resolve();
}
const activeIndex = nav.views.indexOf(activeViewController);
const promises: Promise<any>[] = [];
for (let i = nav.views.length - 1; i >= 0; i--) {
const inactiveViewController = nav.views[i];
if (i > activeIndex) {
// this view comes after the active view
inactiveViewController.willUnload();
promises.push(destroyView(nav, delegate, inactiveViewController));
} else if ( i < activeIndex && !nav.isPortal) {
// this view comes before the active view
// and it is not a portal then ensure it is hidden
toggleHidden(inactiveViewController.element, true, false);
}
// TODO - review existing z index code!
}
return Promise.all(promises);
}
export function fireViewWillLifecycles(enteringView: ViewController, leavingView: ViewController) {
leavingView && leavingView.willLeave(!enteringView);
enteringView && enteringView.willEnter();
}
export function attachViewToDom(nav: Nav, enteringView: ViewController, delegate: FrameworkDelegate) {
if (enteringView && enteringView.state === STATE_NEW) {
return delegate.attachViewToDom(nav.element, enteringView.component, enteringView.data, ['ion-page']).then((mountingData) => {
Object.assign(enteringView, mountingData);
enteringView.state = STATE_ATTACHED;
});
}
// it's in the wrong state, so don't attach and just return
return Promise.resolve();
}
export function initializeViewBeforeTransition(nav: Nav, ti: TransitionInstruction): Promise<ViewController[]> {
let leavingView: ViewController = null;
let enteringView: ViewController = null;
return startTransaction(ti).then(() => {
const viewControllers = convertComponentToViewController(nav, ti);
ti.insertViews = viewControllers;
leavingView = ti.nav.getActive() as ViewController;
enteringView = getEnteringView(ti, ti.nav, leavingView);
if (!leavingView && !enteringView) {
return Promise.reject(new Error('No views in the stack to remove'));
}
// mark state as initialized
// enteringView.state = STATE_INITIALIZED;
ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView;
return testIfViewsCanLeaveAndEnter(enteringView, leavingView, ti);
}).then(() => {
return updateNavStacks(enteringView, leavingView, ti);
}).then(() => {
return [enteringView, leavingView];
});
}
// called _postViewInit in old world
export function updateNavStacks(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise<any> {
return Promise.resolve().then(() => {
assert(!!(leavingView || enteringView), 'Both leavingView and enteringView are null');
assert(!!ti.resolve, 'resolve must be valid');
assert(!!ti.reject, 'reject must be valid');
const destroyQueue: ViewController[] = [];
ti.opts = ti.opts || {};
if (isDef(ti.removeStart)) {
assert(ti.removeStart >= 0, 'removeStart can not be negative');
assert(ti.removeStart >= 0, 'removeCount can not be negative');
for (let i = 0; i < ti.removeCount; i++) {
const view = ti.nav.views[i + ti.removeStart];
if (view && view !== enteringView && view !== leavingView) {
destroyQueue.push(view);
}
}
ti.opts.direction = ti.opts.direction || DIRECTION_BACK;
}
const finalBalance = ti.nav.views.length + (ti.insertViews ? ti.insertViews.length : 0) - (ti.removeCount ? ti.removeCount : 0);
assert(finalBalance >= 0, 'final balance can not be negative');
if (finalBalance === 0 && !ti.nav.isPortal) {
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`);
throw new Error('Navigation stack needs at least one root page');
}
// At this point the transition can not be rejected, any throw should be an error
// there are views to insert
if (ti.insertViews) {
// manually set the new view's id if an id was passed in the options
if (isDef(ti.opts.id)) {
enteringView.id = ti.opts.id;
}
// add the views to the stack
for (let i = 0; i < ti.insertViews.length; i++) {
insertViewIntoNav(ti.nav, ti.insertViews[i], ti.insertStart + i);
}
if (ti.enteringRequiresTransition) {
// default to forward if not already set
ti.opts.direction = ti.opts.direction || DIRECTION_FORWARD;
}
}
// if the views to be removed are in the beginning or middle
// and there is not a view that needs to visually transition out
// then just destroy them and don't transition anything
// batch all of lifecycles together
if (destroyQueue && destroyQueue.length) {
// TODO, figure out how the zone stuff should work in angular
for (let i = 0; i < destroyQueue.length; i++) {
const view = destroyQueue[i];
view.willLeave(true);
view.didLeave();
view.willUnload();
}
const destroyQueuePromises: Promise<any>[] = [];
for (const viewController of destroyQueue) {
destroyQueuePromises.push(destroyView(ti.nav, ti.delegate, viewController));
}
return Promise.all(destroyQueuePromises);
}
return null;
}).then(() => {
// set which animation it should use if it wasn't set yet
if (ti.requiresTransition && !ti.opts.animation) {
if (isDef(ti.removeStart)) {
ti.opts.animation = (leavingView || enteringView).getTransitionName(ti.opts.direction);
} else {
ti.opts.animation = (enteringView || leavingView).getTransitionName(ti.opts.direction);
}
}
});
}
export function destroyView(nav: Nav, delegate: FrameworkDelegate, viewController: ViewController) {
return viewController.destroy(delegate).then(() => {
return removeViewFromList(nav, viewController);
});
}
export function removeViewFromList(nav: Nav, viewController: ViewController) {
assert(viewController.state === STATE_ATTACHED || viewController.state === STATE_DESTROYED, 'view state should be loaded or destroyed');
const index = nav.views.indexOf(viewController);
assert(index > -1, 'view must be part of the stack');
if (index >= 0) {
nav.views.splice(index, 1);
}
}
export function insertViewIntoNav(nav: Nav, view: ViewController, index: number) {
const existingIndex = nav.views.indexOf(view);
if (existingIndex > -1) {
// this view is already in the stack!!
// move it to its new location
assert(view.nav === nav, 'view is not part of the nav');
nav.views.splice(index, 0, nav.views.splice(existingIndex, 1)[0]);
} else {
assert(!view.nav || (nav.isPortal && view.nav === nav), 'nav is used');
// this is a new view to add to the stack
// create the new entering view
view.nav = nav;
// give this inserted view an ID
viewIds++;
if (!view.id) {
view.id = `${nav.navId}-${viewIds}`;
}
// insert the entering view into the correct index in the stack
nav.views.splice(index, 0, view);
}
}
export function testIfViewsCanLeaveAndEnter(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
if (!ti.requiresTransition) {
return Promise.resolve();
}
const promises: Promise<any>[] = [];
if (leavingView) {
promises.push(lifeCycleTest(leavingView, 'Leave'));
}
if (enteringView) {
promises.push(lifeCycleTest(enteringView, 'Enter'));
}
if (promises.length === 0) {
return Promise.resolve();
}
// darn, async promises, gotta wait for them to resolve
return Promise.all(promises).then((values: any[]) => {
if (values.some(result => result === false)) {
ti.reject = null;
throw new Error('canEnter/Leave returned false');
}
});
}
export function lifeCycleTest(viewController: ViewController, enterOrLeave: string) {
const methodName = `ionViewCan${enterOrLeave}`;
if (viewController.instance && viewController.instance[methodName]) {
try {
const result = viewController.instance[methodName];
if (result instanceof Promise) {
return result;
}
return Promise.resolve(result !== false);
} catch (e) {
return Promise.reject(new Error(`Unexpected error when calling ${methodName}: ${e.message}`));
}
}
return Promise.resolve(true);
}
export function startTransaction(ti: TransitionInstruction): Promise<any> {
const viewsLength = ti.nav.views ? ti.nav.views.length : 0;
if (isDef(ti.removeView)) {
assert(isDef(ti.removeStart), 'removeView needs removeStart');
assert(isDef(ti.removeCount), 'removeView needs removeCount');
const index = ti.nav.views.indexOf(ti.removeView());
if (index < 0) {
return Promise.reject(new Error('The removeView was not found'));
}
ti.removeStart += index;
}
if (isDef(ti.removeStart)) {
if (ti.removeStart < 0) {
ti.removeStart = (viewsLength - 1);
}
if (ti.removeCount < 0) {
ti.removeCount = (viewsLength - ti.removeStart);
}
ti.leavingRequiresTransition = (ti.removeCount > 0) && ((ti.removeStart + ti.removeCount) === viewsLength);
}
if (isDef(ti.insertViews)) {
// allow -1 to be passed in to auto push it on the end
// and clean up the index if it's larger then the size of the stack
if (ti.insertStart < 0 || ti.insertStart > viewsLength) {
ti.insertStart = viewsLength;
}
ti.enteringRequiresTransition = (ti.insertStart === viewsLength);
}
ti.nav.transitioning = true;
return Promise.resolve();
}
export function getEnteringView(ti: TransitionInstruction, nav: Nav, leavingView: ViewController): ViewController {
if (ti.insertViews && ti.insertViews.length) {
// grab the very last view of the views to be inserted
// and initialize it as the new entering view
return ti.insertViews[ti.insertViews.length - 1];
}
if (isDef(ti.removeStart)) {
var removeEnd = ti.removeStart + ti.removeCount;
for (let i = nav.views.length - 1; i >= 0; i--) {
if ((i < ti.removeStart || i >= removeEnd) && nav.views[i] !== leavingView) {
return nav.views[i];
}
}
}
return null;
}
export function convertViewsToViewControllers(views: any[]): ViewController[] {
return views.map(view => {
if (view) {
if (isViewController(view)) {
return view as ViewController;
}
return new ViewController(view.page, view.params);
}
return null;
}).filter(view => !!view);
}
export function convertComponentToViewController(nav: Nav, ti: TransitionInstruction): ViewController[] {
if (ti.insertViews) {
assert(ti.insertViews.length > 0, 'length can not be zero');
const viewControllers = convertViewsToViewControllers(ti.insertViews);
assert(ti.insertViews.length === viewControllers.length, 'lengths does not match');
if (viewControllers.length === 0) {
throw new Error('No views to insert');
}
for (const viewController of viewControllers) {
if (viewController.nav && viewController.nav.navId !== ti.id) {
throw new Error('The view has already inserted into a different nav');
}
if (viewController.state === STATE_DESTROYED) {
throw new Error('The view has already been destroyed');
}
if (nav.useRouter && !resolveRoute(nav, viewController.component)) {
throw new Error('Route not specified for ' + viewController.component);
}
}
return viewControllers;
}
return [];
}
export function addToQueue(ti: TransitionInstruction) {
const list = queueMap.get(ti.id) || [];
list.push(ti);
queueMap.set(ti.id, list);
}
export function getQueue(id: number) {
return queueMap.get(id) || [];
}
export function resetQueue(id: number) {
queueMap.set(id, []);
}
export function getTopTransaction(id: number) {
const queue = getQueue(id);
if (!queue.length) {
return null;
}
const tmp = queue.concat();
const toReturn = tmp.shift();
queueMap.set(id, tmp);
return toReturn;
}
export function getDefaultTransition(config: Config) {
return config.get('mode') === 'md' ? buildMdTransition : buildIOSTransition;
}
let viewIds = VIEW_ID_START;
const DISABLE_APP_MINIMUM_DURATION = 64;

View File

@ -1,6 +1,6 @@
import { AnimationOptions, Transition, ViewController } from '../../index';
import { AnimationOptions, Transition, ViewController } from '../../../index';
import { canNavGoBack } from '../nav-utils';
import { isDef } from '../../utils/helpers';
import { isDef } from '../../../utils/helpers';
const DURATION = 500;
const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';

View File

@ -1,6 +1,6 @@
import { AnimationOptions, Transition, ViewController } from '../../index';
import { AnimationOptions, Transition, ViewController } from '../../../index';
import { canNavGoBack } from '../nav-utils';
import { isDef } from '../../utils/helpers';
import { isDef } from '../../../utils/helpers';
const TRANSLATEY = 'translateY';
const OFF_BOTTOM = '40px';

View File

@ -1,7 +1,7 @@
import { FrameworkDelegate, Nav, PublicViewController } from '../index';
import { FrameworkDelegate, Nav, PublicViewController } from '../../index';
import { STATE_ATTACHED, STATE_DESTROYED, STATE_INITIALIZED, STATE_NEW } from './nav-utils';
import { assert } from '../utils/helpers';
import { assert } from '../../utils/helpers';
export class ViewController implements PublicViewController {

View File

@ -65,9 +65,9 @@ export {
export * from './components/modal/modal';
export { ModalController } from './components/modal-controller/modal-controller';
export { Nav } from './components/nav/nav';
export { PublicNav } from './components/nav/nav-interface';
export * from './components/nav/nav-interfaces';
export { ViewController } from './components/nav/view-controller';
export { Navbar } from './components/navbar/navbar';
export { NavController } from './components/nav-controller/nav-controller';
export { Note } from './components/note/note';
export { Page } from './components/page/page';
export { PickerColumnCmp } from './components/picker-column/picker-column';
@ -110,9 +110,6 @@ export { ToastController } from './components/toast-controller/toast-controller'
export { Toggle } from './components/toggle/toggle';
export { Toolbar } from './components/toolbar/toolbar';
export * from './navigation/nav-interfaces';
export { ViewController } from './navigation/view-controller';
// export all of the component declarations that are dynamically created
export * from './components';
@ -152,3 +149,12 @@ export interface OverlayDismissEventDetail {
export interface OverlayController {
create(): HTMLElement;
}
export interface FrameworkDelegate {
attachViewToDom(elementOrContainerToMountTo: any, elementOrComponentToMount: any, propsOrDataObj?: any, classesToAdd?: string[], escapeHatch?: any): Promise<FrameworkMountingData>;
removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any, escapeHatch?: any): Promise<FrameworkMountingData>;
}
export interface FrameworkMountingData {
element: HTMLElement;
}

View File

@ -1,771 +0,0 @@
import { Animation, AnimationOptions, Config, FrameworkDelegate, Nav, NavOptions, Transition} from '../index';
import { ComponentDataPair, NavResult, TransitionInstruction } from './nav-interfaces';
import { DIRECTION_BACK, DIRECTION_FORWARD, STATE_ATTACHED, STATE_DESTROYED, STATE_NEW, VIEW_ID_START, destroyTransition, getHydratedTransition, getNextTransitionId, getParentTransitionId, isViewController, resolveRoute, setZIndex, toggleHidden, transitionFactory } from './nav-utils';
import { ViewController } from './view-controller';
import { assert, focusOutActiveElement, isDef, isNumber } from '../utils/helpers';
import { buildIOSTransition } from './transitions/transition.ios';
import { buildMdTransition } from './transitions/transition.md';
const queueMap = new Map<number, TransitionInstruction[]>();
// public api
export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data?: any, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
insertStart: -1,
insertViews: [{page: component, params: data}],
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
insertStart: insertIndex,
insertViews: [{ page: page, params: params }],
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, insertPages: any[], opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
insertStart: insertIndex,
insertViews: insertPages,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeStart: -1,
removeCount: 1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeStart: 1,
removeCount: -1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animation, indexOrViewCtrl: any, opts?: NavOptions, done?: () => void): Promise<any> {
const config: TransitionInstruction = {
removeStart: -1,
removeCount: -1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
};
if (isViewController(indexOrViewCtrl)) {
config.removeView = indexOrViewCtrl;
config.removeStart = 1;
} else if (isNumber(indexOrViewCtrl)) {
config.removeStart = indexOrViewCtrl + 1;
}
return queueTransaction(config, done);
}
export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animation, startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeStart: startIndex,
removeCount: removeCount,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Animation, viewController: ViewController, opts?: NavOptions, done?: () => void): Promise<any> {
return queueTransaction({
removeView: viewController,
removeStart: 0,
removeCount: 1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
export function setRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise<any> {
return setPages(nav, delegate, animation, [{ page: page, params: params }], opts, done);
}
export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, componentDataPars: ComponentDataPair[], opts?: NavOptions, done?: () => void): Promise<any> {
if (!isDef(opts)) {
opts = {};
}
if (opts.animate !== true) {
opts.animate = false;
}
return queueTransaction({
insertStart: 0,
insertViews: componentDataPars,
removeStart: 0,
removeCount: -1,
opts: opts,
nav: nav,
delegate: delegate,
id: nav.navId,
animation: animation
}, done);
}
// private api, exported for testing
export function queueTransaction(ti: TransitionInstruction, done: () => void): Promise<boolean> {
const promise = new Promise<boolean>((resolve, reject) => {
ti.resolve = resolve;
ti.reject = reject;
});
ti.done = done;
// Normalize empty
if (ti.insertViews && ti.insertViews.length === 0) {
ti.insertViews = undefined;
}
// Normalize empty
if (ti.insertViews && ti.insertViews.length === 0) {
ti.insertViews = undefined;
}
// Enqueue transition instruction
addToQueue(ti);
// if there isn't a transition already happening
// then this will kick off this transition
nextTransaction(ti.nav);
return promise;
}
export function nextTransaction(nav: Nav): Promise<any> {
if (nav.transitioning) {
return Promise.resolve();
}
const topTransaction = getTopTransaction(nav.navId);
if (!topTransaction) {
return Promise.resolve();
}
let enteringView: ViewController;
let leavingView: ViewController;
return initializeViewBeforeTransition(nav, topTransaction).then(([_enteringView, _leavingView]) => {
enteringView = _enteringView;
leavingView = _leavingView;
return attachViewToDom(nav, enteringView, topTransaction.delegate);
}).then(() => {
return loadViewAndTransition(nav, enteringView, leavingView, topTransaction);
}).then((result: NavResult) => {
nav.ionNavChanged.emit({ isPop: false });
return successfullyTransitioned(result, topTransaction);
}).catch((err: Error) => {
return transitionFailed(err, topTransaction);
});
}
export function successfullyTransitioned(result: NavResult, ti: TransitionInstruction) {
const queue = getQueue(ti.id);
if (!queue) {
// TODO, make throw error in the future
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
}
ti.nav.isViewInitialized = true;
ti.nav.transitionId = null;
ti.nav.transitioning = false;
// TODO - check if it's a swipe back
// kick off next transition for this nav I guess
nextTransaction(ti.nav);
if (ti.done) {
ti.done(
result.hasCompleted,
result.requiresTransition,
result.enteringName,
result.leavingName,
result.direction
);
}
ti.resolve(result.hasCompleted);
}
export function transitionFailed(error: Error, ti: TransitionInstruction) {
const queue = getQueue(ti.nav.navId);
if (!queue) {
// TODO, make throw error in the future
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
}
ti.nav.transitionId = null;
resetQueue(ti.nav.navId);
ti.nav.transitioning = false;
// TODO - check if it's a swipe back
// kick off next transition for this nav I guess
nextTransaction(ti.nav);
fireError(error, ti);
}
export function fireError(error: Error, ti: TransitionInstruction) {
if (ti.done) {
ti.done(false, false, error.message);
}
if (ti.reject && !ti.nav.destroyed) {
ti.reject(error);
} else {
ti.resolve(false);
}
}
export function loadViewAndTransition(nav: Nav, enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
if (!ti.requiresTransition) {
// transition is not required, so we are already done!
// they're inserting/removing the views somewhere in the middle or
// beginning, so visually nothing needs to animate/transition
// resolve immediately because there's no animation that's happening
return Promise.resolve({
hasCompleted: true,
requiresTransition: false
});
}
let transition: Transition = null;
const transitionId = getParentTransitionId(nav);
nav.transitionId = transitionId >= 0 ? transitionId : getNextTransitionId();
// create the transition options
const animationOpts: AnimationOptions = {
animation: ti.opts.animation,
direction: ti.opts.direction,
duration: (ti.opts.animate === false ? 0 : ti.opts.duration),
easing: ti.opts.easing,
isRTL: false, // TODO
ev: ti.opts.event,
};
const emptyTransition = transitionFactory(ti.animation);
transition = getHydratedTransition(animationOpts.animation, nav.config, nav.transitionId, emptyTransition, enteringView, leavingView, animationOpts, getDefaultTransition(nav.config));
if (nav.swipeToGoBackTransition) {
nav.swipeToGoBackTransition.destroy();
nav.swipeToGoBackTransition = null;
}
// it's a swipe to go back transition
if (transition.isRoot() && ti.opts.progressAnimation) {
nav.swipeToGoBackTransition = transition;
}
transition.start();
return executeAsyncTransition(nav, transition, enteringView, leavingView, ti.delegate, ti.opts, ti.nav.config.getBoolean('animate'));
}
// TODO - transition type
export function executeAsyncTransition(nav: Nav, transition: Transition, enteringView: ViewController, leavingView: ViewController, delegate: FrameworkDelegate, opts: NavOptions, configShouldAnimate: boolean): Promise<NavResult> {
assert(nav.transitioning, 'must be transitioning');
nav.transitionId = null;
setZIndex(nav, enteringView, leavingView, opts.direction);
// always ensure the entering view is viewable
// ******** DOM WRITE ****************
// TODO, figure out where we want to read this data from
enteringView && toggleHidden(enteringView.element, true, true);
// always ensure the leaving view is viewable
// ******** DOM WRITE ****************
leavingView && toggleHidden(leavingView.element, true, true);
const isFirstPage = !nav.isViewInitialized && nav.views.length === 1;
const shouldNotAnimate = isFirstPage && !nav.isPortal;
if (configShouldAnimate || shouldNotAnimate) {
opts.animate = false;
}
if (opts.animate === false) {
// if it was somehow set to not animation, then make the duration zero
transition.duration(0);
}
transition.beforeAddRead(() => {
fireViewWillLifecycles(enteringView, leavingView);
});
// get the set duration of this transition
const duration = transition.getDuration();
// create a callback for when the animation is done
const transitionCompletePromise = new Promise(resolve => {
transition.onFinish(resolve);
});
if (transition.isRoot()) {
if (duration > DISABLE_APP_MINIMUM_DURATION && opts.disableApp !== false) {
// if this transition has a duration and this is the root transition
// then set that the app is actively disabled
// this._app.setEnabled(false, duration + ACTIVE_TRANSITION_OFFSET, opts.minClickBlockDuration);
// TODO - figure out how to disable the app
}
if (opts.progressAnimation) {
// this is a swipe to go back, just get the transition progress ready
// kick off the swipe animation start
transition.progressStart();
} else {
// only the top level transition should actually start "play"
// kick it off and let it play through
// ******** DOM WRITE ****************
transition.play();
}
}
return transitionCompletePromise.then(() => {
return transitionFinish(nav, transition, delegate, opts);
});
}
export function transitionFinish(nav: Nav, transition: Transition, delegate: FrameworkDelegate, opts: NavOptions): Promise<NavResult> {
let promise: Promise<any> = null;
if (transition.hasCompleted) {
transition.enteringView && transition.enteringView.didEnter();
transition.leavingView && transition.leavingView.didLeave();
promise = cleanUpView(nav, delegate, transition.enteringView);
} else {
promise = cleanUpView(nav, delegate, transition.leavingView);
}
return promise.then(() => {
if (transition.isRoot()) {
destroyTransition(transition.transitionId);
// TODO - enable app
nav.transitioning = false;
// TODO - navChange on the deep linker used to be called here
if (opts.keyboardClose !== false) {
focusOutActiveElement();
}
}
return {
hasCompleted: transition.hasCompleted,
requiresTransition: true,
direction: opts.direction
};
});
}
export function cleanUpView(nav: Nav, delegate: FrameworkDelegate, activeViewController: ViewController): Promise<any> {
if (nav.destroyed) {
return Promise.resolve();
}
const activeIndex = nav.views.indexOf(activeViewController);
const promises: Promise<any>[] = [];
for (let i = nav.views.length - 1; i >= 0; i--) {
const inactiveViewController = nav.views[i];
if (i > activeIndex) {
// this view comes after the active view
inactiveViewController.willUnload();
promises.push(destroyView(nav, delegate, inactiveViewController));
} else if ( i < activeIndex && !nav.isPortal) {
// this view comes before the active view
// and it is not a portal then ensure it is hidden
toggleHidden(inactiveViewController.element, true, false);
}
// TODO - review existing z index code!
}
return Promise.all(promises);
}
export function fireViewWillLifecycles(enteringView: ViewController, leavingView: ViewController) {
leavingView && leavingView.willLeave(!enteringView);
enteringView && enteringView.willEnter();
}
export function attachViewToDom(nav: Nav, enteringView: ViewController, delegate: FrameworkDelegate) {
if (enteringView && enteringView.state === STATE_NEW) {
return delegate.attachViewToDom(nav.element, enteringView.component, enteringView.data, ['ion-page']).then((mountingData) => {
Object.assign(enteringView, mountingData);
enteringView.state = STATE_ATTACHED;
});
}
// it's in the wrong state, so don't attach and just return
return Promise.resolve();
}
export function initializeViewBeforeTransition(nav: Nav, ti: TransitionInstruction): Promise<ViewController[]> {
let leavingView: ViewController = null;
let enteringView: ViewController = null;
return startTransaction(ti).then(() => {
const viewControllers = convertComponentToViewController(nav, ti);
ti.insertViews = viewControllers;
leavingView = ti.nav.getActive() as ViewController;
enteringView = getEnteringView(ti, ti.nav, leavingView);
if (!leavingView && !enteringView) {
return Promise.reject(new Error('No views in the stack to remove'));
}
// mark state as initialized
// enteringView.state = STATE_INITIALIZED;
ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView;
return testIfViewsCanLeaveAndEnter(enteringView, leavingView, ti);
}).then(() => {
return updateNavStacks(enteringView, leavingView, ti);
}).then(() => {
return [enteringView, leavingView];
});
}
// called _postViewInit in old world
export function updateNavStacks(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise<any> {
return Promise.resolve().then(() => {
assert(!!(leavingView || enteringView), 'Both leavingView and enteringView are null');
assert(!!ti.resolve, 'resolve must be valid');
assert(!!ti.reject, 'reject must be valid');
const destroyQueue: ViewController[] = [];
ti.opts = ti.opts || {};
if (isDef(ti.removeStart)) {
assert(ti.removeStart >= 0, 'removeStart can not be negative');
assert(ti.removeStart >= 0, 'removeCount can not be negative');
for (let i = 0; i < ti.removeCount; i++) {
const view = ti.nav.views[i + ti.removeStart];
if (view && view !== enteringView && view !== leavingView) {
destroyQueue.push(view);
}
}
ti.opts.direction = ti.opts.direction || DIRECTION_BACK;
}
const finalBalance = ti.nav.views.length + (ti.insertViews ? ti.insertViews.length : 0) - (ti.removeCount ? ti.removeCount : 0);
assert(finalBalance >= 0, 'final balance can not be negative');
if (finalBalance === 0 && !ti.nav.isPortal) {
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`);
throw new Error('Navigation stack needs at least one root page');
}
// At this point the transition can not be rejected, any throw should be an error
// there are views to insert
if (ti.insertViews) {
// manually set the new view's id if an id was passed in the options
if (isDef(ti.opts.id)) {
enteringView.id = ti.opts.id;
}
// add the views to the stack
for (let i = 0; i < ti.insertViews.length; i++) {
insertViewIntoNav(ti.nav, ti.insertViews[i], ti.insertStart + i);
}
if (ti.enteringRequiresTransition) {
// default to forward if not already set
ti.opts.direction = ti.opts.direction || DIRECTION_FORWARD;
}
}
// if the views to be removed are in the beginning or middle
// and there is not a view that needs to visually transition out
// then just destroy them and don't transition anything
// batch all of lifecycles together
if (destroyQueue && destroyQueue.length) {
// TODO, figure out how the zone stuff should work in angular
for (let i = 0; i < destroyQueue.length; i++) {
const view = destroyQueue[i];
view.willLeave(true);
view.didLeave();
view.willUnload();
}
const destroyQueuePromises: Promise<any>[] = [];
for (const viewController of destroyQueue) {
destroyQueuePromises.push(destroyView(ti.nav, ti.delegate, viewController));
}
return Promise.all(destroyQueuePromises);
}
return null;
}).then(() => {
// set which animation it should use if it wasn't set yet
if (ti.requiresTransition && !ti.opts.animation) {
if (isDef(ti.removeStart)) {
ti.opts.animation = (leavingView || enteringView).getTransitionName(ti.opts.direction);
} else {
ti.opts.animation = (enteringView || leavingView).getTransitionName(ti.opts.direction);
}
}
});
}
export function destroyView(nav: Nav, delegate: FrameworkDelegate, viewController: ViewController) {
return viewController.destroy(delegate).then(() => {
return removeViewFromList(nav, viewController);
});
}
export function removeViewFromList(nav: Nav, viewController: ViewController) {
assert(viewController.state === STATE_ATTACHED || viewController.state === STATE_DESTROYED, 'view state should be loaded or destroyed');
const index = nav.views.indexOf(viewController);
assert(index > -1, 'view must be part of the stack');
if (index >= 0) {
nav.views.splice(index, 1);
}
}
export function insertViewIntoNav(nav: Nav, view: ViewController, index: number) {
const existingIndex = nav.views.indexOf(view);
if (existingIndex > -1) {
// this view is already in the stack!!
// move it to its new location
assert(view.nav === nav, 'view is not part of the nav');
nav.views.splice(index, 0, nav.views.splice(existingIndex, 1)[0]);
} else {
assert(!view.nav || (nav.isPortal && view.nav === nav), 'nav is used');
// this is a new view to add to the stack
// create the new entering view
view.nav = nav;
// give this inserted view an ID
viewIds++;
if (!view.id) {
view.id = `${nav.navId}-${viewIds}`;
}
// insert the entering view into the correct index in the stack
nav.views.splice(index, 0, view);
}
}
export function testIfViewsCanLeaveAndEnter(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
if (!ti.requiresTransition) {
return Promise.resolve();
}
const promises: Promise<any>[] = [];
if (leavingView) {
promises.push(lifeCycleTest(leavingView, 'Leave'));
}
if (enteringView) {
promises.push(lifeCycleTest(enteringView, 'Enter'));
}
if (promises.length === 0) {
return Promise.resolve();
}
// darn, async promises, gotta wait for them to resolve
return Promise.all(promises).then((values: any[]) => {
if (values.some(result => result === false)) {
ti.reject = null;
throw new Error('canEnter/Leave returned false');
}
});
}
export function lifeCycleTest(viewController: ViewController, enterOrLeave: string) {
const methodName = `ionViewCan${enterOrLeave}`;
if (viewController.instance && viewController.instance[methodName]) {
try {
const result = viewController.instance[methodName];
if (result instanceof Promise) {
return result;
}
return Promise.resolve(result !== false);
} catch (e) {
return Promise.reject(new Error(`Unexpected error when calling ${methodName}: ${e.message}`));
}
}
return Promise.resolve(true);
}
export function startTransaction(ti: TransitionInstruction): Promise<any> {
const viewsLength = ti.nav.views ? ti.nav.views.length : 0;
if (isDef(ti.removeView)) {
assert(isDef(ti.removeStart), 'removeView needs removeStart');
assert(isDef(ti.removeCount), 'removeView needs removeCount');
const index = ti.nav.views.indexOf(ti.removeView());
if (index < 0) {
return Promise.reject(new Error('The removeView was not found'));
}
ti.removeStart += index;
}
if (isDef(ti.removeStart)) {
if (ti.removeStart < 0) {
ti.removeStart = (viewsLength - 1);
}
if (ti.removeCount < 0) {
ti.removeCount = (viewsLength - ti.removeStart);
}
ti.leavingRequiresTransition = (ti.removeCount > 0) && ((ti.removeStart + ti.removeCount) === viewsLength);
}
if (isDef(ti.insertViews)) {
// allow -1 to be passed in to auto push it on the end
// and clean up the index if it's larger then the size of the stack
if (ti.insertStart < 0 || ti.insertStart > viewsLength) {
ti.insertStart = viewsLength;
}
ti.enteringRequiresTransition = (ti.insertStart === viewsLength);
}
ti.nav.transitioning = true;
return Promise.resolve();
}
export function getEnteringView(ti: TransitionInstruction, nav: Nav, leavingView: ViewController): ViewController {
if (ti.insertViews && ti.insertViews.length) {
// grab the very last view of the views to be inserted
// and initialize it as the new entering view
return ti.insertViews[ti.insertViews.length - 1];
}
if (isDef(ti.removeStart)) {
var removeEnd = ti.removeStart + ti.removeCount;
for (let i = nav.views.length - 1; i >= 0; i--) {
if ((i < ti.removeStart || i >= removeEnd) && nav.views[i] !== leavingView) {
return nav.views[i];
}
}
}
return null;
}
export function convertViewsToViewControllers(views: any[]): ViewController[] {
return views.map(view => {
if (view) {
if (isViewController(view)) {
return view as ViewController;
}
return new ViewController(view.page, view.params);
}
return null;
}).filter(view => !!view);
}
export function convertComponentToViewController(nav: Nav, ti: TransitionInstruction): ViewController[] {
if (ti.insertViews) {
assert(ti.insertViews.length > 0, 'length can not be zero');
const viewControllers = convertViewsToViewControllers(ti.insertViews);
assert(ti.insertViews.length === viewControllers.length, 'lengths does not match');
if (viewControllers.length === 0) {
throw new Error('No views to insert');
}
for (const viewController of viewControllers) {
if (viewController.nav && viewController.nav.navId !== ti.id) {
throw new Error('The view has already inserted into a different nav');
}
if (viewController.state === STATE_DESTROYED) {
throw new Error('The view has already been destroyed');
}
if (nav.useRouter && !resolveRoute(nav, viewController.component)) {
throw new Error('Route not specified for ' + viewController.component);
}
}
return viewControllers;
}
return [];
}
export function addToQueue(ti: TransitionInstruction) {
const list = queueMap.get(ti.id) || [];
list.push(ti);
queueMap.set(ti.id, list);
}
export function getQueue(id: number) {
return queueMap.get(id) || [];
}
export function resetQueue(id: number) {
queueMap.set(id, []);
}
export function getTopTransaction(id: number) {
const queue = getQueue(id);
if (!queue.length) {
return null;
}
const tmp = queue.concat();
const toReturn = tmp.shift();
queueMap.set(id, tmp);
return toReturn;
}
export function getDefaultTransition(config: Config) {
return config.get('mode') === 'md' ? buildMdTransition : buildIOSTransition;
}
let viewIds = VIEW_ID_START;
const DISABLE_APP_MINIMUM_DURATION = 64;

View File

@ -1,9 +1,9 @@
import { FrameworkDelegate, FrameworkMountingData, } from '../../index';
import { isString } from '../../utils/helpers';
import { FrameworkDelegate, FrameworkMountingData, } from '../index';
import { isString } from './helpers';
export class DomFrameworkDelegate implements FrameworkDelegate {
attachViewToDom(parentElement: HTMLElement, tagOrElement: string | HTMLElement, propsOrDataObj: any = {}, classesToAdd: string[] = []): Promise<FrameworkMountingData> {
attachViewToDom(parentElement: HTMLElement, tagOrElement: string | HTMLElement, propsOrDataObj: any = {}, classesToAdd: string[] = []): Promise<FrameworkMountingData> {
return new Promise((resolve) => {
const usersElement = (isString(tagOrElement) ? document.createElement(tagOrElement) : tagOrElement) as HTMLElement;
@ -21,7 +21,6 @@ export class DomFrameworkDelegate implements FrameworkDelegate {
}
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement): Promise<FrameworkMountingData> {
parentElement.removeChild(childElement);
return Promise.resolve({
element: null

View File

@ -38,7 +38,7 @@ exports.config = {
{ components: ['ion-range', 'ion-range-knob']},
{ components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button', 'ion-tab-highlight'] },
{ components: ['ion-toggle'] },
{ components: ['ion-nav', 'ion-nav-controller'] },
{ components: ['ion-nav'] },
{ components: ['ion-toast', 'ion-toast-controller'] },
],
collections: [

View File

@ -9,7 +9,8 @@ const routes: Routes = [
{ path: 'alert', loadChildren: 'app/alert/alert.module#AlertModule' },
{ path: 'actionSheet', loadChildren: 'app/action-sheet/action-sheet.module#ActionSheetModule' },
{ path: 'toast', loadChildren: 'app/toast/toast.module#ToastModule' },
{ path: 'loading', loadChildren: 'app/loading/loading.module#LoadingModule' }
{ path: 'loading', loadChildren: 'app/loading/loading.module#LoadingModule' },
{ path: 'nav', loadChildren: 'app/nav/nav.module#NavModule' }
];
@NgModule({

View File

@ -21,4 +21,7 @@
<li>
<a href='loading'>Loading Page</a>
</li>
<li>
<a href='nav'>Nav Page</a>
</li>
</ul>

View File

@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NavPageComponent } from './nav.component';
const routes: Routes = [
{ path: '', component: NavPageComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class NavRoutingModule { }

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { PageOne } from './pages/page-one';
@Component({
selector: 'app-nav-page',
template: `
<ion-app>
<ion-nav [root]="pageOne"></ion-nav>
</ion-app>
`
})
export class NavPageComponent {
pageOne: any = PageOne;
constructor() {
}
}

View File

@ -0,0 +1,31 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NavPageComponent } from './nav.component';
import { NavRoutingModule } from './nav-routing.module';
import { IonicAngularModule } from '@ionic/angular';
import { PageOne } from './pages/page-one';
import { PageTwo } from './pages/page-two';
import { PageThree } from './pages/page-three';
@NgModule({
imports: [
CommonModule,
NavRoutingModule,
IonicAngularModule
],
declarations: [
NavPageComponent,
PageOne,
PageTwo,
PageThree
],
entryComponents: [
PageOne,
PageTwo,
PageThree
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class NavModule { }

View File

@ -0,0 +1,62 @@
import { Component } from '@angular/core';
import { PageTwo } from './page-two';
@Component({
selector: 'page-one',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Page One</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
Page One
<div>
<ion-button (click)="goToPageTwo()">Go to Page Two</ion-button>
</div>
<ul>
<li>ngOnInit - {{ngOnInitDetection}}</li>
<li>ionViewWillEnter - {{ionViewWillEnterDetection}}</li>
<li>ionViewDidEnter - {{ionViewDidEnterDetection}}</li>
</ul>
</ion-content>
`
})
export class PageOne {
ngOnInitDetection = 'initial';
ionViewWillEnterDetection = 'initial';
ionViewDidEnterDetection = 'initial';
constructor() {
}
ngOnInit() {
console.log('page one ngOnInit');
setInterval(() => {
this.ngOnInitDetection = '' + Date.now();
}, 500);
}
ionViewWillEnter() {
console.log('page one ionViewWillEnter');
setInterval(() => {
this.ionViewWillEnterDetection = '' + Date.now();
}, 500);
}
ionViewDidEnter() {
console.log('page one ionViewDidEnter');
setInterval(() => {
this.ionViewDidEnterDetection = '' + Date.now();
}, 500);
}
goToPageTwo() {
const nav = document.querySelector('ion-nav') as any;
nav.push(PageTwo).then(() => console.log('push complete'));
}
}

View File

@ -0,0 +1,62 @@
import { Component } from '@angular/core';
@Component({
selector: 'page-three',
template: `
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Page Three</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
Page Three
<div>
<ion-button (click)="goBack()">Go Back</ion-button>
</div>
<ul>
<li>ngOnInit - {{ngOnInitDetection}}</li>
<li>ionViewWillEnter - {{ionViewWillEnterDetection}}</li>
<li>ionViewDidEnter - {{ionViewDidEnterDetection}}</li>
</ul>
</ion-content>
</ion-page>
`
})
export class PageThree {
ngOnInitDetection = 'initial';
ionViewWillEnterDetection = 'initial';
ionViewDidEnterDetection = 'initial';
constructor() {
}
ngOnInit() {
console.log('page two ngOnInit');
setInterval(() => {
this.ngOnInitDetection = '' + Date.now();
}, 500);
}
ionViewWillEnter() {
console.log('page two ionViewWillEnter');
setInterval(() => {
this.ionViewWillEnterDetection = '' + Date.now();
}, 500);
}
ionViewDidEnter() {
console.log('page two ionViewDidEnter');
setInterval(() => {
this.ionViewDidEnterDetection = '' + Date.now();
}, 500);
}
goBack() {
const nav = document.querySelector('ion-nav') as any;
nav.pop().then(() => console.log('pop complete'));
}
}

View File

@ -0,0 +1,72 @@
import { Component } from '@angular/core';
import { PageThree } from './page-three';
@Component({
selector: 'page-two',
template: `
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Page Two</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
Page Two
<div>
<ion-button (click)="goNext()">Go to Page Three</ion-button>
</div>
<div>
<ion-button (click)="goBack()">Go Back</ion-button>
</div>
<ul>
<li>ngOnInit - {{ngOnInitDetection}}</li>
<li>ionViewWillEnter - {{ionViewWillEnterDetection}}</li>
<li>ionViewDidEnter - {{ionViewDidEnterDetection}}</li>
</ul>
</ion-content>
</ion-page>
`
})
export class PageTwo {
ngOnInitDetection = 'initial';
ionViewWillEnterDetection = 'initial';
ionViewDidEnterDetection = 'initial';
constructor() {
}
ngOnInit() {
console.log('page two ngOnInit');
setInterval(() => {
this.ngOnInitDetection = '' + Date.now();
}, 500);
}
ionViewWillEnter() {
console.log('page two ionViewWillEnter');
setInterval(() => {
this.ionViewWillEnterDetection = '' + Date.now();
}, 500);
}
ionViewDidEnter() {
console.log('page two ionViewDidEnter');
setInterval(() => {
this.ionViewDidEnterDetection = '' + Date.now();
}, 500);
}
goNext() {
const nav = document.querySelector('ion-nav') as any;
nav.push(PageThree).then(() => console.log('push complete'));
}
goBack() {
const nav = document.querySelector('ion-nav') as any;
nav.pop().then(() => console.log('pop complete'));
}
}

View File

@ -1,6 +1,6 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
// "node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,

View File

@ -10,11 +10,9 @@ class App extends Component {
render() {
return (
<ion-app>
<ion-nav-controller ref={wc({}, {
delegate: Delegate
})}></ion-nav-controller>
<ion-nav ref={wc({},{
root: PageOne
root: PageOne,
delegate: Delegate
})}></ion-nav>
</ion-app>
);