mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 01:52:19 +08:00
refactor(modal): use framework delegate for mounting the user's component
This commit is contained in:
@ -1,65 +1,33 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Directive,
|
||||
ElementRef,
|
||||
Injector,
|
||||
ReflectiveInjector,
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { FrameworkDelegate } from '@ionic/core';
|
||||
|
||||
import { getProviders } from '../di/di';
|
||||
import { AngularComponentMounter } from '../providers/angular-component-mounter';
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
const elementToComponentRefMap = new Map<HTMLElement, ComponentRef<any>>();
|
||||
|
||||
@Component({
|
||||
@Directive({
|
||||
selector: 'ion-nav',
|
||||
template: `
|
||||
<div #viewport class="ng-nav-viewport"></div>
|
||||
`
|
||||
})
|
||||
export class IonNavDelegate implements FrameworkDelegate {
|
||||
|
||||
@ViewChild('viewport', { read: ViewContainerRef}) viewport: ViewContainerRef;
|
||||
|
||||
constructor(private elementRef: ElementRef, private changeDetection: ChangeDetectorRef, private angularComponentMounter: AngularComponentMounter, private injector: Injector, private componentResolveFactory: ComponentFactoryResolver) {
|
||||
constructor(private elementRef: ElementRef, private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
|
||||
this.elementRef.nativeElement.delegate = this;
|
||||
|
||||
}
|
||||
|
||||
async attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>,
|
||||
_propsOrDataObj?: any, _classesToAdd?: string[]): Promise<AngularMountingData> {
|
||||
attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
|
||||
|
||||
const componentProviders = ReflectiveInjector.resolve(getProviders(elementOrContainerToMountTo));
|
||||
console.log('componentProviders: ', componentProviders);
|
||||
|
||||
const element = document.createElement('ion-page');
|
||||
for (const clazz of _classesToAdd) {
|
||||
element.classList.add(clazz);
|
||||
}
|
||||
|
||||
elementOrContainerToMountTo.appendChild(element);
|
||||
const mountingData = await this.angularComponentMounter.attachViewToDom(element, elementOrComponentToMount, [], this.changeDetection, this.componentResolveFactory, this.injector);
|
||||
mountingData.element = element;
|
||||
|
||||
elementToComponentRefMap.set(mountingData.angularHostElement, mountingData.componentRef);
|
||||
|
||||
return mountingData;
|
||||
const hostElement = document.createElement('div');
|
||||
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, hostElement, elementOrComponentToMount, this.componentResolveFactory, this.injector, classesToAdd);
|
||||
}
|
||||
|
||||
async removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
const componentRef = elementToComponentRefMap.get(childElement);
|
||||
if (componentRef) {
|
||||
return this.angularComponentMounter.removeViewFromDom(componentRef);
|
||||
}
|
||||
return Promise.resolve();
|
||||
removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
return this.angularComponentMounter.removeViewFromDom(childElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,10 +8,16 @@ export const NavControllerToken = new InjectionToken<any>('NavControllerToken');
|
||||
export const ViewControllerToken = new InjectionToken<any>('ViewControllerToken');
|
||||
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 [
|
||||
{
|
||||
provide: NavControllerToken, useValue: element
|
||||
provide: NavControllerToken, useValue: nearestNavElement
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -1,10 +1,13 @@
|
||||
export { IonNavDelegate } from './components/ion-nav';
|
||||
export { IonicAngularModule } from './module';
|
||||
|
||||
/* Directives/Components */
|
||||
export { IonNavDelegate } from './components/ion-nav';
|
||||
|
||||
/* Providers */
|
||||
export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller';
|
||||
export { AlertController, AlertProxy } from './providers/alert-controller';
|
||||
export { App } from './providers/app';
|
||||
export { LoadingController, LoadingProxy } from './providers/loading-controller';
|
||||
export { ModalController, ModalProxy } from './providers/modal-controller';
|
||||
export { NavController } from './providers/nav-controller';
|
||||
export { ToastController, ToastProxy } from './providers/toast-controller';
|
@ -1,3 +1,4 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ModuleWithProviders,
|
||||
NgModule,
|
||||
@ -11,6 +12,7 @@ import { SelectValueAccessor } from './control-value-accessors/select-value-acce
|
||||
import { TextValueAccessor } from './control-value-accessors/text-value-accessor';
|
||||
|
||||
|
||||
/* Components */
|
||||
import { IonNavDelegate } from './components/ion-nav';
|
||||
|
||||
/* Providers */
|
||||
@ -18,6 +20,7 @@ import { ActionSheetController } from './providers/action-sheet-controller';
|
||||
import { AlertController } from './providers/alert-controller';
|
||||
import { AngularComponentMounter } from './providers/angular-component-mounter';
|
||||
import { LoadingController } from './providers/loading-controller';
|
||||
import { ModalController } from './providers/modal-controller';
|
||||
import { ToastController } from './providers/toast-controller';
|
||||
|
||||
@NgModule({
|
||||
@ -37,6 +40,9 @@ import { ToastController } from './providers/toast-controller';
|
||||
SelectValueAccessor,
|
||||
TextValueAccessor
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
],
|
||||
@ -50,6 +56,7 @@ export class IonicAngularModule {
|
||||
ActionSheetController,
|
||||
AngularComponentMounter,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
ToastController
|
||||
]
|
||||
};
|
||||
|
@ -1,51 +1,70 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
ApplicationRef,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Injectable,
|
||||
Injector,
|
||||
NgZone,
|
||||
ReflectiveInjector,
|
||||
Type,
|
||||
Type
|
||||
} from '@angular/core';
|
||||
|
||||
import { getProviders } from '../di/di';
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
const elementToComponentRefMap = new Map<HTMLElement, ComponentRef<any>>();
|
||||
|
||||
@Injectable()
|
||||
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) => {
|
||||
this.zone.run(() => {
|
||||
console.log('parentElement: ', parentElement);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> {
|
||||
removeViewFromDom(childElement: HTMLElement): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
componentRef.destroy();
|
||||
removeViewFromDom(childElement);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, element: HTMLElement, providers: any, changeDetection: ChangeDetectorRef, injector: Injector): AngularMountingData {
|
||||
const componentFactory = crf.resolveComponentFactory(componentToMount);
|
||||
const componentProviders = ReflectiveInjector.resolve(providers);
|
||||
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector);
|
||||
const componentRef = componentFactory.create(childInjector, [], element);
|
||||
export function removeViewFromDom(childElement: HTMLElement) {
|
||||
const componentRef = elementToComponentRefMap.get(childElement);
|
||||
if (componentRef) {
|
||||
componentRef.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
componentFactory,
|
||||
@ -53,7 +72,7 @@ export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount:
|
||||
componentRef: componentRef,
|
||||
instance: componentRef.instance,
|
||||
angularHostElement: componentRef.location.nativeElement,
|
||||
element: componentRef.location.nativeElement,
|
||||
element: hostElement,
|
||||
};
|
||||
}
|
||||
|
||||
|
134
packages/angular/src/providers/modal-controller.ts
Normal file
134
packages/angular/src/providers/modal-controller.ts
Normal 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';
|
||||
|
@ -12,4 +12,4 @@ export interface AngularMountingData extends FrameworkMountingData {
|
||||
componentRef?: ComponentRef<any>;
|
||||
instance?: any;
|
||||
angularHostElement?: HTMLElement;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user