UNPKG

@ionic/angular

Version:

Angular specific wrappers for @ionic/core

229 lines 10.3 kB
import { ApplicationRef, createComponent, inject, Injectable, InjectionToken, Injector, NgZone, } from '@angular/core'; import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_WILL_UNLOAD, } from '@ionic/core/components'; import { NavParams } from '../directives/navigation/nav-params'; import { ConfigToken } from './config'; import * as i0 from "@angular/core"; // Token for injecting the modal element export const IonModalToken = new InjectionToken('IonModalToken'); // TODO(FW-2827): types export class AngularDelegate { zone = inject(NgZone); applicationRef = inject(ApplicationRef); config = inject(ConfigToken); create(environmentInjector, injector, elementReferenceKey, customInjector) { return new AngularFrameworkDelegate(environmentInjector, injector, this.applicationRef, this.zone, elementReferenceKey, this.config.useSetInputAPI ?? false, customInjector); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AngularDelegate, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AngularDelegate }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: AngularDelegate, decorators: [{ type: Injectable }] }); export class AngularFrameworkDelegate { environmentInjector; injector; applicationRef; zone; elementReferenceKey; enableSignalsSupport; customInjector; elRefMap = new WeakMap(); elEventsMap = new WeakMap(); constructor(environmentInjector, injector, applicationRef, zone, elementReferenceKey, enableSignalsSupport, customInjector) { this.environmentInjector = environmentInjector; this.injector = injector; this.applicationRef = applicationRef; this.zone = zone; this.elementReferenceKey = elementReferenceKey; this.enableSignalsSupport = enableSignalsSupport; this.customInjector = customInjector; } attachViewToDom(container, component, params, cssClasses) { 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, this.elementReferenceKey, this.enableSignalsSupport, this.customInjector); resolve(el); }); }); } removeViewFromDom(_container, component) { 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, environmentInjector, injector, applicationRef, elRefMap, elEventsMap, container, component, params, cssClasses, elementReferenceKey, enableSignalsSupport, customInjector) => { /** * 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 providers = getProviders(params); // If this is an ion-modal, provide the modal element as an injectable // so components inside the modal can inject it directly if (container.tagName.toLowerCase() === 'ion-modal') { providers.push({ provide: IonModalToken, useValue: container, }); } const childInjector = Injector.create({ providers, parent: customInjector ?? injector, }); const componentRef = createComponent(component, { environmentInjector, elementInjector: childInjector, }); const instance = componentRef.instance; const hostElement = componentRef.location.nativeElement; if (params) { /** * For modals and popovers, a reference to the component is * added to `params` during the call to attachViewToDom. If * a reference using this name is already set, this means * the app is trying to use the name as a component prop, * which will cause collisions. */ if (elementReferenceKey && instance[elementReferenceKey] !== undefined) { console.error(`[Ionic Error]: ${elementReferenceKey} is a reserved property when using ${container.tagName.toLowerCase()}. Rename or remove the "${elementReferenceKey}" property from ${component.name}.`); } /** * Angular 14.1 added support for setInput * so we need to fall back to Object.assign * for Angular 14.0. */ if (enableSignalsSupport === true && componentRef.setInput !== undefined) { const { modal, popover, ...otherParams } = params; /** * Any key/value pairs set in componentProps * must be set as inputs on the component instance. */ for (const key in otherParams) { componentRef.setInput(key, otherParams[key]); } /** * Using setInput will cause an error when * setting modal/popover on a component that * does not define them as an input. For backwards * compatibility purposes we fall back to using * Object.assign for these properties. */ if (modal !== undefined) { Object.assign(instance, { modal }); } if (popover !== undefined) { Object.assign(instance, { popover }); } } else { Object.assign(instance, params); } } if (cssClasses) { for (const cssClass of cssClasses) { hostElement.classList.add(cssClass); } } const unbindEvents = bindLifecycleEvents(zone, componentRef.changeDetectorRef, instance, hostElement); container.appendChild(hostElement); applicationRef.attachView(componentRef.hostView); /** * Run change detection on the freshly attached view so Angular's init hooks * (`ngOnInit`, `ngAfterViewInit`) fire and template bindings (e.g. * `<ion-nav [root]="rootPage">`) apply during this synchronous pass, before the * web component runs its load lifecycle and dispatches its Ionic lifecycle events * (`ionViewWillEnter`, etc.). `createComponent` only runs the creation pass; the * init hooks and binding updates run during an update pass. Under Zone.js an * implicit tick used to cover this, but zoneless Angular schedules no such tick, * so without this the binding could land after the element has loaded (too late * for `ion-nav` to read it) and the first `ionViewWillEnter` could run before * `ngOnInit`. */ componentRef.changeDetectorRef.detectChanges(); 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, changeDetectorRef, instance, element) => { /** * `zone.run` keeps the listener registration (and, under Zone.js, the handler * execution) inside the Angular zone, so async work started inside a lifecycle * hook is still zone-tracked. Under zoneless Angular it is a passthrough. */ return zone.run(() => { const unregisters = LIFECYCLES.filter((eventName) => typeof instance[eventName] === 'function').map((eventName) => { const handler = (ev) => { instance[eventName](ev.detail); /** * Ionic lifecycle events (`ionViewWillEnter`, etc.) are dispatched from * the web component via a native event listener, so under zoneless * Angular nothing schedules change detection for state the hook mutates. * Mark the view dirty explicitly. This is a no-op-or-better under Zone.js. */ changeDetectorRef.markForCheck(); }; element.addEventListener(eventName, handler); return () => element.removeEventListener(eventName, handler); }); return () => unregisters.forEach((fn) => fn()); }); }; const NavParamsToken = new InjectionToken('NavParamsToken'); const getProviders = (params) => { return [ { provide: NavParamsToken, useValue: params, }, { provide: NavParams, useFactory: provideNavParamsInjectable, deps: [NavParamsToken], }, ]; }; const provideNavParamsInjectable = (params) => { return new NavParams(params); }; //# sourceMappingURL=angular-delegate.js.map