mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Feature/observable ext (#6670)
* refactor(nav-controller): refactor to better support dynamic component loading
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
$modal-md-background-color: $background-md-color !default;
|
||||
|
||||
|
||||
.modal ion-page {
|
||||
.modal-wrapper {
|
||||
background-color: $modal-md-background-color;
|
||||
}
|
||||
|
||||
@@ -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<any>) => {
|
||||
this.setInstance(componentRef.instance);
|
||||
done();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -139,20 +159,22 @@ export class Modal extends ViewController {
|
||||
'<div #viewport></div>' +
|
||||
'</div>'
|
||||
})
|
||||
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<ComponentRef<any>> {
|
||||
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 = <HTMLElement> ele.querySelector('ion-page');
|
||||
page.classList.add('show-page');
|
||||
|
||||
// auto-add page css className created from component JS class name
|
||||
let cssClassName = pascalCaseToDashCase((<Modal>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 = <HTMLElement> 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 = <HTMLElement> ele.querySelector('ion-page');
|
||||
page.classList.add('show-page');
|
||||
|
||||
// auto-add page css className created from component JS class name
|
||||
let cssClassName = pascalCaseToDashCase((<Modal>enteringView).modalViewType);
|
||||
page.classList.add(cssClassName);
|
||||
|
||||
this
|
||||
.element(enteringView.pageRef())
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
$modal-wp-background-color: $background-wp-color !default;
|
||||
|
||||
|
||||
.modal ion-page {
|
||||
.modal-wrapper {
|
||||
background-color: $modal-wp-background-color;
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<ion-navbar *navbar>
|
||||
<ion-title>Page One</ion-title>
|
||||
</ion-navbar>
|
||||
<ion-content>
|
||||
<button full (click)="submit()">Submit</button>
|
||||
</ion-content>
|
||||
`
|
||||
})
|
||||
class NavigableModal{
|
||||
constructor(private navController:NavController){
|
||||
}
|
||||
|
||||
submit(){
|
||||
this.navController.push(NavigableModal2);
|
||||
}
|
||||
}
|
||||
|
||||
@Page({
|
||||
template: `
|
||||
<ion-navbar *navbar>
|
||||
<ion-title>Page Two</ion-title>
|
||||
</ion-navbar>
|
||||
<ion-content>
|
||||
<button full (click)="submit()">Submit</button>
|
||||
</ion-content>
|
||||
`
|
||||
})
|
||||
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);
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
<ion-navbar *navbar>
|
||||
<ion-title>Modals</ion-title>
|
||||
</ion-navbar>
|
||||
@@ -7,6 +6,9 @@
|
||||
<p>
|
||||
<button (click)="presentModal()">Present modal, pass params</button>
|
||||
</p>
|
||||
<p>
|
||||
<button (click)="presentNavigableModal()">Present modal, push page</button>
|
||||
</p>
|
||||
<p>
|
||||
<button class="e2eOpenModal" (click)="presentModalChildNav()">Present modal w/ child ion-nav</button>
|
||||
</p>
|
||||
|
||||
100
src/components/modal/test/modal.spec.ts
Normal file
100
src/components/modal/test/modal.spec.ts
Normal file
@@ -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: `<div class="myComponent"></div>`
|
||||
})
|
||||
class ComponentToPresent{
|
||||
constructor(){
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user