diff --git a/src/components/modal/modal.ios.scss b/src/components/modal/modal.ios.scss index 68e67f4aa4..cbdd55e7fc 100644 --- a/src/components/modal/modal.ios.scss +++ b/src/components/modal/modal.ios.scss @@ -6,14 +6,7 @@ $modal-ios-background-color: $background-ios-color !default; $modal-ios-border-radius: 5px !default; -.modal ion-page { - background-color: $modal-ios-background-color; -} .modal-wrapper { - @media only screen and (min-width: 768px) and (min-height: 600px) { - overflow: hidden; - - border-radius: $modal-ios-border-radius; - } + background-color: $modal-ios-background-color; } diff --git a/src/components/modal/modal.md.scss b/src/components/modal/modal.md.scss index 33fafdf095..479bc69f33 100644 --- a/src/components/modal/modal.md.scss +++ b/src/components/modal/modal.md.scss @@ -6,6 +6,6 @@ $modal-md-background-color: $background-md-color !default; -.modal ion-page { +.modal-wrapper { background-color: $modal-md-background-color; } diff --git a/src/components/modal/modal.ts b/src/components/modal/modal.ts index f83c99e04e..63a87b5b81 100644 --- a/src/components/modal/modal.ts +++ b/src/components/modal/modal.ts @@ -1,5 +1,7 @@ -import {Component, DynamicComponentLoader, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, ComponentRef, DynamicComponentLoader, ElementRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {windowDimensions} from '../../util/dom'; +import {pascalCaseToDashCase} from '../../util/util'; import {NavParams} from '../nav/nav-params'; import {ViewController} from '../nav/view-controller'; import {Animation} from '../../animations/animation'; @@ -106,9 +108,12 @@ import {Transition, TransitionOptions} from '../../transitions/transition'; */ export class Modal extends ViewController { + public modalViewType: string; + constructor(componentType, data: any = {}) { data.componentType = componentType; super(ModalCmp, data); + this.modalViewType = componentType.name; this.viewType = 'modal'; this.isOverlay = true; } @@ -129,6 +134,21 @@ export class Modal extends ViewController { return new Modal(componentType, data); } + // Override the load method and load our child component + loaded(done) { + // grab the instance, and proxy the ngAfterViewInit method + let originalNgAfterViewInit = this.instance.ngAfterViewInit; + + this.instance.ngAfterViewInit = () => { + if ( originalNgAfterViewInit ) { + originalNgAfterViewInit(); + } + this.instance.loadComponent().then( (componentRef: ComponentRef) => { + this.setInstance(componentRef.instance); + done(); + }); + }; + } } @Component({ @@ -139,20 +159,22 @@ export class Modal extends ViewController { '
' + '' }) -class ModalCmp { +export class ModalCmp { @ViewChild('viewport', {read: ViewContainerRef}) viewport: ViewContainerRef; - constructor(private _loader: DynamicComponentLoader, private _navParams: NavParams, private _viewCtrl: ViewController) {} + constructor(protected _eleRef: ElementRef, protected _loader: DynamicComponentLoader, protected _navParams: NavParams, protected _viewCtrl: ViewController) { + } - onPageWillEnter() { - this._loader.loadNextToLocation(this._navParams.data.componentType, this.viewport).then(componentRef => { - this._viewCtrl.setInstance(componentRef.instance); - - // manually fire onPageWillEnter() since ModalCmp's onPageWillEnter already happened - this._viewCtrl.willEnter(); + loadComponent(): Promise> { + return this._loader.loadNextToLocation(this._navParams.data.componentType, this.viewport).then(componentRef => { + return componentRef; }); } + + ngAfterViewInit() { + // intentionally kept empty + } } /** @@ -166,6 +188,13 @@ class ModalSlideIn extends Transition { let backdrop = new Animation(ele.querySelector('.backdrop')); backdrop.fromTo('opacity', 0.01, 0.4); let wrapper = new Animation(ele.querySelector('.modal-wrapper')); + let page = ele.querySelector('ion-page'); + page.classList.add('show-page'); + + // auto-add page css className created from component JS class name + let cssClassName = pascalCaseToDashCase((enteringView).modalViewType); + page.classList.add(cssClassName); + wrapper.fromTo('translateY', '100%', '0%'); this .element(enteringView.pageRef()) @@ -191,10 +220,17 @@ class ModalSlideOut extends Transition { super(opts); let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); backdrop.fromTo('opacity', 0.4, 0.0); - let wrapper = new Animation(ele.querySelector('.modal-wrapper')); - wrapper.fromTo('translateY', '0%', '100%'); + let wrapperEle = ele.querySelector('.modal-wrapper'); + let wrapperEleRect = wrapperEle.getBoundingClientRect(); + let wrapper = new Animation(wrapperEle); + + // height of the screen - top of the container tells us how much to scoot it down + // so it's off-screen + let screenDimensions = windowDimensions(); + wrapper.fromTo('translateY', '0px', `${screenDimensions.height - wrapperEleRect.top}px`); this .element(leavingView.pageRef()) @@ -216,6 +252,12 @@ class ModalMDSlideIn extends Transition { backdrop.fromTo('opacity', 0.01, 0.4); let wrapper = new Animation(ele.querySelector('.modal-wrapper')); wrapper.fromTo('translateY', '40px', '0px'); + let page = ele.querySelector('ion-page'); + page.classList.add('show-page'); + + // auto-add page css className created from component JS class name + let cssClassName = pascalCaseToDashCase((enteringView).modalViewType); + page.classList.add(cssClassName); this .element(enteringView.pageRef()) diff --git a/src/components/modal/modal.wp.scss b/src/components/modal/modal.wp.scss index 11f8daa08c..bc5fd33694 100644 --- a/src/components/modal/modal.wp.scss +++ b/src/components/modal/modal.wp.scss @@ -6,6 +6,6 @@ $modal-wp-background-color: $background-wp-color !default; -.modal ion-page { +.modal-wrapper { background-color: $modal-wp-background-color; } diff --git a/src/components/modal/test/basic/index.ts b/src/components/modal/test/basic/index.ts index 7a23ef620f..1c7ee9e2e8 100644 --- a/src/components/modal/test/basic/index.ts +++ b/src/components/modal/test/basic/index.ts @@ -68,8 +68,53 @@ class E2EPage { animation: 'my-fade-in' }); } + + presentNavigableModal(){ + let modal = Modal.create(NavigableModal); + this.nav.present(modal); + //this.nav.push(NavigableModal); + } } +@Page({ + template: ` + + Page One + + + + + ` +}) +class NavigableModal{ + constructor(private navController:NavController){ + } + + submit(){ + this.navController.push(NavigableModal2); + } +} + +@Page({ + template: ` + + Page Two + + + + + ` +}) +class NavigableModal2{ + constructor(private navController:NavController){ + } + + submit(){ + this.navController.pop(); + } +} + + @Page({ template: ` @@ -105,6 +150,10 @@ class ModalPassData { this.viewCtrl.dismiss(this.data); } + onPageLoaded(){ + console.log("ModalPassData onPageLoaded fired"); + } + onPageWillEnter(){ console.log("ModalPassData onPagewillEnter fired"); } @@ -280,15 +329,26 @@ class ModalFirstPage { push() { let page = ModalSecondPage; let params = { id: 8675309, myData: [1,2,3,4] }; - let opts = { animation: 'ios-transition' }; - this.nav.push(page, params, opts); + this.nav.push(page, params); } dismiss() { this.nav.rootNav.pop(); } + onPageLoaded(){ + console.log("ModalFirstPage OnPageLoaded fired"); + } + + onPageWillEnter(){ + console.log("ModalFirstPage onPageWillEnter fired"); + } + + onPageDidEnter(){ + console.log("ModalFirstPage onPageDidEnter fired"); + } + openActionSheet() { let actionSheet = ActionSheet.create({ buttons: [ @@ -352,12 +412,21 @@ class ModalFirstPage { ` }) class ModalSecondPage { - constructor( - private nav: NavController, - params: NavParams - ) { + constructor(private nav: NavController, params: NavParams) { console.log('Second page params:', params); } + + onPageLoaded(){ + console.log("ModalSecondPage onPageLoaded"); + } + + onPageWillEnter(){ + console.log("ModalSecondPage onPageWillEnter"); + } + + onPageDidEnter(){ + console.log("ModalSecondPage onPageDidEnter"); + } } @@ -398,4 +467,4 @@ class FadeOut extends Transition { .before.addClass('show-page'); } } -Transition.register('my-fade-out', FadeOut); +Transition.register('my-fade-out', FadeOut); \ No newline at end of file diff --git a/src/components/modal/test/basic/main.html b/src/components/modal/test/basic/main.html index 693650afe4..7ce7623491 100644 --- a/src/components/modal/test/basic/main.html +++ b/src/components/modal/test/basic/main.html @@ -1,4 +1,3 @@ - Modals @@ -7,6 +6,9 @@

+

+ +

diff --git a/src/components/modal/test/modal.spec.ts b/src/components/modal/test/modal.spec.ts new file mode 100644 index 0000000000..a7fa498d34 --- /dev/null +++ b/src/components/modal/test/modal.spec.ts @@ -0,0 +1,100 @@ +import {Modal, ModalCmp, Page, NavController, ViewController} from '../../../../src'; + +export function run() { + describe('Modal', () => { + + describe('create', () => { + + it('should have the correct properties on modal view controller instance', () => { + let modalViewController = Modal.create(ComponentToPresent); + expect(modalViewController.modalViewType).toEqual("ComponentToPresent"); + expect(modalViewController.componentType).toEqual(ModalCmp); + expect(modalViewController.viewType).toEqual("modal"); + expect(modalViewController.isOverlay).toEqual(true); + expect(modalViewController instanceof ViewController).toEqual(true); + }); + }); + + describe('loaded', () => { + it('should call done after loading component and call original ngAfterViewInit method', (done) => { + // arrange + let modal = new Modal({}, {}); + let mockInstance = { + ngAfterViewInit: () => {}, + loadComponent: () => {} + }; + let mockComponentRef = { + instance: "someData" + }; + modal.instance = mockInstance; + + let ngAfterViewInitSpy = spyOn(mockInstance, "ngAfterViewInit"); + spyOn(mockInstance, "loadComponent").and.returnValue(Promise.resolve(mockComponentRef)); + + let doneCallback = () => { + // assert + expect(ngAfterViewInitSpy).toHaveBeenCalled(); + expect(modal.instance).toEqual("someData"); + done(); + }; + + // act + modal.loaded(doneCallback); + // (angular calls ngAfterViewInit, we're not testing angular so manually call it) + mockInstance.ngAfterViewInit(); + + }, 5000); + }); + }); + + describe('ModalCmp', () => { + + it('should return a componentRef object after loading component', (done) => { + // arrange + let mockLoader = { + loadNextToLocation: () => {} + }; + let mockNavParams = { + data: { + componentType: "myComponentType" + } + }; + let mockComponentRef = {}; + + spyOn(mockLoader, "loadNextToLocation").and.returnValue(Promise.resolve(mockComponentRef)); + let modalCmp = new ModalCmp(null, mockLoader, mockNavParams, null); + modalCmp.viewport = "mockViewport"; + + // act + modalCmp.loadComponent().then(loadedComponentRef => { + // assert + expect(loadedComponentRef).toEqual(mockComponentRef); + expect(mockLoader.loadNextToLocation).toHaveBeenCalledWith(mockNavParams.data.componentType, modalCmp.viewport); + done(); + }); + }, 5000); + }); +} + +const STATE_ACTIVE = 'active'; +const STATE_INACTIVE = 'inactive'; +const STATE_INIT_ENTER = 'init_enter'; +const STATE_INIT_LEAVE = 'init_leave'; +const STATE_TRANS_ENTER = 'trans_enter'; +const STATE_TRANS_LEAVE = 'trans_leave'; +const STATE_REMOVE = 'remove'; +const STATE_REMOVE_AFTER_TRANS = 'remove_after_trans'; +const STATE_FORCE_ACTIVE = 'force_active'; + + +let componentToPresentSpy = { + _ionicProjectContent: () => {}, +}; + +@Page({ + template: `
` +}) +class ComponentToPresent{ + constructor(){ + } +} diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index 3d2ac2d12a..34d3583866 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -818,10 +818,10 @@ export class NavController extends Ion { if (!parentNav['_tabs']) { // Tabs can be a parent, but it is not a collection of views // only we're looking for an actual NavController w/ stack of views - leavingView.willLeave(); + leavingView.fireWillLeave(); return parentNav.pop(opts).then((rtnVal: boolean) => { - leavingView.didLeave(); + leavingView.fireDidLeave(); return rtnVal; }); } @@ -918,7 +918,7 @@ export class NavController extends Ion { // set that it is the init leaving view // the first view to be removed, it should init leave view.state = STATE_INIT_LEAVE; - view.willUnload(); + view.fireWillUnload(); // from the index of the leaving view, go backwards and // find the first view that is inactive so it can be the entering @@ -951,8 +951,8 @@ export class NavController extends Ion { // remove views that have been set to be removed, but not // apart of any transitions that will eventually happen this._views.filter(v => v.state === STATE_REMOVE).forEach(view => { - view.willLeave(); - view.didLeave(); + view.fireWillLeave(); + view.fireDidLeave(); this._views.splice(this.indexOf(view), 1); view.destroy(); }); @@ -986,7 +986,7 @@ export class NavController extends Ion { if (!enteringView) { // if no entering view then create a bogus one enteringView = new ViewController(); - enteringView.loaded(); + enteringView.fireLoaded(); } /* Async steps to complete a transition @@ -1042,19 +1042,8 @@ export class NavController extends Ion { this.setTransitioning(true, 500); this.loadPage(enteringView, null, opts, () => { - if (enteringView.onReady) { - // this entering view needs to wait for it to be ready - // this is used by Tabs to wait for the first page of - // the first selected tab to be loaded - enteringView.onReady(() => { - enteringView.loaded(); - this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); - }); - - } else { - enteringView.loaded(); - this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); - } + enteringView.fireLoaded(); + this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); }); } } @@ -1112,13 +1101,13 @@ export class NavController extends Ion { if (leavingView.fireOtherLifecycles) { // only fire entering lifecycle if the leaving // view hasn't explicitly set not to - enteringView.willEnter(); + enteringView.fireWillEnter(); } if (enteringView.fireOtherLifecycles) { // only fire leaving lifecycle if the entering // view hasn't explicitly set not to - leavingView.willLeave(); + leavingView.fireWillLeave(); } } else { @@ -1224,13 +1213,13 @@ export class NavController extends Ion { if (leavingView.fireOtherLifecycles) { // only fire entering lifecycle if the leaving // view hasn't explicitly set not to - enteringView.didEnter(); + enteringView.fireDidEnter(); } if (enteringView.fireOtherLifecycles) { // only fire leaving lifecycle if the entering // view hasn't explicitly set not to - leavingView.didLeave(); + leavingView.fireDidLeave(); } } @@ -1440,56 +1429,61 @@ export class NavController extends Ion { // load the page component inside the nav this._loader.loadNextToLocation(view.componentType, this._viewport, providers).then(component => { - // the ElementRef of the actual ion-page created - let pageElementRef = component.location; - + // a new ComponentRef has been created // set the ComponentRef's instance to its ViewController view.setInstance(component.instance); + + // the component has been loaded, so call the view controller's loaded method to load any dependencies into the dom + view.loaded( () => { + + // the ElementRef of the actual ion-page created + let pageElementRef = component.location; - // remember the ChangeDetectorRef for this ViewController - view.setChangeDetector(component.changeDetectorRef); + // remember the ChangeDetectorRef for this ViewController + view.setChangeDetector(component.changeDetectorRef); - // remember the ElementRef to the ion-page elementRef that was just created - view.setPageRef(pageElementRef); + // remember the ElementRef to the ion-page elementRef that was just created + view.setPageRef(pageElementRef); - // auto-add page css className created from component JS class name - let cssClassName = pascalCaseToDashCase(view.componentType['name']); - this._renderer.setElementClass(pageElementRef.nativeElement, cssClassName, true); - - view.onDestroy(() => { - // ensure the element is cleaned up for when the view pool reuses this element - this._renderer.setElementAttribute(pageElementRef.nativeElement, 'class', null); - this._renderer.setElementAttribute(pageElementRef.nativeElement, 'style', null); - component.destroy(); - }); - - if (!navbarContainerRef) { - // there was not a navbar container ref already provided - // so use the location of the actual navbar template - navbarContainerRef = view.getNavbarViewRef(); - } - - // find a navbar template if one is in the page - let navbarTemplateRef = view.getNavbarTemplateRef(); - - // check if we have both a navbar ViewContainerRef and a template - if (navbarContainerRef && navbarTemplateRef) { - // let's now create the navbar view - let navbarViewRef = navbarContainerRef.createEmbeddedView(navbarTemplateRef); + // auto-add page css className created from component JS class name + let cssClassName = pascalCaseToDashCase(view.componentType['name']); + this._renderer.setElementClass(pageElementRef.nativeElement, cssClassName, true); view.onDestroy(() => { - // manually destroy the navbar when the page is destroyed - navbarViewRef.destroy(); + // ensure the element is cleaned up for when the view pool reuses this element + this._renderer.setElementAttribute(pageElementRef.nativeElement, 'class', null); + this._renderer.setElementAttribute(pageElementRef.nativeElement, 'style', null); + component.destroy(); }); - } - // options may have had a postLoad method - // used mainly by tabs - opts.postLoad && opts.postLoad(view); + if (!navbarContainerRef) { + // there was not a navbar container ref already provided + // so use the location of the actual navbar template + navbarContainerRef = view.getNavbarViewRef(); + } - // our job is done here - done(view); + // find a navbar template if one is in the page + let navbarTemplateRef = view.getNavbarTemplateRef(); + + // check if we have both a navbar ViewContainerRef and a template + if (navbarContainerRef && navbarTemplateRef) { + // let's now create the navbar view + let navbarViewRef = navbarContainerRef.createEmbeddedView(navbarTemplateRef); + + view.onDestroy(() => { + // manually destroy the navbar when the page is destroyed + navbarViewRef.destroy(); + }); + } + + // options may have had a postLoad method + // used mainly by tabs + opts.postLoad && opts.postLoad(view); + + // our job is done here + done(view); + }); }); } diff --git a/src/components/nav/test/nav-controller.spec.ts b/src/components/nav/test/nav-controller.spec.ts index 109f5955d4..2b2bf7a43c 100644 --- a/src/components/nav/test/nav-controller.spec.ts +++ b/src/components/nav/test/nav-controller.spec.ts @@ -269,20 +269,20 @@ export function run() { view4.state = STATE_ACTIVE; nav._views = [view1, view2, view3, view4]; - spyOn(view1, 'willLeave'); - spyOn(view1, 'didLeave'); + spyOn(view1, 'fireWillLeave'); + spyOn(view1, 'fireDidLeave'); spyOn(view1, 'destroy'); - spyOn(view2, 'willLeave'); - spyOn(view2, 'didLeave'); + spyOn(view2, 'fireWillLeave'); + spyOn(view2, 'fireDidLeave'); spyOn(view2, 'destroy'); - spyOn(view3, 'willLeave'); - spyOn(view3, 'didLeave'); + spyOn(view3, 'fireWillLeave'); + spyOn(view3, 'fireDidLeave'); spyOn(view3, 'destroy'); - spyOn(view4, 'willLeave'); - spyOn(view4, 'didLeave'); + spyOn(view4, 'fireWillLeave'); + spyOn(view4, 'fireDidLeave'); spyOn(view4, 'destroy'); nav._remove(1, 3); @@ -292,20 +292,20 @@ export function run() { expect(view3.state).toBe(STATE_REMOVE); expect(view4.state).toBe(STATE_INIT_LEAVE); - expect(view1.willLeave).not.toHaveBeenCalled(); - expect(view1.didLeave).not.toHaveBeenCalled(); + expect(view1.fireWillLeave).not.toHaveBeenCalled(); + expect(view1.fireDidLeave).not.toHaveBeenCalled(); expect(view1.destroy).not.toHaveBeenCalled(); - expect(view2.willLeave).toHaveBeenCalled(); - expect(view2.didLeave).toHaveBeenCalled(); + expect(view2.fireWillLeave).toHaveBeenCalled(); + expect(view2.fireDidLeave).toHaveBeenCalled(); expect(view2.destroy).toHaveBeenCalled(); - expect(view3.willLeave).toHaveBeenCalled(); - expect(view3.didLeave).toHaveBeenCalled(); + expect(view3.fireWillLeave).toHaveBeenCalled(); + expect(view3.fireDidLeave).toHaveBeenCalled(); expect(view3.destroy).toHaveBeenCalled(); - expect(view4.willLeave).not.toHaveBeenCalled(); - expect(view4.didLeave).not.toHaveBeenCalled(); + expect(view4.fireWillLeave).not.toHaveBeenCalled(); + expect(view4.fireDidLeave).not.toHaveBeenCalled(); expect(view4.destroy).not.toHaveBeenCalled(); }); }); @@ -412,11 +412,11 @@ export function run() { var done = () => {}; nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - spyOn(enteringView, 'willEnter'); + spyOn(enteringView, 'fireWillEnter'); nav._postRender(1, enteringView, leavingView, false, navOptions, done); - expect(enteringView.willEnter).toHaveBeenCalled(); + expect(enteringView.fireWillEnter).toHaveBeenCalled(); }); it('should not call willEnter on entering view when it is being preloaded', () => { @@ -428,11 +428,11 @@ export function run() { var done = () => {}; nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - spyOn(enteringView, 'willEnter'); + spyOn(enteringView, 'fireWillEnter'); nav._postRender(1, enteringView, leavingView, false, navOptions, done); - expect(enteringView.willEnter).not.toHaveBeenCalled(); + expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); }); it('should call willLeave on leaving view', () => { @@ -442,11 +442,11 @@ export function run() { var done = () => {}; nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - spyOn(leavingView, 'willLeave'); + spyOn(leavingView, 'fireWillLeave'); nav._postRender(1, enteringView, leavingView, false, navOptions, done); - expect(leavingView.willLeave).toHaveBeenCalled(); + expect(leavingView.fireWillLeave).toHaveBeenCalled(); }); it('should not call willEnter when the leaving view has fireOtherLifecycles not true', () => { @@ -456,15 +456,15 @@ export function run() { var done = () => {}; nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - spyOn(enteringView, 'willEnter'); - spyOn(leavingView, 'willLeave'); + spyOn(enteringView, 'fireWillEnter'); + spyOn(leavingView, 'fireWillLeave'); leavingView.fireOtherLifecycles = false; nav._postRender(1, enteringView, leavingView, false, navOptions, done); - expect(enteringView.willEnter).not.toHaveBeenCalled(); - expect(leavingView.willLeave).toHaveBeenCalled(); + expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); + expect(leavingView.fireWillLeave).toHaveBeenCalled(); }); it('should not call willLeave when the entering view has fireOtherLifecycles not true', () => { @@ -474,15 +474,15 @@ export function run() { var done = () => {}; nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - spyOn(enteringView, 'willEnter'); - spyOn(leavingView, 'willLeave'); + spyOn(enteringView, 'fireWillEnter'); + spyOn(leavingView, 'fireWillLeave'); enteringView.fireOtherLifecycles = false; nav._postRender(1, enteringView, leavingView, false, navOptions, done); - expect(enteringView.willEnter).toHaveBeenCalled(); - expect(leavingView.willLeave).not.toHaveBeenCalled(); + expect(enteringView.fireWillEnter).toHaveBeenCalled(); + expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); }); it('should not call willLeave on leaving view when it is being preloaded', () => { @@ -494,11 +494,11 @@ export function run() { var done = () => {}; nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - spyOn(leavingView, 'willLeave'); + spyOn(leavingView, 'fireWillLeave'); nav._postRender(1, enteringView, leavingView, false, navOptions, done); - expect(leavingView.willLeave).not.toHaveBeenCalled(); + expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); }); it('should set animate false when preloading', () => { @@ -725,13 +725,13 @@ export function run() { let doneCalled = false; let done = () => {doneCalled = true;} - spyOn(enteringView, 'didEnter'); - spyOn(leavingView, 'didLeave'); + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - expect(enteringView.didEnter).toHaveBeenCalled(); - expect(leavingView.didLeave).toHaveBeenCalled(); + expect(enteringView.fireDidEnter).toHaveBeenCalled(); + expect(leavingView.fireDidLeave).toHaveBeenCalled(); expect(doneCalled).toBe(true); }); @@ -745,13 +745,13 @@ export function run() { let doneCalled = false; let done = () => {doneCalled = true;} - spyOn(enteringView, 'didEnter'); - spyOn(leavingView, 'didLeave'); + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - expect(enteringView.didEnter).not.toHaveBeenCalled(); - expect(leavingView.didLeave).not.toHaveBeenCalled(); + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); expect(doneCalled).toBe(true); }); @@ -765,13 +765,13 @@ export function run() { enteringView.fireOtherLifecycles = false; - spyOn(enteringView, 'didEnter'); - spyOn(leavingView, 'didLeave'); + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - expect(enteringView.didEnter).toHaveBeenCalled(); - expect(leavingView.didLeave).not.toHaveBeenCalled(); + expect(enteringView.fireDidEnter).toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); expect(doneCalled).toBe(true); }); @@ -785,13 +785,13 @@ export function run() { leavingView.fireOtherLifecycles = false; - spyOn(enteringView, 'didEnter'); - spyOn(leavingView, 'didLeave'); + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - expect(enteringView.didEnter).not.toHaveBeenCalled(); - expect(leavingView.didLeave).toHaveBeenCalled(); + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).toHaveBeenCalled(); expect(doneCalled).toBe(true); }); @@ -803,13 +803,13 @@ export function run() { let doneCalled = false; let done = () => {doneCalled = true;} - spyOn(enteringView, 'didEnter'); - spyOn(leavingView, 'didLeave'); + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - expect(enteringView.didEnter).not.toHaveBeenCalled(); - expect(leavingView.didLeave).not.toHaveBeenCalled(); + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); expect(doneCalled).toBe(true); }); diff --git a/src/components/nav/test/view-controller.spec.ts b/src/components/nav/test/view-controller.spec.ts new file mode 100644 index 0000000000..680e29997f --- /dev/null +++ b/src/components/nav/test/view-controller.spec.ts @@ -0,0 +1,133 @@ +import {LifeCycleEvent, ViewController} from '../../../../src'; + +export function run() { + describe('ViewController', () => { + + afterEach(() => { + if ( subscription ){ + subscription.unsubscribe(); + } + }); + + describe('loaded', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.didLoad.subscribe((event:LifeCycleEvent) => { + // assert + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.fireLoaded(); + }, 10000); + }); + + describe('willEnter', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.willEnter.subscribe((event:LifeCycleEvent) => { + // assert + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.fireWillEnter(); + }, 10000); + }); + + describe('didEnter', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.didEnter.subscribe((event:LifeCycleEvent) => { + // assert + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.fireDidEnter(); + }, 10000); + }); + + describe('willLeave', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.willLeave.subscribe((event:LifeCycleEvent) => { + // assert + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.fireWillLeave(); + }, 10000); + }); + + describe('didLeave', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.didLeave.subscribe((event:LifeCycleEvent) => { + // assert + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.fireDidLeave(); + }, 10000); + }); + + describe('willUnload', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.willUnload.subscribe((event:LifeCycleEvent) => { + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.fireWillUnload(); + }, 10000); + }); + + describe('destroy', () => { + it('should emit LifeCycleEvent when called with component data', (done) => { + // arrange + let viewController = new ViewController(FakePage); + subscription = viewController.didUnload.subscribe((event:LifeCycleEvent) => { + // assert + expect(event).toEqual(null); + done(); + }, err => { + done(err); + }); + + // act + viewController.destroy(); + }, 10000); + }); + }); + + let subscription = null; + class FakePage{} +} diff --git a/src/components/nav/view-controller.ts b/src/components/nav/view-controller.ts index 6936e2be5d..5365970817 100644 --- a/src/components/nav/view-controller.ts +++ b/src/components/nav/view-controller.ts @@ -37,6 +37,14 @@ export class ViewController { private _cd: ChangeDetectorRef; protected _nav: NavController; + didLoad: EventEmitter; + willEnter: EventEmitter; + didEnter: EventEmitter; + willLeave: EventEmitter; + didLeave: EventEmitter; + willUnload: EventEmitter; + didUnload: EventEmitter; + /** * @private */ @@ -62,11 +70,6 @@ export class ViewController { */ viewType: string = ''; - /** - * @private - */ - onReady: Function; - /** * @private * If this is currently the active view, then set to false @@ -97,6 +100,14 @@ export class ViewController { constructor(public componentType?: Type, data?: any) { // passed in data could be NavParams, but all we care about is its data object this.data = (data instanceof NavParams ? data.data : (isPresent(data) ? data : {})); + + this.didLoad = new EventEmitter(); + this.willEnter = new EventEmitter(); + this.didEnter = new EventEmitter(); + this.willLeave = new EventEmitter(); + this.didLeave = new EventEmitter(); + this.willUnload = new EventEmitter(); + this.didUnload = new EventEmitter(); } subscribe(generatorOrNext?: any): any { @@ -472,6 +483,16 @@ export class ViewController { isLoaded(): boolean { return this._loaded; } + /** + * The loaded method is used to load any dynamic content/components + * into the dom before proceeding with the transition. If a component needs + * dynamic component loading, extending ViewController and overriding + * this method is a good option + * @param {function} done is a callback that must be called when async loading/actions are completed + */ + loaded(done: (() => any)) { + done(); + } /** * @private @@ -481,8 +502,9 @@ export class ViewController { * to put your setup code for the view; however, it is not the * recommended method to use when a view becomes active. */ - loaded() { + fireLoaded() { this._loaded = true; + this.didLoad.emit(null); ctrlFn(this, 'onPageLoaded'); } @@ -490,7 +512,7 @@ export class ViewController { * @private * The view is about to enter and become the active view. */ - willEnter() { + fireWillEnter() { if (this._cd) { // ensure this has been re-attached to the change detector this._cd.reattach(); @@ -498,7 +520,7 @@ export class ViewController { // detect changes before we run any user code this._cd.detectChanges(); } - + this.willEnter.emit(null); ctrlFn(this, 'onPageWillEnter'); } @@ -507,9 +529,10 @@ export class ViewController { * The view has fully entered and is now the active view. This * will fire, whether it was the first load or loaded from the cache. */ - didEnter() { + fireDidEnter() { let navbar = this.getNavbar(); navbar && navbar.didEnter(); + this.didEnter.emit(null); ctrlFn(this, 'onPageDidEnter'); } @@ -517,7 +540,8 @@ export class ViewController { * @private * The view has is about to leave and no longer be the active view. */ - willLeave() { + fireWillLeave() { + this.willLeave.emit(null); ctrlFn(this, 'onPageWillLeave'); } @@ -526,7 +550,8 @@ export class ViewController { * The view has finished leaving and is no longer the active view. This * will fire, whether it is cached or unloaded. */ - didLeave() { + fireDidLeave() { + this.didLeave.emit(null); ctrlFn(this, 'onPageDidLeave'); // when this is not the active page @@ -538,7 +563,8 @@ export class ViewController { * @private * The view is about to be destroyed and have its elements removed. */ - willUnload() { + fireWillUnload() { + this.willUnload.emit(null); ctrlFn(this, 'onPageWillUnload'); } @@ -553,6 +579,7 @@ export class ViewController { * @private */ destroy() { + this.didUnload.emit(null); ctrlFn(this, 'onPageDidUnload'); for (var i = 0; i < this._destroys.length; i++) { @@ -564,6 +591,10 @@ export class ViewController { } +export interface LifeCycleEvent { + componentType?: any; +} + function ctrlFn(viewCtrl: ViewController, fnName: string) { if (viewCtrl.instance && viewCtrl.instance[fnName]) { try { diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 65eecf694e..14a3e25041 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -252,7 +252,7 @@ export class Tabs extends Ion { viewCtrl.setContent(this); viewCtrl.setContentRef(_elementRef); - viewCtrl.onReady = (done) => { + viewCtrl.loaded = (done) => { this._onReady = done; }; } @@ -357,11 +357,11 @@ export class Tabs extends Ion { let deselectedPage; if (deselectedTab) { deselectedPage = deselectedTab.getActive(); - deselectedPage && deselectedPage.willLeave(); + deselectedPage && deselectedPage.fireWillLeave(); } let selectedPage = selectedTab.getActive(); - selectedPage && selectedPage.willEnter(); + selectedPage && selectedPage.fireWillEnter(); selectedTab.load(opts, () => { @@ -382,8 +382,8 @@ export class Tabs extends Ion { } } - selectedPage && selectedPage.didEnter(); - deselectedPage && deselectedPage.didLeave(); + selectedPage && selectedPage.fireDidEnter(); + deselectedPage && deselectedPage.fireDidLeave(); if (this._onReady) { this._onReady();