refactor(modal): use framework delegate for mounting the user's component

This commit is contained in:
Dan Bucholtz
2017-12-14 00:10:52 -06:00
parent c30337bf8c
commit ec33d4e725
35 changed files with 546 additions and 148 deletions

View File

@ -1,65 +1,33 @@
import { import {
ChangeDetectorRef,
Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ComponentRef, Directive,
ElementRef, ElementRef,
Injector, Injector,
ReflectiveInjector,
Type, Type,
ViewContainerRef,
ViewChild
} from '@angular/core'; } from '@angular/core';
import { FrameworkDelegate } from '@ionic/core'; import { FrameworkDelegate } from '@ionic/core';
import { getProviders } from '../di/di';
import { AngularComponentMounter } from '../providers/angular-component-mounter'; import { AngularComponentMounter } from '../providers/angular-component-mounter';
import { AngularMountingData } from '../types/interfaces'; import { AngularMountingData } from '../types/interfaces';
const elementToComponentRefMap = new Map<HTMLElement, ComponentRef<any>>(); @Directive({
@Component({
selector: 'ion-nav', selector: 'ion-nav',
template: `
<div #viewport class="ng-nav-viewport"></div>
`
}) })
export class IonNavDelegate implements FrameworkDelegate { export class IonNavDelegate implements FrameworkDelegate {
@ViewChild('viewport', { read: ViewContainerRef}) viewport: ViewContainerRef; constructor(private elementRef: ElementRef, private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
constructor(private elementRef: ElementRef, private changeDetection: ChangeDetectorRef, private angularComponentMounter: AngularComponentMounter, private injector: Injector, private componentResolveFactory: ComponentFactoryResolver) {
this.elementRef.nativeElement.delegate = this; this.elementRef.nativeElement.delegate = this;
} }
async attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>, attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
_propsOrDataObj?: any, _classesToAdd?: string[]): Promise<AngularMountingData> {
const componentProviders = ReflectiveInjector.resolve(getProviders(elementOrContainerToMountTo)); const hostElement = document.createElement('div');
console.log('componentProviders: ', componentProviders); return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, hostElement, elementOrComponentToMount, this.componentResolveFactory, this.injector, classesToAdd);
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;
} }
async removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) { removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
const componentRef = elementToComponentRefMap.get(childElement); return this.angularComponentMounter.removeViewFromDom(childElement);
if (componentRef) {
return this.angularComponentMounter.removeViewFromDom(componentRef);
}
return Promise.resolve();
} }
} }

View File

