@ionic/angular
Version:
Angular specific wrappers for @ionic/core
229 lines • 10.3 kB
JavaScript
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