diff --git a/packages/angular/src/components/ion-nav.ts b/packages/angular/src/components/ion-nav.ts index 21b1b2347b..ea061b463e 100644 --- a/packages/angular/src/components/ion-nav.ts +++ b/packages/angular/src/components/ion-nav.ts @@ -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>(); @Component({ selector: 'ion-nav', @@ -19,41 +25,41 @@ import { AngularViewController } from '../types/angular-view-controller';
` }) -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, + _propsOrDataObj?: any, _classesToAdd?: string[]): Promise { + + 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 { - - 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(); } } diff --git a/packages/angular/src/module.ts b/packages/angular/src/module.ts index edbe1a5975..81caad18f0 100644 --- a/packages/angular/src/module.ts +++ b/packages/angular/src/module.ts @@ -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 ] diff --git a/packages/angular/src/providers/angular-component-mounter.ts b/packages/angular/src/providers/angular-component-mounter.ts new file mode 100644 index 0000000000..dea3dbd7ee --- /dev/null +++ b/packages/angular/src/providers/angular-component-mounter.ts @@ -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, providers: any[], changeDetection: ChangeDetectorRef, componentResolveFactory: ComponentFactoryResolver, injector: Injector): Promise { + + 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): Promise { + return new Promise((resolve) => { + this.zone.run(() => { + componentRef.destroy(); + resolve(); + }); + }); + } +} + +export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type, 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, + }; +} + diff --git a/packages/angular/src/providers/angular-framework-delegate.ts b/packages/angular/src/providers/angular-framework-delegate.ts deleted file mode 100644 index 1f87a2f022..0000000000 --- a/packages/angular/src/providers/angular-framework-delegate.ts +++ /dev/null @@ -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, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): Promise { - - return new Promise((resolve) => { - this.zone.run(() => { - const mountingData = attachViewToDom(this.crf, componentToMount, viewport, providers, changeDetection); - resolve(mountingData); - }); - }); - } - - removeViewFromDom(componentRef: ComponentRef): Promise { - return new Promise((resolve) => { - this.zone.run(() => { - componentRef.destroy(); - resolve(); - }); - }); - } -} - -export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type, 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; - childInjector: Injector; - componentRef: ComponentRef; -} diff --git a/packages/angular/src/types/angular-view-controller.ts b/packages/angular/src/types/interfaces.ts similarity index 58% rename from packages/angular/src/types/angular-view-controller.ts rename to packages/angular/src/types/interfaces.ts index f13c2dd4d0..26d7efae26 100644 --- a/packages/angular/src/types/angular-view-controller.ts +++ b/packages/angular/src/types/interfaces.ts @@ -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; - injector?: Injector; + childInjector?: Injector; componentRef?: ComponentRef; instance?: any; angularHostElement?: HTMLElement; -} +} \ No newline at end of file diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index d4ba305f44..43ecf98534 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -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'; diff --git a/packages/core/src/components/nav-controller/nav-controller.tsx b/packages/core/src/components/nav-controller/nav-controller.tsx deleted file mode 100644 index 2ee4b4a2ea..0000000000 --- a/packages/core/src/components/nav-controller/nav-controller.tsx +++ /dev/null @@ -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 { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return pushImpl(nav, delegate, animation, component, data, opts); - }); - } - - @Method() - pop(nav: Nav, opts?: NavOptions): Promise { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return popImpl(nav, delegate, animation, opts); - }); - } - - @Method() - setRoot(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise { - 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 { - 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 { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return insertPagesImpl(nav, delegate, animation, insertIndex, insertPages, opts); - }); - } - - @Method() - popToRoot(nav: Nav, opts?: NavOptions): Promise { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return popToRootImpl(nav, delegate, animation, opts); - }); - } - - @Method() - popTo(nav: Nav, indexOrViewCtrl: any, opts?: NavOptions): Promise { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return popToImpl(nav, delegate, animation, indexOrViewCtrl, opts); - }); - } - - @Method() - removeIndex(nav: Nav, startIndex: number, removeCount?: number, opts?: NavOptions): Promise { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return removeImpl(nav, delegate, animation, startIndex, removeCount, opts); - }); - } - - @Method() - removeView(nav: Nav, viewController: ViewController, opts?: NavOptions): Promise { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return removeViewImpl(nav, delegate, animation, viewController, opts); - }); - } - - @Method() - setPages(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions): Promise { - return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => { - return setPagesImpl(nav, delegate, animation, componentDataPairs, opts); - }); - } - - render() { - return ; - } -} - -export function hydrateDelegateAndAnimation(navController: NavController): Promise { - return Promise.all([hydrateDelegate(navController), hydrateAnimationController(navController.animationCtrl)]); -} - -export function hydrateDelegate(navController: NavController): Promise { - 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 { - return animationController.create(); -} diff --git a/packages/core/src/components/nav-controller/readme.md b/packages/core/src/components/nav-controller/readme.md deleted file mode 100644 index c96276aa78..0000000000 --- a/packages/core/src/components/nav-controller/readme.md +++ /dev/null @@ -1,57 +0,0 @@ -# ion-nav-controller - - - - - - -## Properties - -#### delegate - -any - - -## Attributes - -#### delegate - -any - - -## Methods - -#### insert() - - -#### insertPages() - - -#### pop() - - -#### popTo() - - -#### popToRoot() - - -#### push() - - -#### removeIndex() - - -#### removeView() - - -#### setPages() - - -#### setRoot() - - - ----------------------------------------------- - -*Built by [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/nav/nav-interface.ts b/packages/core/src/components/nav/nav-interface.ts deleted file mode 100644 index 1813fcdbf8..0000000000 --- a/packages/core/src/components/nav/nav-interface.ts +++ /dev/null @@ -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; - pop(opts?: NavOptions): Promise; - setRoot(component: any, data?: any, opts?: NavOptions): Promise; - insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise; - insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise; - popToRoot(opts?: NavOptions): Promise; - popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise; - removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise; - removeView(viewController: PublicViewController, opts?: NavOptions): Promise; - setPages(componentDataPairs: any[], opts?: NavOptions): Promise; - - getActive?(): PublicViewController; - getPrevious?(view?: PublicViewController): PublicViewController; - canGoBack?(): boolean; - canSwipeBack?(): boolean; - getFirstView?(): PublicViewController; - - element?: HTMLElement; -} diff --git a/packages/core/src/navigation/nav-interfaces.ts b/packages/core/src/components/nav/nav-interfaces.ts similarity index 64% rename from packages/core/src/navigation/nav-interfaces.ts rename to packages/core/src/components/nav/nav-interfaces.ts index 310aa8c119..9530f2a14d 100644 --- a/packages/core/src/navigation/nav-interfaces.ts +++ b/packages/core/src/components/nav/nav-interfaces.ts @@ -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; + pop(opts?: NavOptions): Promise; + setRoot(component: any, data?: any, opts?: NavOptions): Promise; + insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise; + insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise; + popToRoot(opts?: NavOptions): Promise; + popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise; + removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise; + removeView(viewController: PublicViewController, opts?: NavOptions): Promise; + setPages(componentDataPairs: any[], opts?: NavOptions): Promise; - attachViewToDom(elementOrContainerToMountTo: any, elementOrComponentToMount: any, propsOrDataObj?: any, classesToAdd?: string[]): Promise; - removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any): Promise; -} + getActive?(): PublicViewController; + getPrevious?(view?: PublicViewController): PublicViewController; + canGoBack?(): boolean; + canSwipeBack?(): boolean; + getFirstView?(): PublicViewController; -export interface FrameworkMountingData { - element: HTMLElement; + element?: HTMLElement; } export interface NavContainer { diff --git a/packages/core/src/navigation/nav-utils.ts b/packages/core/src/components/nav/nav-utils.ts similarity index 98% rename from packages/core/src/navigation/nav-utils.ts rename to packages/core/src/components/nav/nav-utils.ts index d78656176a..618bdca57b 100644 --- a/packages/core/src/navigation/nav-utils.ts +++ b/packages/core/src/components/nav/nav-utils.ts @@ -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; diff --git a/packages/core/src/components/nav/nav.tsx b/packages/core/src/components/nav/nav.tsx index 23d2cd4fbc..fb107d3b0d 100644 --- a/packages/core/src/components/nav/nav.tsx +++ b/packages/core/src/components/nav/nav.tsx @@ -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(); /* 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 { - 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 { + return animationController.create(); +} + + +// public api + +export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data?: any, opts?: NavOptions, done?: () => void): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const promise = new Promise((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 { + + 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 { + 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 { + + let promise: Promise = 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 { + + if (nav.destroyed) { + return Promise.resolve(); + } + + const activeIndex = nav.views.indexOf(activeViewController); + const promises: Promise[] = []; + 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 { + 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 { + 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[] = []; + 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[] = []; + + + 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 { + + 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; diff --git a/packages/core/src/navigation/transitions/transition.ios.ts b/packages/core/src/components/nav/transitions/transition.ios.ts similarity index 99% rename from packages/core/src/navigation/transitions/transition.ios.ts rename to packages/core/src/components/nav/transitions/transition.ios.ts index dfe779bc7a..11c25b2f15 100644 --- a/packages/core/src/navigation/transitions/transition.ios.ts +++ b/packages/core/src/components/nav/transitions/transition.ios.ts @@ -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)'; diff --git a/packages/core/src/navigation/transitions/transition.md.ts b/packages/core/src/components/nav/transitions/transition.md.ts similarity index 97% rename from packages/core/src/navigation/transitions/transition.md.ts rename to packages/core/src/components/nav/transitions/transition.md.ts index d9b7a42a2f..81ca8137d5 100644 --- a/packages/core/src/navigation/transitions/transition.md.ts +++ b/packages/core/src/components/nav/transitions/transition.md.ts @@ -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'; diff --git a/packages/core/src/navigation/view-controller.ts b/packages/core/src/components/nav/view-controller.ts similarity index 98% rename from packages/core/src/navigation/view-controller.ts rename to packages/core/src/components/nav/view-controller.ts index 7543b142c9..6c8562c444 100644 --- a/packages/core/src/navigation/view-controller.ts +++ b/packages/core/src/components/nav/view-controller.ts @@ -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 { diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index 0b3f2fba26..efde24a77d 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -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; + removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any, escapeHatch?: any): Promise; +} + +export interface FrameworkMountingData { + element: HTMLElement; +} diff --git a/packages/core/src/navigation/nav-controller-functions.ts b/packages/core/src/navigation/nav-controller-functions.ts deleted file mode 100644 index a3f7cbcb1d..0000000000 --- a/packages/core/src/navigation/nav-controller-functions.ts +++ /dev/null @@ -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(); - -// public api - -export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data?: any, opts?: NavOptions, done?: () => void): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - const promise = new Promise((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 { - - 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 { - 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 { - - let promise: Promise = 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 { - - if (nav.destroyed) { - return Promise.resolve(); - } - - const activeIndex = nav.views.indexOf(activeViewController); - const promises: Promise[] = []; - 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 { - 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 { - 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[] = []; - 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[] = []; - - - 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 { - - 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; diff --git a/packages/core/src/components/nav-controller/dom-framework-delegate.tsx b/packages/core/src/utils/dom-framework-delegate.ts similarity index 88% rename from packages/core/src/components/nav-controller/dom-framework-delegate.tsx rename to packages/core/src/utils/dom-framework-delegate.ts index caacc32b9a..fb68411cfe 100644 --- a/packages/core/src/components/nav-controller/dom-framework-delegate.tsx +++ b/packages/core/src/utils/dom-framework-delegate.ts @@ -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 { + attachViewToDom(parentElement: HTMLElement, tagOrElement: string | HTMLElement, propsOrDataObj: any = {}, classesToAdd: string[] = []): Promise { 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 { - parentElement.removeChild(childElement); return Promise.resolve({ element: null diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index ee8509850e..1ea1b6727d 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -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: [ diff --git a/packages/demos/angular/src/app/app-routing.module.ts b/packages/demos/angular/src/app/app-routing.module.ts index 83e0a7b43f..9bb353c5c7 100644 --- a/packages/demos/angular/src/app/app-routing.module.ts +++ b/packages/demos/angular/src/app/app-routing.module.ts @@ -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({ diff --git a/packages/demos/angular/src/app/home-page/home-page.component.html b/packages/demos/angular/src/app/home-page/home-page.component.html index 09924873c9..0bf4f10966 100644 --- a/packages/demos/angular/src/app/home-page/home-page.component.html +++ b/packages/demos/angular/src/app/home-page/home-page.component.html @@ -21,4 +21,7 @@
  • Loading Page
  • +
  • + Nav Page +
  • diff --git a/packages/demos/angular/src/app/nav/nav-routing.module.ts b/packages/demos/angular/src/app/nav/nav-routing.module.ts new file mode 100644 index 0000000000..0a320b2423 --- /dev/null +++ b/packages/demos/angular/src/app/nav/nav-routing.module.ts @@ -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 { } diff --git a/packages/demos/angular/src/app/nav/nav.component.ts b/packages/demos/angular/src/app/nav/nav.component.ts new file mode 100644 index 0000000000..45e9f03242 --- /dev/null +++ b/packages/demos/angular/src/app/nav/nav.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +import { PageOne } from './pages/page-one'; + +@Component({ + selector: 'app-nav-page', + template: ` + + + + ` +}) +export class NavPageComponent { + + pageOne: any = PageOne; + constructor() { + + } + +} diff --git a/packages/demos/angular/src/app/nav/nav.module.ts b/packages/demos/angular/src/app/nav/nav.module.ts new file mode 100644 index 0000000000..af5e14ca2e --- /dev/null +++ b/packages/demos/angular/src/app/nav/nav.module.ts @@ -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 { } diff --git a/packages/demos/angular/src/app/nav/pages/page-one.ts b/packages/demos/angular/src/app/nav/pages/page-one.ts new file mode 100644 index 0000000000..f1d26e9059 --- /dev/null +++ b/packages/demos/angular/src/app/nav/pages/page-one.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; + +import { PageTwo } from './page-two'; + +@Component({ + selector: 'page-one', + template: ` + + + Page One + + + + Page One +
    + Go to Page Two +
    +
      +
    • ngOnInit - {{ngOnInitDetection}}
    • +
    • ionViewWillEnter - {{ionViewWillEnterDetection}}
    • +
    • ionViewDidEnter - {{ionViewDidEnterDetection}}
    • +
    +
    + ` +}) +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')); + } +} diff --git a/packages/demos/angular/src/app/nav/pages/page-three.ts b/packages/demos/angular/src/app/nav/pages/page-three.ts new file mode 100644 index 0000000000..ffca811b68 --- /dev/null +++ b/packages/demos/angular/src/app/nav/pages/page-three.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'page-three', + template: ` + + + + Page Three + + + + Page Three +
    + Go Back +
    +
      +
    • ngOnInit - {{ngOnInitDetection}}
    • +
    • ionViewWillEnter - {{ionViewWillEnterDetection}}
    • +
    • ionViewDidEnter - {{ionViewDidEnterDetection}}
    • +
    +
    +
    + ` +}) +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')); + } +} diff --git a/packages/demos/angular/src/app/nav/pages/page-two.ts b/packages/demos/angular/src/app/nav/pages/page-two.ts new file mode 100644 index 0000000000..8ccdb35c0f --- /dev/null +++ b/packages/demos/angular/src/app/nav/pages/page-two.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; + +import { PageThree } from './page-three'; + +@Component({ + selector: 'page-two', + template: ` + + + + Page Two + + + + Page Two +
    + Go to Page Three +
    +
    + Go Back +
    +
      +
    • ngOnInit - {{ngOnInitDetection}}
    • +
    • ionViewWillEnter - {{ionViewWillEnterDetection}}
    • +
    • ionViewDidEnter - {{ionViewDidEnterDetection}}
    • +
    +
    +
    + ` +}) +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')); + } +} diff --git a/packages/demos/angular/tslint.json b/packages/demos/angular/tslint.json index c24dc293d7..c3b6f17eb6 100644 --- a/packages/demos/angular/tslint.json +++ b/packages/demos/angular/tslint.json @@ -1,6 +1,6 @@ { "rulesDirectory": [ - "node_modules/codelyzer" + // "node_modules/codelyzer" ], "rules": { "arrow-return-shorthand": true, diff --git a/packages/demos/react/src/App.js b/packages/demos/react/src/App.js index 5bfef7b872..57e9a40623 100644 --- a/packages/demos/react/src/App.js +++ b/packages/demos/react/src/App.js @@ -10,11 +10,9 @@ class App extends Component { render() { return ( - );