@ -8,10 +8,16 @@ export const NavControllerToken = new InjectionToken<any>('NavControllerToken');
export const ViewControllerToken = new InjectionToken<any>('ViewControllerToken'); export const ViewControllerToken = new InjectionToken<any>('ViewControllerToken');
export const AppToken = new InjectionToken<any>('AppToken'); export const AppToken = new InjectionToken<any>('AppToken');
export function getProviders(element: HTMLIonNavElement) { export function getProviders(element: HTMLElement) {
if (element.tagName !== 'ion-nav') {
element.closest('ion-nav');
}
const nearestNavElement = (element.tagName.toLowerCase() === 'ion-nav' ? element : element.closest('ion-nav')) as HTMLIonNavElement;
return [ return [
{ {
provide: NavControllerToken, useValue: element provide: NavControllerToken, useValue: nearestNavElement
}, },
{ {

View File

@ -1,10 +1,13 @@
export { IonNavDelegate } from './components/ion-nav';
export { IonicAngularModule } from './module'; export { IonicAngularModule } from './module';
/* Directives/Components */
export { IonNavDelegate } from './components/ion-nav';
/* Providers */
export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller'; export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller';
export { AlertController, AlertProxy } from './providers/alert-controller'; export { AlertController, AlertProxy } from './providers/alert-controller';
export { App } from './providers/app'; export { App } from './providers/app';
export { LoadingController, LoadingProxy } from './providers/loading-controller'; export { LoadingController, LoadingProxy } from './providers/loading-controller';
export { ModalController, ModalProxy } from './providers/modal-controller';
export { NavController } from './providers/nav-controller'; export { NavController } from './providers/nav-controller';
export { ToastController, ToastProxy } from './providers/toast-controller'; export { ToastController, ToastProxy } from './providers/toast-controller';

View File

@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common';
import { import {
ModuleWithProviders, ModuleWithProviders,
NgModule, NgModule,
@ -11,6 +12,7 @@ import { SelectValueAccessor } from './control-value-accessors/select-value-acce
import { TextValueAccessor } from './control-value-accessors/text-value-accessor'; import { TextValueAccessor } from './control-value-accessors/text-value-accessor';
/* Components */
import { IonNavDelegate } from './components/ion-nav'; import { IonNavDelegate } from './components/ion-nav';
/* Providers */ /* Providers */
@ -18,6 +20,7 @@ import { ActionSheetController } from './providers/action-sheet-controller';
import { AlertController } from './providers/alert-controller'; import { AlertController } from './providers/alert-controller';
import { AngularComponentMounter } from './providers/angular-component-mounter'; import { AngularComponentMounter } from './providers/angular-component-mounter';
import { LoadingController } from './providers/loading-controller'; import { LoadingController } from './providers/loading-controller';
import { ModalController } from './providers/modal-controller';
import { ToastController } from './providers/toast-controller'; import { ToastController } from './providers/toast-controller';
@NgModule({ @NgModule({
@ -37,6 +40,9 @@ import { ToastController } from './providers/toast-controller';
SelectValueAccessor, SelectValueAccessor,
TextValueAccessor TextValueAccessor
], ],
imports: [
CommonModule,
],
schemas: [ schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA
], ],
@ -50,6 +56,7 @@ export class IonicAngularModule {
ActionSheetController, ActionSheetController,
AngularComponentMounter, AngularComponentMounter,
LoadingController, LoadingController,
ModalController,
ToastController ToastController
] ]
}; };

View File

@ -1,51 +1,70 @@
import { import {
ChangeDetectorRef, ApplicationRef,
ComponentFactoryResolver, ComponentFactoryResolver,
ComponentRef, ComponentRef,
Injectable, Injectable,
Injector, Injector,
NgZone, NgZone,
ReflectiveInjector, ReflectiveInjector,
Type, Type
} from '@angular/core'; } from '@angular/core';
import { getProviders } from '../di/di';
import { AngularMountingData } from '../types/interfaces'; import { AngularMountingData } from '../types/interfaces';
const elementToComponentRefMap = new Map<HTMLElement, ComponentRef<any>>();
@Injectable() @Injectable()
export class AngularComponentMounter { export class AngularComponentMounter {
constructor(private defaultCfr: ComponentFactoryResolver, private zone: NgZone) { constructor(private defaultCfr: ComponentFactoryResolver, private zone: NgZone, private appRef: ApplicationRef) {
} }
attachViewToDom(parentElement: HTMLElement, componentToMount: Type<any>, providers: any[], changeDetection: ChangeDetectorRef, componentResolveFactory: ComponentFactoryResolver, injector: Injector): Promise<AngularMountingData> { attachViewToDom(parentElement: HTMLElement, hostElement: HTMLElement, componentToMount: Type<any>, componentResolveFactory: ComponentFactoryResolver, injector: Injector, classesToAdd: string[]): Promise<AngularMountingData> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.zone.run(() => { this.zone.run(() => {
console.log('parentElement: ', parentElement);
const crf = componentResolveFactory ? componentResolveFactory : this.defaultCfr; const crf = componentResolveFactory ? componentResolveFactory : this.defaultCfr;
const mountingData = attachViewToDom(crf, componentToMount, parentElement, providers, changeDetection, injector);
const mountingData = attachViewToDom(crf, parentElement, hostElement, componentToMount, injector, this.appRef, classesToAdd);
resolve(mountingData); resolve(mountingData);
}); });
}); });
} }
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> { removeViewFromDom(childElement: HTMLElement): Promise<any> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.zone.run(() => { this.zone.run(() => {
componentRef.destroy(); removeViewFromDom(childElement);
resolve(); resolve();
}); });
}); });
} }
} }
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, element: HTMLElement, providers: any, changeDetection: ChangeDetectorRef, injector: Injector): AngularMountingData { export function removeViewFromDom(childElement: HTMLElement) {
const componentFactory = crf.resolveComponentFactory(componentToMount); const componentRef = elementToComponentRefMap.get(childElement);
const componentProviders = ReflectiveInjector.resolve(providers); if (componentRef) {
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector); componentRef.destroy();
const componentRef = componentFactory.create(childInjector, [], element); }
}
changeDetection.detectChanges(); export function attachViewToDom(crf: ComponentFactoryResolver, parentElement: HTMLElement, hostElement: HTMLElement, componentToMount: Type<any>, injector: Injector, appRef: ApplicationRef, classesToAdd: string[]): AngularMountingData {
const componentProviders = ReflectiveInjector.resolve(getProviders(parentElement));
const componentFactory = crf.resolveComponentFactory(componentToMount);
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector);
const componentRef = componentFactory.create(childInjector, [], hostElement);
for (const clazz of classesToAdd) {
hostElement.classList.add(clazz);
}
parentElement.appendChild(hostElement);
appRef.attachView(componentRef.hostView);
elementToComponentRefMap.set(hostElement, componentRef);
return { return {
componentFactory, componentFactory,
@ -53,7 +72,7 @@ export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount:
componentRef: componentRef, componentRef: componentRef,
instance: componentRef.instance, instance: componentRef.instance,
angularHostElement: componentRef.location.nativeElement, angularHostElement: componentRef.location.nativeElement,
element: componentRef.location.nativeElement, element: hostElement,
}; };
} }

View File

