diff --git a/src/components/modal/modal-component.ts b/src/components/modal/modal-component.ts index 7b659b236e..ddfec8ede9 100644 --- a/src/components/modal/modal-component.ts +++ b/src/components/modal/modal-component.ts @@ -5,6 +5,7 @@ import { NavParams } from '../../navigation/nav-params'; import { NavOptions } from '../../navigation/nav-util'; import { ViewController } from '../../navigation/view-controller'; import { GestureController, BlockerDelegate, GESTURE_MENU_SWIPE, GESTURE_GO_BACK_SWIPE } from '../../gestures/gesture-controller'; +import { ModuleLoader } from '../../util/module-loader'; import { assert } from '../../util/util'; /** @@ -31,7 +32,9 @@ export class ModalCmp { public _renderer: Renderer, public _navParams: NavParams, public _viewCtrl: ViewController, - gestureCtrl: GestureController + gestureCtrl: GestureController, + public moduleLoader: ModuleLoader + ) { let opts = _navParams.get('opts'); assert(opts, 'modal data must be valid'); @@ -49,7 +52,12 @@ export class ModalCmp { /** @private */ _load(component: any) { if (component) { - const componentFactory = this._cfr.resolveComponentFactory(component); + + let cfr = this.moduleLoader.getComponentFactoryResolver(component); + if (!cfr) { + cfr = this._cfr; + } + const componentFactory = cfr.resolveComponentFactory(component); // ******** DOM WRITE **************** const componentRef = this._viewport.createComponent(componentFactory, this._viewport.length, this._viewport.parentInjector, []); diff --git a/src/components/modal/modal-controller.ts b/src/components/modal/modal-controller.ts index 86ef2775db..0a703b1714 100644 --- a/src/components/modal/modal-controller.ts +++ b/src/components/modal/modal-controller.ts @@ -4,6 +4,7 @@ import { App } from '../app/app'; import { Config } from '../../config/config'; import { Modal } from './modal'; import { ModalOptions } from './modal-options'; +import { DeepLinker } from '../../navigation/deep-linker'; /** * @name ModalController @@ -116,7 +117,7 @@ import { ModalOptions } from './modal-options'; @Injectable() export class ModalController { - constructor(private _app: App, public config: Config) { } + constructor(private _app: App, public config: Config, private deepLinker: DeepLinker) { } /** * Create a modal to display. See below for options. @@ -126,6 +127,6 @@ export class ModalController { * @param {object} opts Modal options */ create(component: any, data: any = {}, opts: ModalOptions = {}) { - return new Modal(this._app, component, data, opts, this.config); + return new Modal(this._app, component, data, opts, this.config, this.deepLinker); } -} +} \ No newline at end of file diff --git a/src/components/modal/modal-impl.ts b/src/components/modal/modal-impl.ts new file mode 100644 index 0000000000..e839644e78 --- /dev/null +++ b/src/components/modal/modal-impl.ts @@ -0,0 +1,69 @@ +import { App } from '../app/app'; +import { Config } from '../../config/config'; +import { isPresent } from '../../util/util'; +import { PORTAL_MODAL } from '../app/app-constants'; +import { ModalCmp } from './modal-component'; +import { ModalOptions } from './modal-options'; +import { ModalSlideIn, ModalSlideOut, ModalMDSlideIn, ModalMDSlideOut } from './modal-transitions'; +import { NavOptions } from '../../navigation/nav-util'; +import { ViewController } from '../../navigation/view-controller'; + +/** + * @private + */ +export class ModalImpl extends ViewController { + private _app: App; + private _enterAnimation: string; + private _leaveAnimation: string; + + constructor(app: App, component: any, data: any, opts: ModalOptions = {}, config: Config) { + data = data || {}; + data.component = component; + opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + data.opts = opts; + + super(ModalCmp, data, null); + this._app = app; + this._enterAnimation = opts.enterAnimation; + this._leaveAnimation = opts.leaveAnimation; + + this.isOverlay = true; + + config.setTransition('modal-slide-in', ModalSlideIn); + config.setTransition('modal-slide-out', ModalSlideOut); + config.setTransition('modal-md-slide-in', ModalMDSlideIn); + config.setTransition('modal-md-slide-out', ModalMDSlideOut); + } + + /** + * @private + */ + getTransitionName(direction: string): string { + let key: string; + if (direction === 'back') { + if (this._leaveAnimation) { + return this._leaveAnimation; + } + key = 'modalLeave'; + } else { + if (this._enterAnimation) { + return this._enterAnimation; + } + key = 'modalEnter'; + } + return this._nav && this._nav.config.get(key); + } + + /** + * Present the action sheet instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + navOptions.minClickBlockDuration = navOptions.minClickBlockDuration || 400; + return this._app.present(this, navOptions, PORTAL_MODAL); + } + +} \ No newline at end of file diff --git a/src/components/modal/modal.ts b/src/components/modal/modal.ts index 243f45e9a3..3a54817cf6 100644 --- a/src/components/modal/modal.ts +++ b/src/components/modal/modal.ts @@ -1,70 +1,25 @@ import { App } from '../app/app'; import { Config } from '../../config/config'; -import { isPresent } from '../../util/util'; -import { PORTAL_MODAL } from '../app/app-constants'; -import { ModalCmp } from './modal-component'; -import { ModalOptions } from './modal-options'; -import { ModalSlideIn, ModalSlideOut, ModalMDSlideIn, ModalMDSlideOut } from './modal-transitions'; -import { NavOptions } from '../../navigation/nav-util'; -import { ViewController } from '../../navigation/view-controller'; +import { ModalOptions } from './modal-options'; +import { DeepLinker } from '../../navigation/deep-linker'; + +import { Overlay } from '../../navigation/overlay'; +import { OverlayProxy } from '../../navigation/overlay-proxy'; +import { ModalImpl } from './modal-impl'; /** * @private */ -export class Modal extends ViewController { - private _app: App; - private _enterAnimation: string; - private _leaveAnimation: string; +export class Modal extends OverlayProxy { - constructor(app: App, component: any, data: any, opts: ModalOptions = {}, config: Config) { - data = data || {}; - data.component = component; - opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; - opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; - data.opts = opts; + public isOverlay: boolean = true; - super(ModalCmp, data, null); - this._app = app; - this._enterAnimation = opts.enterAnimation; - this._leaveAnimation = opts.leaveAnimation; - - this.isOverlay = true; - - config.setTransition('modal-slide-in', ModalSlideIn); - config.setTransition('modal-slide-out', ModalSlideOut); - config.setTransition('modal-md-slide-in', ModalMDSlideIn); - config.setTransition('modal-md-slide-out', ModalMDSlideOut); + constructor(app: App, component: any, private data: any, private opts: ModalOptions = {}, config: Config, deepLinker: DeepLinker) { + super(app, component, config, deepLinker); } - /** - * @private - */ - getTransitionName(direction: string): string { - let key: string; - if (direction === 'back') { - if (this._leaveAnimation) { - return this._leaveAnimation; - } - key = 'modalLeave'; - } else { - if (this._enterAnimation) { - return this._enterAnimation; - } - key = 'modalEnter'; - } - return this._nav && this._nav.config.get(key); + getImplementation(): Overlay { + return new ModalImpl(this._app, this._component, this.data, this.opts, this._config); } - - /** - * Present the action sheet instance. - * - * @param {NavOptions} [opts={}] Nav options to go with this transition. - * @returns {Promise} Returns a promise which is resolved when the transition has completed. - */ - present(navOptions: NavOptions = {}) { - navOptions.minClickBlockDuration = navOptions.minClickBlockDuration || 400; - return this._app.present(this, navOptions, PORTAL_MODAL); - } - } diff --git a/src/components/modal/test/modal.spec.ts b/src/components/modal/test/modal.spec.ts index 583b01d4b5..67c7f05bf2 100644 --- a/src/components/modal/test/modal.spec.ts +++ b/src/components/modal/test/modal.spec.ts @@ -1,19 +1,16 @@ -import { mockApp, mockConfig } from '../../../util/mock-providers'; +import { mockApp, mockConfig, mockDeepLinker } from '../../../util/mock-providers'; import { Component } from '@angular/core'; import { ModalController } from '../modal-controller'; import { ModalCmp } from '../modal-component'; -import { ViewController } from '../../../navigation/view-controller'; describe('Modal', () => { describe('create', () => { - it('should have the correct properties on modal view controller instance', () => { - let modalCtrl = new ModalController(mockApp(), mockConfig()); - let modalViewController = modalCtrl.create(ComponentToPresent); - expect(modalViewController.component).toEqual(ModalCmp); - expect(modalViewController.isOverlay).toEqual(true); - expect(modalViewController instanceof ViewController).toEqual(true); + it('should have the correct properties on modal view controller proxy instance', () => { + let modalCtrl = new ModalController(mockApp(), mockConfig(), mockDeepLinker()); + let modalViewControllerProxy = modalCtrl.create(ComponentToPresent); + expect(modalViewControllerProxy._component).toEqual(ModalCmp); }); }); diff --git a/src/navigation/deep-linker.ts b/src/navigation/deep-linker.ts index 3adbf47191..9ef95ae81d 100644 --- a/src/navigation/deep-linker.ts +++ b/src/navigation/deep-linker.ts @@ -125,8 +125,6 @@ export class DeepLinker { _history: string[] = []; /** @internal */ _indexAliasUrl: string; - /** @internal */ - _cfrMap = new Map(); constructor( @@ -277,12 +275,9 @@ export class DeepLinker { if (link.loadChildren) { // awesome, looks like we'll lazy load this component // using loadChildren as the URL to request - return this._moduleLoader.load(link.loadChildren).then(loadedModule => { - // kerpow!! we just lazy loaded a component!! - // update the existing link with the loaded component - link.component = loadedModule.component; - this._cfrMap.set(link.component, loadedModule.componentFactoryResolver); - return link.component; + return this._moduleLoader.load(link.loadChildren).then((response) => { + link.component = response.component; + return response.component; }); } @@ -294,7 +289,8 @@ export class DeepLinker { * @internal */ resolveComponent(component: any): ComponentFactory { - let cfr = this._cfrMap.get(component); + + let cfr = this._moduleLoader.getComponentFactoryResolver(component); if (!cfr) { cfr = this._baseCfr; } @@ -599,4 +595,4 @@ export function normalizeUrl(browserUrl: string): string { browserUrl = browserUrl.substr(0, browserUrl.length - 1); } return browserUrl; -} +} \ No newline at end of file diff --git a/src/navigation/overlay-proxy.ts b/src/navigation/overlay-proxy.ts new file mode 100644 index 0000000000..57ca1cc6df --- /dev/null +++ b/src/navigation/overlay-proxy.ts @@ -0,0 +1,77 @@ +import { App } from '../components/app/app'; +import { Config } from '../config/config'; +import { isString } from '../util/util'; + + +import { DeepLinker } from './deep-linker'; +import { NavOptions } from './nav-util'; +import { Overlay } from './overlay'; + + +export class OverlayProxy { + + overlay: Overlay; + _onWillDismiss: Function; + _onDidDismiss: Function; + + + constructor(public _app: App, public _component: any, public _config: Config, public _deepLinker: DeepLinker) { + } + + getImplementation(): Overlay { + throw new Error('Child class must implement "getImplementation" method'); + } + + /** + * Present the modal instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + // check if it's a lazy loaded component, or not + const isLazyLoaded = isString(this._component); + if (isLazyLoaded) { + + return this._deepLinker.getComponentFromName(this._component).then((loadedComponent: any) => { + this._component = loadedComponent; + return this.createAndPresentOverlay(navOptions); + }); + } else { + return this.createAndPresentOverlay(navOptions); + } + } + + dismiss(data?: any, role?: any, navOptions?: NavOptions): Promise { + if (this.overlay) { + return this.overlay.dismiss(); + } + } + + /** + * Called when the current viewController has be successfully dismissed + */ + onDidDismiss(callback: Function) { + this._onDidDismiss = callback; + if (this.overlay) { + this.overlay.onDidDismiss(this._onDidDismiss); + } + } + + createAndPresentOverlay(navOptions: NavOptions) { + this.overlay = this.getImplementation(); + this.overlay.onWillDismiss(this._onWillDismiss); + this.overlay.onDidDismiss(this._onDidDismiss); + return this.overlay.present(navOptions); + } + + /** + * Called when the current viewController will be dismissed + */ + onWillDismiss(callback: Function) { + this._onWillDismiss = callback; + if (this.overlay) { + this.overlay.onWillDismiss(this._onWillDismiss); + } + } +} diff --git a/src/navigation/overlay.ts b/src/navigation/overlay.ts new file mode 100644 index 0000000000..4c782516bf --- /dev/null +++ b/src/navigation/overlay.ts @@ -0,0 +1,8 @@ +import { NavOptions } from './nav-util'; + +export interface Overlay { + present(opts?: NavOptions): Promise; + dismiss(data?: any, role?: any, navOptions?: NavOptions): Promise; + onDidDismiss(callback: Function): void; + onWillDismiss(callback: Function): void; +} diff --git a/src/navigation/test/overlay-proxy.spec.ts b/src/navigation/test/overlay-proxy.spec.ts new file mode 100644 index 0000000000..16693e5c7c --- /dev/null +++ b/src/navigation/test/overlay-proxy.spec.ts @@ -0,0 +1,119 @@ +import { OverlayProxy } from '../overlay-proxy'; + +import { mockApp, mockConfig, mockDeepLinker, mockOverlay } from '../../util/mock-providers'; + +describe('Overlay Proxy', () => { + describe('dismiss', () => { + it('should call dismiss if overlay is loaded', (done: Function) => { + + const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker()); + instance.overlay = mockOverlay(); + spyOn(instance.overlay, instance.overlay.dismiss.name).and.returnValue(Promise.resolve()); + + const promise = instance.dismiss(null, null, null); + + promise.then(() => { + expect(instance.overlay.dismiss).toHaveBeenCalled(); + done(); + }).catch((err: Error) => { + done(err); + }); + }); + }); + + describe('onWillDismiss', () => { + it('should update the handler on the overlay object', () => { + const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker()); + instance.overlay = mockOverlay(); + spyOn(instance.overlay, instance.overlay.onWillDismiss.name); + + const handler = () => { }; + instance.onWillDismiss(handler); + + expect(instance.overlay.onWillDismiss).toHaveBeenCalledWith(handler); + }); + }); + + describe('onDidDismiss', () => { + it('should update the handler on the overlay object', () => { + const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker()); + instance.overlay = mockOverlay(); + spyOn(instance.overlay, instance.overlay.onDidDismiss.name); + + const handler = () => { }; + instance.onDidDismiss(handler); + + expect(instance.overlay.onDidDismiss).toHaveBeenCalledWith(handler); + }); + }); + + describe('createAndPresentOverlay', () => { + it('should set onWillDismiss and onDidDismiss handlers', (done: Function) => { + const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker()); + const handler = () => { }; + instance.onWillDismiss(handler); + instance.onDidDismiss(handler); + const knownOptions = {}; + const knownOverlay = mockOverlay(); + + spyOn(knownOverlay, knownOverlay.present.name).and.returnValue(Promise.resolve()); + spyOn(knownOverlay, knownOverlay.onDidDismiss.name); + spyOn(knownOverlay, knownOverlay.onWillDismiss.name); + spyOn(instance, 'getImplementation').and.returnValue(knownOverlay); + + const promise = instance.createAndPresentOverlay(knownOptions); + + promise.then(() => { + expect(knownOverlay.present).toHaveBeenCalledWith(knownOptions); + expect(knownOverlay.onDidDismiss).toHaveBeenCalledWith(handler); + expect(knownOverlay.onWillDismiss).toHaveBeenCalledWith(handler); + done(); + }).catch((err: Error) => { + done(err); + }); + }); + }); + + describe('present', () => { + it('should use present the overlay immediately if the component is not a string', (done: Function) => { + const knownComponent = { }; + const deepLinker = mockDeepLinker(); + const knownOverlay = mockOverlay(); + const instance = new OverlayProxy(mockApp(), knownComponent, mockConfig(), deepLinker); + const knownOptions = {}; + + spyOn(instance, 'getImplementation').and.returnValue(knownOverlay); + spyOn(deepLinker, 'getComponentFromName'); + + const promise = instance.present(knownOptions); + + promise.then(() => { + expect(deepLinker.getComponentFromName).not.toHaveBeenCalled(); + done(); + }).catch((err: Error) => { + done(err); + }); + }); + + it('should load the component if its a string before using it', (done: Function) => { + const knownComponent = { }; + const deepLinker = mockDeepLinker(); + const knownOverlay = mockOverlay(); + const componentName = 'my-component'; + const instance = new OverlayProxy(mockApp(), componentName, mockConfig(), deepLinker); + const knownOptions = {}; + + spyOn(instance, 'getImplementation').and.returnValue(knownOverlay); + spyOn(deepLinker, 'getComponentFromName').and.returnValue(Promise.resolve(knownComponent)); + + const promise = instance.present(knownOptions); + + promise.then(() => { + expect(deepLinker.getComponentFromName).toHaveBeenCalledWith(componentName); + done(); + }).catch((err: Error) => { + done(err); + }); + }); + }); +}); diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts index 5445df5d41..52d63a25c3 100644 --- a/src/util/mock-providers.ts +++ b/src/util/mock-providers.ts @@ -12,6 +12,7 @@ import { Haptic } from '../tap-click/haptic'; import { IonicApp } from '../components/app/app-root'; import { Keyboard } from '../platform/keyboard'; import { Menu } from '../components/menu/menu'; +import { NavOptions } from '../navigation/nav-util'; import { NavControllerBase } from '../navigation/nav-controller-base'; import { OverlayPortal } from '../components/nav/overlay-portal'; import { PageTransition } from '../transitions/page-transition'; @@ -528,3 +529,12 @@ export function noop(): any { return 'noop'; }; export function mockModuleLoader() { return { }; } + +export function mockOverlay() { + return { + present: (opts?: NavOptions) => { return Promise.resolve(); }, + dismiss: (data?: any, role?: any, navOptions?: NavOptions) => { return Promise.resolve(); }, + onDidDismiss: (callback: Function) => { }, + onWillDismiss: (callback: Function) => { } + }; +} \ No newline at end of file diff --git a/src/util/module-loader.ts b/src/util/module-loader.ts index 32524c2520..6a588394a4 100644 --- a/src/util/module-loader.ts +++ b/src/util/module-loader.ts @@ -4,12 +4,17 @@ import { NgModuleLoader } from './ng-module-loader'; export const LAZY_LOADED_TOKEN = new OpaqueToken('LZYCMP'); + + /** * @private */ @Injectable() export class ModuleLoader { + /** @internal */ + _cfrMap = new Map(); + constructor( private _ngModuleLoader: NgModuleLoader, private _injector: Injector) {} @@ -20,16 +25,23 @@ export class ModuleLoader { const splitString = modulePath.split(SPLITTER); - return this._ngModuleLoader.load(splitString[0], splitString[1]) - .then(loadedModule => { - console.timeEnd(`ModuleLoader, load: ${modulePath}'`); - const ref = loadedModule.create(this._injector); + return this._ngModuleLoader.load(splitString[0], splitString[1]).then(loadedModule => { + console.timeEnd(`ModuleLoader, load: ${modulePath}'`); + const ref = loadedModule.create(this._injector); - return { - componentFactoryResolver: ref.componentFactoryResolver, - component: ref.injector.get(LAZY_LOADED_TOKEN) - }; - }); + const component = ref.injector.get(LAZY_LOADED_TOKEN); + + this._cfrMap.set(component, ref.componentFactoryResolver); + + return { + componentFactoryResolver: ref.componentFactoryResolver, + component: component + }; + }); + } + + getComponentFactoryResolver(component: Type) { + return this._cfrMap.get(component); } }