import { ApplicationRef, NgZone, Injectable, Injector, EnvironmentInjector, inject, createComponent, InjectionToken, ComponentRef, } from '@angular/core'; import { FrameworkDelegate, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_WILL_UNLOAD, } from '@ionic/core'; import { NavParams } from '../directives/navigation/nav-params'; // TODO(FW-2827): types @Injectable() export class AngularDelegate { private zone = inject(NgZone); private applicationRef = inject(ApplicationRef); create( environmentInjector: EnvironmentInjector, injector: Injector, elementReferenceKey?: string ): AngularFrameworkDelegate { return new AngularFrameworkDelegate( environmentInjector, injector, this.applicationRef, this.zone, elementReferenceKey ); } } export class AngularFrameworkDelegate implements FrameworkDelegate { private elRefMap = new WeakMap>(); private elEventsMap = new WeakMap void>(); constructor( private environmentInjector: EnvironmentInjector, private injector: Injector, private applicationRef: ApplicationRef, private zone: NgZone, private elementReferenceKey?: string ) {} attachViewToDom(container: any, component: any, params?: any, cssClasses?: string[]): Promise { return this.zone.run(() => { return new Promise((resolve) => { const componentProps = { ...params, }; /** * Ionic Angular passes a reference to a modal * or popover that can be accessed using a * variable in the overlay component. If * elementReferenceKey is defined, then we should * pass a reference to the component using * elementReferenceKey as the key. */ if (this.elementReferenceKey !== undefined) { componentProps[this.elementReferenceKey] = container; } const el = attachView( this.zone, this.environmentInjector, this.injector, this.applicationRef, this.elRefMap, this.elEventsMap, container, component, componentProps, cssClasses ); resolve(el); }); }); } removeViewFromDom(_container: any, component: any): Promise { return this.zone.run(() => { return new Promise((resolve) => { const componentRef = this.elRefMap.get(component); if (componentRef) { componentRef.destroy(); this.elRefMap.delete(component); const unbindEvents = this.elEventsMap.get(component); if (unbindEvents) { unbindEvents(); this.elEventsMap.delete(component); } } resolve(); }); }); } } export const attachView = ( zone: NgZone, environmentInjector: EnvironmentInjector, injector: Injector, applicationRef: ApplicationRef, elRefMap: WeakMap>, elEventsMap: WeakMap void>, container: any, component: any, params: any, cssClasses: string[] | undefined ): any => { /** * Wraps the injector with a custom injector that * provides NavParams to the component. * * NavParams is a legacy feature from Ionic v3 that allows * Angular developers to provide data to a component * and access it by providing NavParams as a dependency * in the constructor. * * The modern approach is to access the data directly * from the component's class instance. */ const childInjector = Injector.create({ providers: getProviders(params), parent: injector, }); const componentRef = createComponent(component, { environmentInjector, elementInjector: childInjector, }); const instance = componentRef.instance; const hostElement = componentRef.location.nativeElement; if (params) { Object.assign(instance, params); } if (cssClasses) { for (const cssClass of cssClasses) { hostElement.classList.add(cssClass); } } const unbindEvents = bindLifecycleEvents(zone, instance, hostElement); container.appendChild(hostElement); applicationRef.attachView(componentRef.hostView); elRefMap.set(hostElement, componentRef); elEventsMap.set(hostElement, unbindEvents); return hostElement; }; const LIFECYCLES = [ LIFECYCLE_WILL_ENTER, LIFECYCLE_DID_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_UNLOAD, ]; export const bindLifecycleEvents = (zone: NgZone, instance: any, element: HTMLElement): (() => void) => { return zone.run(() => { const unregisters = LIFECYCLES.filter((eventName) => typeof instance[eventName] === 'function').map((eventName) => { const handler = (ev: any) => instance[eventName](ev.detail); element.addEventListener(eventName, handler); return () => element.removeEventListener(eventName, handler); }); return () => unregisters.forEach((fn) => fn()); }); }; const NavParamsToken = new InjectionToken('NavParamsToken'); const getProviders = (params: { [key: string]: any }) => { return [ { provide: NavParamsToken, useValue: params, }, { provide: NavParams, useFactory: provideNavParamsInjectable, deps: [NavParamsToken], }, ]; }; const provideNavParamsInjectable = (params: { [key: string]: any }) => { return new NavParams(params); };