@ -0,0 +1,134 @@
import {
ComponentFactoryResolver,
Injectable,
Injector,
Type,
} from '@angular/core';
import {
FrameworkDelegate,
ModalDismissEvent,
ModalOptions
} from '@ionic/core';
import { AngularComponentMounter } from '../providers/angular-component-mounter';
import { AngularMountingData } from '../types/interfaces';
import { ensureElementInBody, hydrateElement } from '../util/util';
let modalId = 0;
@Injectable()
export class ModalController implements FrameworkDelegate {
constructor(private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
}
create(opts?: ModalOptions): ModalProxy {
opts.delegate = this;
return getModalProxy(opts);
}
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
const hostElement = document.createElement('div');
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, hostElement, elementOrComponentToMount, this.componentResolveFactory, this.injector, classesToAdd);
}
removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
return this.angularComponentMounter.removeViewFromDom(childElement);
}
}
export function getModalProxy(opts: ModalOptions) {
return {
id: modalId++,
state: PRESENTING,
opts: opts,
present: function() { return present(this); },
dismiss: function() { return dismiss(this); },
onDidDismiss: function(callback: (data: any, role: string) => void) {
(this as ModalProxyInternal).onDidDismissHandler = callback;
},
onWillDismiss: function(callback: (data: any, role: string) => void) {
(this as ModalProxyInternal).onWillDismissHandler = callback;
},
};
}
export function present(modalProxy: ModalProxyInternal): Promise<any> {
modalProxy.state = PRESENTING;
return loadOverlay(modalProxy.opts).then((modalElement: HTMLIonModalElement) => {
Object.assign(modalElement, modalProxy.opts);
modalProxy.element = modalElement;
const onDidDismissHandler = (event: ModalDismissEvent) => {
modalElement.removeEventListener(ION_MODAL_DID_DISMISS_EVENT, onDidDismissHandler);
if (modalProxy.onDidDismissHandler) {
modalProxy.onDidDismissHandler(event.detail.data, event.detail.role);
}
};
const onWillDismissHandler = (event: ModalDismissEvent) => {
modalElement.removeEventListener(ION_MODAL_WILL_DISMISS_EVENT, onWillDismissHandler);
if (modalProxy.onWillDismissHandler) {
modalProxy.onWillDismissHandler(event.detail.data, event.detail.role);
}
};
modalElement.addEventListener(ION_MODAL_DID_DISMISS_EVENT, onDidDismissHandler);
modalElement.addEventListener(ION_MODAL_WILL_DISMISS_EVENT, onWillDismissHandler);
if (modalProxy.state === PRESENTING) {
return modalElement.present();
}
// we'll only ever get here if someone tried to dismiss the overlay or mess with it's internal state
// attribute before it could async load and present itself.
// with that in mind, just return null to make the TS compiler happy
return null;
});
}
export function dismiss(modalProxy: ModalProxyInternal): Promise<any> {
modalProxy.state = DISMISSING;
if (modalProxy.element) {
if (modalProxy.state === DISMISSING) {
return modalProxy.element.dismiss();
}
}
// either we're not in the dismissing state
// or we're calling this before the element is created
// so just return a resolved promise
return Promise.resolve();
}
export function loadOverlay(opts: ModalOptions): Promise<HTMLIonModalElement> {
const element = ensureElementInBody('ion-modal-controller') as HTMLIonModalControllerElement;
return hydrateElement(element).then(() => {
return element.create(opts);
});
}
export interface ModalProxy {
present(): Promise<void>;
dismiss(): Promise<void>;
onDidDismiss(callback: (data: any, role: string) => void): void;
onWillDismiss(callback: (data: any, role: string) => void): void;
}
export interface ModalProxyInternal extends ModalProxy {
id: number;
opts: ModalOptions;
state: number;
element: HTMLIonModalElement;
onDidDismissHandler?: (data: any, role: string) => void;
onWillDismissHandler?: (data: any, role: string) => void;
}
export const PRESENTING = 1;
export const DISMISSING = 2;
const ION_MODAL_DID_DISMISS_EVENT = 'ionModalDidDismiss';
const ION_MODAL_WILL_DISMISS_EVENT = 'ionModalWillDismiss';

View File

@ -12,4 +12,4 @@ export interface AngularMountingData extends FrameworkMountingData {
componentRef?: ComponentRef<any>; componentRef?: ComponentRef<any>;
instance?: any; instance?: any;
angularHostElement?: HTMLElement; angularHostElement?: HTMLElement;
} }

View File

@ -1630,6 +1630,7 @@ declare global {
enterAnimation?: AnimationBuilder; enterAnimation?: AnimationBuilder;
leaveAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder;
animate?: boolean; animate?: boolean;
delegate?: FrameworkDelegate;
} }
} }
} }

View File

@ -22,7 +22,6 @@ export class ActionSheetController {
// give this action sheet a unique id // give this action sheet a unique id
actionSheet.actionSheetId = `action-sheet-${id}`; actionSheet.actionSheetId = `action-sheet-${id}`;
actionSheet.style.zIndex = (20000 + id).toString();
// convert the passed in action sheet options into props // convert the passed in action sheet options into props
// that get passed down into the new action sheet // that get passed down into the new action sheet

View File

@ -125,6 +125,8 @@ export class ActionSheet {
} }
this.ionActionSheetWillPresent.emit(); this.ionActionSheetWillPresent.emit();
this.el.style.zIndex = `${20000 + this.actionSheetId}`;
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('actionSheetEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); const animationBuilder = this.enterAnimation || this.config.get('actionSheetEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);

View File

@ -23,7 +23,6 @@ export class AlertController {
// give this action sheet a unique id // give this action sheet a unique id
alert.alertId = `alert-${id}`; alert.alertId = `alert-${id}`;
alert.style.zIndex = (20000 + id).toString();
// convert the passed in action sheet options into props // convert the passed in action sheet options into props
// that get passed down into the new action sheet // that get passed down into the new action sheet

View File

@ -131,6 +131,8 @@ export class Alert {
} }
this.ionAlertWillPresent.emit(); this.ionAlertWillPresent.emit();
this.el.style.zIndex = `${20000 + this.alertId}`;
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('alertEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); const animationBuilder = this.enterAnimation || this.config.get('alertEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);

View File

@ -22,7 +22,6 @@ export class LoadingController {
// give this loading a unique id // give this loading a unique id
loading.loadingId = `loading-${id}`; loading.loadingId = `loading-${id}`;
loading.style.zIndex = (20000 + id).toString();
// convert the passed in loading options into props // convert the passed in loading options into props
// that get passed down into the new loading // that get passed down into the new loading

View File

@ -129,6 +129,8 @@ export class Loading {
this.ionLoadingWillPresent.emit(); this.ionLoadingWillPresent.emit();
this.el.style.zIndex = `${20000 + this.loadingId}`;
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('loadingEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); const animationBuilder = this.enterAnimation || this.config.get('loadingEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);

View File

@ -20,7 +20,6 @@ export class ModalController {
// give this modal a unique id // give this modal a unique id
modal.modalId = `modal-${id}`; modal.modalId = `modal-${id}`;
modal.style.zIndex = (10000 + id).toString();
// convert the passed in modal options into props // convert the passed in modal options into props
// that get passed down into the new modal // that get passed down into the new modal

View File

@ -4,9 +4,12 @@ import {
AnimationBuilder, AnimationBuilder,
AnimationController, AnimationController,
Config, Config,
FrameworkDelegate,
OverlayDismissEvent, OverlayDismissEvent,
OverlayDismissEventDetail OverlayDismissEventDetail
} from '../../index'; } from '../../index';
import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate';
import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; import { domControllerAsync, playAnimationAsync } from '../../utils/helpers';
import { createThemedClasses } from '../../utils/theme'; import { createThemedClasses } from '../../utils/theme';
@ -73,12 +76,13 @@ export class Modal {
@Prop() enterAnimation: AnimationBuilder; @Prop() enterAnimation: AnimationBuilder;
@Prop() leaveAnimation: AnimationBuilder; @Prop() leaveAnimation: AnimationBuilder;
@Prop() animate: boolean; @Prop() animate: boolean;
@Prop({ mutable: true }) delegate: FrameworkDelegate;
private animation: Animation; private animation: Animation;
private usersComponentElement: HTMLElement;
@Method() @Method()
present() { async present() {
if (this.animation) { if (this.animation) {
this.animation.destroy(); this.animation.destroy();
this.animation = null; this.animation = null;
@ -86,27 +90,39 @@ export class Modal {
this.ionModalWillPresent.emit(); this.ionModalWillPresent.emit();
this.el.style.zIndex = `${20000 + this.modalId}`;
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('modalEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); const animationBuilder = this.enterAnimation || this.config.get('modalEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);
// build the animation and kick it off const userComponentParent = this.el.querySelector(`.${USER_COMPONENT_MODAL_CONTAINER_CLASS}`);
// build the animation and kick it off if (!this.delegate) {
return this.animationCtrl.create(animationBuilder, this.el).then(animation => { this.delegate = new DomFrameworkDelegate();
this.animation = animation; }
if (!this.animate) {
// if the duration is 0, it won't actually animate I don't think const cssClasses = ['ion-page'];
// TODO - validate this if (this.cssClass && this.cssClass.length) {
this.animation = animation.duration(0); cssClasses.push(this.cssClass);
} }
return playAnimationAsync(animation);
}).then((animation) => { // add the modal by default to the data being passed
animation.destroy(); this.data = this.data || {};
this.ionModalDidPresent.emit(); this.data.modal = this.el;
}); const mountingData = await this.delegate.attachViewToDom(userComponentParent, this.component, this.data, cssClasses);
this.usersComponentElement = mountingData.element;
this.animation = await this.animationCtrl.create(animationBuilder, this.el);
if (!this.animate) {
// if the duration is 0, it won't actually animate I don't think
// TODO - validate this
this.animation = this.animation.duration(0);
}
await playAnimationAsync(this.animation);
this.animation.destroy();
this.ionModalDidPresent.emit();
} }
@Method() @Method()
dismiss(data?: any, role?: string) { async dismiss(data?: any, role?: string) {
if (this.animation) { if (this.animation) {
this.animation.destroy(); this.animation.destroy();
this.animation = null; this.animation = null;
@ -116,25 +132,37 @@ export class Modal {
role role
}); });
if (!this.delegate) {
this.delegate = new DomFrameworkDelegate();
}
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.leaveAnimation || this.config.get('modalLeave', this.mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation); const animationBuilder = this.leaveAnimation || this.config.get('modalLeave', this.mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation);
return this.animationCtrl.create(animationBuilder, this.el).then(animation => { this.animation = await this.animationCtrl.create(animationBuilder, this.el);
this.animation = animation; await playAnimationAsync(this.animation);
return playAnimationAsync(animation); this.animation.destroy();
}).then((animation) => {
animation.destroy();
return domControllerAsync(Context.dom.write, () => { await domControllerAsync(Context.dom.write, () => {});
this.el.parentNode.removeChild(this.el);
}); // TODO - Figure out how to make DOM controller work with callbacks that return a promise or are async
}).then(() => { const userComponentParent = this.el.querySelector(`.${USER_COMPONENT_MODAL_CONTAINER_CLASS}`);
this.ionModalDidDismiss.emit({ await this.delegate.removeViewFromDom(userComponentParent, this.usersComponentElement);
data,
role this.el.parentElement.removeChild(this.el);
});
this.ionModalDidDismiss.emit({
data,
role
}); });
} }
@Method()
getUserComponentContainer(): HTMLElement {
return this.el.querySelector(`.${USER_COMPONENT_MODAL_CONTAINER_CLASS}`);
}
@Listen('ionDismiss') @Listen('ionDismiss')
protected onDismiss(ev: UIEvent) { protected onDismiss(ev: UIEvent) {
ev.stopPropagation(); ev.stopPropagation();
@ -188,6 +216,7 @@ export interface ModalOptions {
enterAnimation?: AnimationBuilder; enterAnimation?: AnimationBuilder;
exitAnimation?: AnimationBuilder; exitAnimation?: AnimationBuilder;
cssClass?: string; cssClass?: string;
delegate?: FrameworkDelegate;
} }
@ -213,3 +242,5 @@ export {
mdEnterAnimation as mdModalEnterAnimation, mdEnterAnimation as mdModalEnterAnimation,
mdLeaveAnimation as mdModalLeaveAnimation mdLeaveAnimation as mdModalLeaveAnimation
}; };
export const USER_COMPONENT_MODAL_CONTAINER_CLASS = 'modal-wrapper';

View File

@ -32,6 +32,11 @@ string
any any
#### delegate
any
#### enableBackdropDismiss #### enableBackdropDismiss
boolean boolean
@ -89,6 +94,11 @@ string
any any
#### delegate
any
#### enableBackdropDismiss #### enableBackdropDismiss
boolean boolean
@ -144,6 +154,9 @@ boolean
#### dismiss() #### dismiss()
#### getUserComponentContainer()
#### present() #### present()

View File

@ -28,10 +28,21 @@
<script> <script>
async function presentModal() { async function presentModal() {
const element = document.createElement('div');
element.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Cool Modal</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<div>Sure, I like dogs. I like caravans moar.</div>
</ion-content>
`;
const modalController = document.querySelector('ion-modal-controller'); const modalController = document.querySelector('ion-modal-controller');
await modalController.componentOnReady(); await modalController.componentOnReady();
const modalElement = await modalController.create({ const modalElement = await modalController.create({
component: 'page-one' component: element
}); });
modalElement.present(); modalElement.present();
} }

View File

@ -31,7 +31,7 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie
if (enteringView) { if (enteringView) {
const enteringContent = rootTransition.create(); const enteringContent = rootTransition.create();
enteringContent.addElement(enteringView.element.querySelectorAll('ion-header > *:not(ion-navbar),ion-footer > *')); enteringContent.addElement(enteringView.element.querySelectorAll('ion-header > *:not(ion-toolbar),ion-footer > *'));
rootTransition.add(enteringContent); rootTransition.add(enteringContent);
@ -42,30 +42,30 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie
enteringContent.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); enteringContent.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true);
} }
const enteringNavbarEle = enteringView.element.querySelector('ion-navbar'); const enteringToolBarEle = enteringView.element.querySelector('ion-toolbar');
if (enteringNavbarEle) { if (enteringToolBarEle) {
const enteringNavBar = rootTransition.create(); const enteringToolBar = rootTransition.create();
enteringNavBar.addElement(enteringNavbarEle); enteringToolBar.addElement(enteringToolBarEle);
rootTransition.add(enteringNavBar); rootTransition.add(enteringToolBar);
const enteringTitle = rootTransition.create(); const enteringTitle = rootTransition.create();
enteringTitle.addElement(enteringNavbarEle.querySelector('ion-title')); enteringTitle.addElement(enteringToolBarEle.querySelector('ion-title'));
const enteringNavbarItems = rootTransition.create(); const enteringToolBarItems = rootTransition.create();
enteringNavbarItems.addElement(enteringNavbarEle.querySelectorAll('ion-buttons,[menuToggle]')); enteringToolBarItems.addElement(enteringToolBarEle.querySelectorAll('ion-buttons,[menuToggle]'));
const enteringNavbarBg = rootTransition.create(); const enteringToolBarBg = rootTransition.create();
enteringNavbarBg.addElement(enteringNavbarEle.querySelector('.toolbar-background')); enteringToolBarBg.addElement(enteringToolBarEle.querySelector('.toolbar-background'));
const enteringBackButton = rootTransition.create(); const enteringBackButton = rootTransition.create();
enteringBackButton.addElement(enteringNavbarEle.querySelector('.back-button')); enteringBackButton.addElement(enteringToolBarEle.querySelector('.back-button'));
enteringNavBar enteringToolBar
.add(enteringTitle) .add(enteringTitle)
.add(enteringNavbarItems) .add(enteringToolBarItems)
.add(enteringNavbarBg) .add(enteringToolBarBg)
.add(enteringBackButton); .add(enteringBackButton);
enteringTitle.fromTo(OPACITY, 0.01, 1, true); enteringTitle.fromTo(OPACITY, 0.01, 1, true);
enteringNavbarItems.fromTo(OPACITY, 0.01, 1, true); enteringToolBarItems.fromTo(OPACITY, 0.01, 1, true);
if (backDirection) { if (backDirection) {
enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true);
@ -75,10 +75,10 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie
enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS).fromTo(OPACITY, 0.01, 1, true); enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS).fromTo(OPACITY, 0.01, 1, true);
} }
} else { } else {
// entering navbar, forward direction // entering toolbar, forward direction
enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true);
enteringNavbarBg.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); enteringToolBarBg.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true);
if (canNavGoBack(enteringView.nav)) { if (canNavGoBack(enteringView.nav)) {
// forward direction, entering page has a back button // forward direction, entering page has a back button
@ -86,10 +86,10 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie
const enteringBackBtnText = rootTransition.create(); const enteringBackBtnText = rootTransition.create();
enteringBackBtnText.addElement(enteringNavbarEle.querySelector('.back-button-text')); enteringBackBtnText.addElement(enteringToolBarEle.querySelector('.back-button-text'));
enteringBackBtnText.fromTo(TRANSLATEX, (isRTL ? '-100px' : '100px'), '0px'); enteringBackBtnText.fromTo(TRANSLATEX, (isRTL ? '-100px' : '100px'), '0px');
enteringNavBar.add(enteringBackBtnText); enteringToolBar.add(enteringBackBtnText);
} else { } else {
enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS);
@ -103,7 +103,7 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie
const leavingContent = rootTransition.create(); const leavingContent = rootTransition.create();
leavingContent.addElement(leavingView.element); leavingContent.addElement(leavingView.element);
leavingContent.addElement(leavingView.element.querySelectorAll('ion-header > *:not(ion-navbar),ion-footer > *')); leavingContent.addElement(leavingView.element.querySelectorAll('ion-header > *:not(ion-toolbar),ion-footer > *'));
rootTransition.add(leavingContent); rootTransition.add(leavingContent);
@ -119,59 +119,59 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie
.afterClearStyles([TRANSFORM, OPACITY]); .afterClearStyles([TRANSFORM, OPACITY]);
} }
const leavingNavbarEle = leavingView.element.querySelector('ion-navbar'); const leavingToolBarEle = leavingView.element.querySelector('ion-toolbar');
if (leavingNavbarEle) { if (leavingToolBarEle) {
const leavingNavBar = rootTransition.create(); const leavingToolBar = rootTransition.create();
leavingNavBar.addElement(leavingNavbarEle); leavingToolBar.addElement(leavingToolBarEle);
const leavingTitle = rootTransition.create(); const leavingTitle = rootTransition.create();
leavingTitle.addElement(leavingNavbarEle.querySelector('ion-title')); leavingTitle.addElement(leavingToolBarEle.querySelector('ion-title'));
const leavingNavbarItems = rootTransition.create(); const leavingToolBarItems = rootTransition.create();
leavingNavbarItems.addElement(leavingNavbarEle.querySelectorAll('ion-buttons,[menuToggle]')); leavingToolBarItems.addElement(leavingToolBarEle.querySelectorAll('ion-buttons,[menuToggle]'));
const leavingNavbarBg = rootTransition.create(); const leavingToolBarBg = rootTransition.create();
leavingNavbarBg.addElement(leavingNavbarEle.querySelector('.toolbar-background')); leavingToolBarBg.addElement(leavingToolBarEle.querySelector('.toolbar-background'));
const leavingBackButton = rootTransition.create(); const leavingBackButton = rootTransition.create();
leavingBackButton.addElement(leavingNavbarEle.querySelector('.back-button')); leavingBackButton.addElement(leavingToolBarEle.querySelector('.back-button'));
leavingNavBar leavingToolBar
.add(leavingTitle) .add(leavingTitle)
.add(leavingNavbarItems) .add(leavingToolBarItems)
.add(leavingBackButton) .add(leavingBackButton)
.add(leavingNavbarBg); .add(leavingToolBarBg);
this.add(leavingNavBar); this.add(leavingToolBar);
// fade out leaving navbar items // fade out leaving toolbar items
leavingBackButton.fromTo(OPACITY, 0.99, 0); leavingBackButton.fromTo(OPACITY, 0.99, 0);
leavingTitle.fromTo(OPACITY, 0.99, 0); leavingTitle.fromTo(OPACITY, 0.99, 0);
leavingNavbarItems.fromTo(OPACITY, 0.99, 0); leavingToolBarItems.fromTo(OPACITY, 0.99, 0);
if (backDirection) { if (backDirection) {
// leaving navbar, back direction // leaving toolbar, back direction
leavingTitle.fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); leavingTitle.fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%'));
// leaving navbar, back direction, and there's no entering navbar // leaving toolbar, back direction, and there's no entering toolbar
// should just slide out, no fading out // should just slide out, no fading out
leavingNavbarBg leavingToolBarBg
.beforeClearStyles([OPACITY]) .beforeClearStyles([OPACITY])
.fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); .fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%'));
const leavingBackBtnText = rootTransition.create(); const leavingBackBtnText = rootTransition.create();
leavingBackBtnText.addElement(leavingNavbarEle.querySelector('.back-button-text')); leavingBackBtnText.addElement(leavingToolBarEle.querySelector('.back-button-text'));
leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (isRTL ? -300 : 300) + 'px'); leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (isRTL ? -300 : 300) + 'px');
leavingNavBar.add(leavingBackBtnText); leavingToolBar.add(leavingBackBtnText);
} else { } else {
// leaving navbar, forward direction // leaving toolbar, forward direction
leavingTitle leavingTitle
.fromTo(TRANSLATEX, CENTER, OFF_LEFT) .fromTo(TRANSLATEX, CENTER, OFF_LEFT)
.afterClearStyles([TRANSFORM]); .afterClearStyles([TRANSFORM]);
leavingBackButton.afterClearStyles([OPACITY]); leavingBackButton.afterClearStyles([OPACITY]);
leavingTitle.afterClearStyles([OPACITY]); leavingTitle.afterClearStyles([OPACITY]);
leavingNavbarItems.afterClearStyles([OPACITY]); leavingToolBarItems.afterClearStyles([OPACITY]);
} }
} }
} }

View File

@ -27,14 +27,14 @@ export function buildMdTransition(rootTransition: Transition, enteringView: View
.fromTo('opacity', 0.01, 1, true); .fromTo('opacity', 0.01, 1, true);
} }
const enteringNavbarEle = enteringView.element.querySelector('ion-navbar'); const enteringToolbarEle = enteringView.element.querySelector('ion-toolbar');
if (enteringNavbarEle) { if (enteringToolbarEle) {
const enteringNavBar = rootTransition.create(); const enteringToolBar = rootTransition.create();
enteringNavBar.addElement(enteringNavbarEle); enteringToolBar.addElement(enteringToolbarEle);
rootTransition.add(enteringNavBar); rootTransition.add(enteringToolBar);
const enteringBackButton = rootTransition.create(); const enteringBackButton = rootTransition.create();
enteringBackButton.addElement(enteringNavbarEle.querySelector('.back-button')); enteringBackButton.addElement(enteringToolbarEle.querySelector('.back-button'));
rootTransition.add(enteringBackButton); rootTransition.add(enteringBackButton);
if (canNavGoBack(enteringView.nav)) { if (canNavGoBack(enteringView.nav)) {

View File

@ -19,7 +19,6 @@ export class PickerController {
// give this picker a unique id // give this picker a unique id
picker.pickerId = `picker-${id}`; picker.pickerId = `picker-${id}`;
picker.style.zIndex = (20000 + id).toString();
// convert the passed in picker options into props // convert the passed in picker options into props
// that get passed down into the new picker // that get passed down into the new picker

View File

@ -89,6 +89,8 @@ export class Picker {
this.ionPickerWillPresent.emit(); this.ionPickerWillPresent.emit();
this.el.style.zIndex = `${20000 + this.pickerId}`;
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('pickerEnter', iosEnterAnimation); const animationBuilder = this.enterAnimation || this.config.get('pickerEnter', iosEnterAnimation);

View File

@ -21,7 +21,6 @@ export class PopoverController {
// give this popover a unique id // give this popover a unique id
popover.popoverId = `popover-${id}`; popover.popoverId = `popover-${id}`;
popover.style.zIndex = (10000 + id).toString();
// convert the passed in popover options into props // convert the passed in popover options into props
// that get passed down into the new popover // that get passed down into the new popover

View File

@ -86,6 +86,8 @@ export class Popover {
} }
this.ionPopoverWillPresent.emit(); this.ionPopoverWillPresent.emit();
this.el.style.zIndex = `${10000 + this.popoverId}`;
// get the user's animation fn if one was provided // get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('popoverEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); const animationBuilder = this.enterAnimation || this.config.get('popoverEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);
@ -249,4 +251,3 @@ export {
mdEnterAnimation as mdPopoverEnterAnimation, mdEnterAnimation as mdPopoverEnterAnimation,
mdLeaveAnimation as mdPopoverLeaveAnimation mdLeaveAnimation as mdPopoverLeaveAnimation
}; };

View File

@ -10,7 +10,8 @@ const routes: Routes = [
{ path: 'actionSheet', loadChildren: 'app/action-sheet/action-sheet.module#ActionSheetModule' }, { path: 'actionSheet', loadChildren: 'app/action-sheet/action-sheet.module#ActionSheetModule' },
{ path: 'toast', loadChildren: 'app/toast/toast.module#ToastModule' }, { 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' } { path: 'nav', loadChildren: 'app/nav/nav.module#NavModule' },
{ path: 'modal', loadChildren: 'app/modal/modal.module#ModalModule' }
]; ];
@NgModule({ @NgModule({

View File

@ -24,4 +24,7 @@
<li> <li>
<a href='nav'>Nav Page</a> <a href='nav'>Nav Page</a>
</li> </li>
<li>
<a href='modal'>Modal Page</a>
</li>
</ul> </ul>

View File

@ -0,0 +1,35 @@
import { Component } from '@angular/core';
@Component({
selector: 'page-one',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Page One</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
Page One
<ul>
<li>ngOnInit - {{ngOnInitDetection}}</li>
</ul>
</ion-content>
`
})
export class ModalPageToPresent {
ngOnInitDetection = 'initial';
constructor() {
}
ngOnInit() {
console.log('page one ngOnInit');
setInterval(() => {
this.ngOnInitDetection = '' + Date.now();
}, 500);
}
}

View File

@ -0,0 +1,35 @@
import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { ModalPageToPresent } from './modal-page-to-present';
@Component({
selector: 'app-modal-page',
template: `
<ion-app>
<ion-page class="show-page">
<ion-header>
<ion-toolbar>
<ion-title>Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-button (click)="clickMe()">Open Basic Modal</ion-button>
</ion-content>
</ion-page>
</ion-app>
`
})
export class ModalPageComponent {
constructor(private modalController: ModalController) {
}
clickMe() {
const modal = this.modalController.create({
component: ModalPageToPresent
});
return modal.present();
}
}

View File

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

View File

@ -0,0 +1,27 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicAngularModule } from '@ionic/angular';
import { ModalPageComponent } from './modal-page.component';
import { ModalRoutingModule } from './modal-routing.module';
import { ModalPageToPresent } from './modal-page-to-present';
@NgModule({
imports: [
CommonModule,
IonicAngularModule.forRoot(),
ModalRoutingModule
],
declarations: [
ModalPageComponent,
ModalPageToPresent
],
providers: [
],
entryComponents: [
ModalPageToPresent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ModalModule { }

View File

@ -0,0 +1,49 @@
import React, { Component } from 'react';
import PageThree from './PageThree';
export default class ModalPage extends Component {
constructor() {
super();
this.style = {
height: '100%'
};
this.state = {
content: 'modal page - ' + 50
}
}
componentDidMount() {
console.log('componentDidMount');
setInterval(() => {
this.setState({ content: 'Modal page - ' + Math.random() * 1000});
}, 1000);
}
dismiss() {
return this.props.modal.dismiss();
}
render() {
return [
<ion-header ref={(element) => this.element = element}>
<ion-toolbar>
<ion-title>Modal Page</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content>
Modal Page
<div>
<ion-button onClick={() => this.dismiss()}>Dismiss</ion-button>
</div>
<div>
Some random content: {this.state.content}
</div>
<div>
Props : {this.props.paramOne}
</div>
</ion-content>
];
}
}

View File

@ -1,6 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { createModal } from '@ionic/react';
import PageTwo from './PageTwo'; import PageTwo from './PageTwo';
import ModalPage from './ModalPage';
export default class PageOne extends Component { export default class PageOne extends Component {
@ -35,6 +38,14 @@ export default class PageOne extends Component {
nav.push(PageTwo, { paramOne: 'Tobey Flenderson'}); nav.push(PageTwo, { paramOne: 'Tobey Flenderson'});
} }
openModal() {
return createModal({
component: ModalPage
}).then((modal) => {
return modal.present();
});
}
componentDidMount() { componentDidMount() {
setInterval(() => { setInterval(() => {
this.setState({ content: Math.random() * 1000}); this.setState({ content: Math.random() * 1000});
@ -54,6 +65,9 @@ export default class PageOne extends Component {
<div> <div>
<ion-button onClick={() => this.goToPageTwo()}>Go to Page Two</ion-button> <ion-button onClick={() => this.goToPageTwo()}>Go to Page Two</ion-button>
</div> </div>
<div>
<ion-button onClick={() => this.openModal()}>OpenModal</ion-button>
</div>
<div> <div>
Some random content: {this.state.content} Some random content: {this.state.content}
</div> </div>

View File

@ -0,0 +1,13 @@
import { ModalOptions } from '@ionic/core';
import { Delegate } from '../react-framework-delegate';
import { getOrAppendElement } from '../utils/helpers';
export function createModal(opts: ModalOptions): Promise<HTMLIonModalElement> {
opts.delegate = Delegate;
const element = getOrAppendElement('ion-modal-controller') as HTMLIonModalControllerElement;
return (element as any).componentOnReady().then(() => {
return element.create(opts);
});
}

View File

@ -1,2 +1,3 @@
export { Delegate } from './react-framework-delegate'; export { Delegate } from './react-framework-delegate';
export { createModal } from './apis/modal';
export * from './utils/wc-shim'; export * from './utils/wc-shim';

View File

@ -0,0 +1,10 @@
export function getOrAppendElement(tagName: string): Element {
const element = document.querySelector(tagName);
if (element) {
return element;
}
const tmp = document.createElement(tagName);
document.body.appendChild(tmp);
return tmp;
}