package.fesm2022.elements.mjs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elements Show documentation
Show all versions of elements Show documentation
Angular - library for using Angular Components as Custom Elements
/**
* @license Angular v18.2.7
* (c) 2010-2024 Google LLC. https://angular.io/
* License: MIT
*/
import { ComponentFactoryResolver, NgZone, Injector, ChangeDetectorRef, ApplicationRef, SimpleChange, Version } from '@angular/core';
import { ReplaySubject, merge } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';
/**
* Provide methods for scheduling the execution of a callback.
*/
const scheduler = {
/**
* Schedule a callback to be called after some delay.
*
* Returns a function that when executed will cancel the scheduled function.
*/
schedule(taskFn, delay) {
const id = setTimeout(taskFn, delay);
return () => clearTimeout(id);
},
/**
* Schedule a callback to be called before the next render.
* (If `window.requestAnimationFrame()` is not available, use `scheduler.schedule()` instead.)
*
* Returns a function that when executed will cancel the scheduled function.
*/
scheduleBeforeRender(taskFn) {
// TODO(gkalpak): Implement a better way of accessing `requestAnimationFrame()`
// (e.g. accounting for vendor prefix, SSR-compatibility, etc).
if (typeof window === 'undefined') {
// For SSR just schedule immediately.
return scheduler.schedule(taskFn, 0);
}
if (typeof window.requestAnimationFrame === 'undefined') {
const frameMs = 16;
return scheduler.schedule(taskFn, frameMs);
}
const id = window.requestAnimationFrame(taskFn);
return () => window.cancelAnimationFrame(id);
},
};
/**
* Convert a camelCased string to kebab-cased.
*/
function camelToDashCase(input) {
return input.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
}
/**
* Check whether the input is an `Element`.
*/
function isElement(node) {
return !!node && node.nodeType === Node.ELEMENT_NODE;
}
/**
* Check whether the input is a function.
*/
function isFunction(value) {
return typeof value === 'function';
}
/**
* Convert a kebab-cased string to camelCased.
*/
function kebabToCamelCase(input) {
return input.replace(/-([a-z\d])/g, (_, char) => char.toUpperCase());
}
let _matches;
/**
* Check whether an `Element` matches a CSS selector.
* NOTE: this is duplicated from @angular/upgrade, and can
* be consolidated in the future
*/
function matchesSelector(el, selector) {
if (!_matches) {
const elProto = Element.prototype;
_matches =
elProto.matches ||
elProto.matchesSelector ||
elProto.mozMatchesSelector ||
elProto.msMatchesSelector ||
elProto.oMatchesSelector ||
elProto.webkitMatchesSelector;
}
return el.nodeType === Node.ELEMENT_NODE ? _matches.call(el, selector) : false;
}
/**
* Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
*/
function strictEquals(value1, value2) {
return value1 === value2 || (value1 !== value1 && value2 !== value2);
}
/** Gets a map of default set of attributes to observe and the properties they affect. */
function getDefaultAttributeToPropertyInputs(inputs) {
const attributeToPropertyInputs = {};
inputs.forEach(({ propName, templateName, transform }) => {
attributeToPropertyInputs[camelToDashCase(templateName)] = [propName, transform];
});
return attributeToPropertyInputs;
}
/**
* Gets a component's set of inputs. Uses the injector to get the component factory where the inputs
* are defined.
*/
function getComponentInputs(component, injector) {
const componentFactoryResolver = injector.get(ComponentFactoryResolver);
const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
return componentFactory.inputs;
}
// NOTE: This is a (slightly improved) version of what is used in ngUpgrade's
function extractProjectableNodes(host, ngContentSelectors) {
const nodes = host.childNodes;
const projectableNodes = ngContentSelectors.map(() => []);
let wildcardIndex = -1;
ngContentSelectors.some((selector, i) => {
if (selector === '*') {
wildcardIndex = i;
return true;
}
return false;
});
for (let i = 0, ii = nodes.length; i < ii; ++i) {
const node = nodes[i];
const ngContentIndex = findMatchingIndex(node, ngContentSelectors, wildcardIndex);
if (ngContentIndex !== -1) {
projectableNodes[ngContentIndex].push(node);
}
}
return projectableNodes;
}
function findMatchingIndex(node, selectors, defaultIndex) {
let matchingIndex = defaultIndex;
if (isElement(node)) {
selectors.some((selector, i) => {
if (selector !== '*' && matchesSelector(node, selector)) {
matchingIndex = i;
return true;
}
return false;
});
}
return matchingIndex;
}
/** Time in milliseconds to wait before destroying the component ref when disconnected. */
const DESTROY_DELAY = 10;
/**
* Factory that creates new ComponentNgElementStrategy instance. Gets the component factory with the
* constructor's injector's factory resolver and passes that factory to each strategy.
*/
class ComponentNgElementStrategyFactory {
constructor(component, injector) {
this.componentFactory = injector
.get(ComponentFactoryResolver)
.resolveComponentFactory(component);
}
create(injector) {
return new ComponentNgElementStrategy(this.componentFactory, injector);
}
}
/**
* Creates and destroys a component ref using a component factory and handles change detection
* in response to input changes.
*/
class ComponentNgElementStrategy {
constructor(componentFactory, injector) {
this.componentFactory = componentFactory;
this.injector = injector;
// Subject of `NgElementStrategyEvent` observables corresponding to the component's outputs.
this.eventEmitters = new ReplaySubject(1);
/** Merged stream of the component's output events. */
this.events = this.eventEmitters.pipe(switchMap((emitters) => merge(...emitters)));
/** Reference to the component that was created on connect. */
this.componentRef = null;
/** Reference to the component view's `ChangeDetectorRef`. */
this.viewChangeDetectorRef = null;
/**
* Changes that have been made to component inputs since the last change detection run.
* (NOTE: These are only recorded if the component implements the `OnChanges` interface.)
*/
this.inputChanges = null;
/** Whether changes have been made to component inputs since the last change detection run. */
this.hasInputChanges = false;
/** Whether the created component implements the `OnChanges` interface. */
this.implementsOnChanges = false;
/** Whether a change detection has been scheduled to run on the component. */
this.scheduledChangeDetectionFn = null;
/** Callback function that when called will cancel a scheduled destruction on the component. */
this.scheduledDestroyFn = null;
/** Initial input values that were set before the component was created. */
this.initialInputValues = new Map();
this.unchangedInputs = new Set(this.componentFactory.inputs.map(({ propName }) => propName));
this.ngZone = this.injector.get(NgZone);
this.elementZone = typeof Zone === 'undefined' ? null : this.ngZone.run(() => Zone.current);
}
/**
* Initializes a new component if one has not yet been created and cancels any scheduled
* destruction.
*/
connect(element) {
this.runInZone(() => {
// If the element is marked to be destroyed, cancel the task since the component was
// reconnected
if (this.scheduledDestroyFn !== null) {
this.scheduledDestroyFn();
this.scheduledDestroyFn = null;
return;
}
if (this.componentRef === null) {
this.initializeComponent(element);
}
});
}
/**
* Schedules the component to be destroyed after some small delay in case the element is just
* being moved across the DOM.
*/
disconnect() {
this.runInZone(() => {
// Return if there is no componentRef or the component is already scheduled for destruction
if (this.componentRef === null || this.scheduledDestroyFn !== null) {
return;
}
// Schedule the component to be destroyed after a small timeout in case it is being
// moved elsewhere in the DOM
this.scheduledDestroyFn = scheduler.schedule(() => {
if (this.componentRef !== null) {
this.componentRef.destroy();
this.componentRef = null;
this.viewChangeDetectorRef = null;
}
}, DESTROY_DELAY);
});
}
/**
* Returns the component property value. If the component has not yet been created, the value is
* retrieved from the cached initialization values.
*/
getInputValue(property) {
return this.runInZone(() => {
if (this.componentRef === null) {
return this.initialInputValues.get(property);
}
return this.componentRef.instance[property];
});
}
/**
* Sets the input value for the property. If the component has not yet been created, the value is
* cached and set when the component is created.
*/
setInputValue(property, value, transform) {
this.runInZone(() => {
if (transform) {
value = transform.call(this.componentRef?.instance, value);
}
if (this.componentRef === null) {
this.initialInputValues.set(property, value);
return;
}
// Ignore the value if it is strictly equal to the current value, except if it is `undefined`
// and this is the first change to the value (because an explicit `undefined` _is_ strictly
// equal to not having a value set at all, but we still need to record this as a change).
if (strictEquals(value, this.getInputValue(property)) &&
!(value === undefined && this.unchangedInputs.has(property))) {
return;
}
// Record the changed value and update internal state to reflect the fact that this input has
// changed.
this.recordInputChange(property, value);
this.unchangedInputs.delete(property);
this.hasInputChanges = true;
// Update the component instance and schedule change detection.
this.componentRef.instance[property] = value;
this.scheduleDetectChanges();
});
}
/**
* Creates a new component through the component factory with the provided element host and
* sets up its initial inputs, listens for outputs changes, and runs an initial change detection.
*/
initializeComponent(element) {
const childInjector = Injector.create({ providers: [], parent: this.injector });
const projectableNodes = extractProjectableNodes(element, this.componentFactory.ngContentSelectors);
this.componentRef = this.componentFactory.create(childInjector, projectableNodes, element);
this.viewChangeDetectorRef = this.componentRef.injector.get(ChangeDetectorRef);
this.implementsOnChanges = isFunction(this.componentRef.instance.ngOnChanges);
this.initializeInputs();
this.initializeOutputs(this.componentRef);
this.detectChanges();
const applicationRef = this.injector.get(ApplicationRef);
applicationRef.attachView(this.componentRef.hostView);
}
/** Set any stored initial inputs on the component's properties. */
initializeInputs() {
this.componentFactory.inputs.forEach(({ propName, transform }) => {
if (this.initialInputValues.has(propName)) {
// Call `setInputValue()` now that the component has been instantiated to update its
// properties and fire `ngOnChanges()`.
this.setInputValue(propName, this.initialInputValues.get(propName), transform);
}
});
this.initialInputValues.clear();
}
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
initializeOutputs(componentRef) {
const eventEmitters = this.componentFactory.outputs.map(({ propName, templateName }) => {
const emitter = componentRef.instance[propName];
return emitter.pipe(map((value) => ({ name: templateName, value })));
});
this.eventEmitters.next(eventEmitters);
}
/** Calls ngOnChanges with all the inputs that have changed since the last call. */
callNgOnChanges(componentRef) {
if (!this.implementsOnChanges || this.inputChanges === null) {
return;
}
// Cache the changes and set inputChanges to null to capture any changes that might occur
// during ngOnChanges.
const inputChanges = this.inputChanges;
this.inputChanges = null;
componentRef.instance.ngOnChanges(inputChanges);
}
/**
* Marks the component view for check, if necessary.
* (NOTE: This is required when the `ChangeDetectionStrategy` is set to `OnPush`.)
*/
markViewForCheck(viewChangeDetectorRef) {
if (this.hasInputChanges) {
this.hasInputChanges = false;
viewChangeDetectorRef.markForCheck();
}
}
/**
* Schedules change detection to run on the component.
* Ignores subsequent calls if already scheduled.
*/
scheduleDetectChanges() {
if (this.scheduledChangeDetectionFn) {
return;
}
this.scheduledChangeDetectionFn = scheduler.scheduleBeforeRender(() => {
this.scheduledChangeDetectionFn = null;
this.detectChanges();
});
}
/**
* Records input changes so that the component receives SimpleChanges in its onChanges function.
*/
recordInputChange(property, currentValue) {
// Do not record the change if the component does not implement `OnChanges`.
if (!this.implementsOnChanges) {
return;
}
if (this.inputChanges === null) {
this.inputChanges = {};
}
// If there already is a change, modify the current value to match but leave the values for
// `previousValue` and `isFirstChange`.
const pendingChange = this.inputChanges[property];
if (pendingChange) {
pendingChange.currentValue = currentValue;
return;
}
const isFirstChange = this.unchangedInputs.has(property);
const previousValue = isFirstChange ? undefined : this.getInputValue(property);
this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange);
}
/** Runs change detection on the component. */
detectChanges() {
if (this.componentRef === null) {
return;
}
this.callNgOnChanges(this.componentRef);
this.markViewForCheck(this.viewChangeDetectorRef);
this.componentRef.changeDetectorRef.detectChanges();
}
/** Runs in the angular zone, if present. */
runInZone(fn) {
return this.elementZone && Zone.current !== this.elementZone ? this.ngZone.run(fn) : fn();
}
}
/**
* Implements the functionality needed for a custom element.
*
* @publicApi
*/
class NgElement extends HTMLElement {
constructor() {
super(...arguments);
/**
* A subscription to change, connect, and disconnect events in the custom element.
*/
this.ngElementEventsSubscription = null;
}
}
/**
* @description Creates a custom element class based on an Angular component.
*
* Builds a class that encapsulates the functionality of the provided component and
* uses the configuration information to provide more context to the class.
* Takes the component factory's inputs and outputs to convert them to the proper
* custom element API and add hooks to input changes.
*
* The configuration's injector is the initial injector set on the class,
* and used by default for each created instance.This behavior can be overridden with the
* static property to affect all newly created instances, or as a constructor argument for
* one-off creations.
*
* @see [Angular Elements Overview](guide/elements "Turning Angular components into custom elements")
*
* @param component The component to transform.
* @param config A configuration that provides initialization information to the created class.
* @returns The custom-element construction class, which can be registered with
* a browser's `CustomElementRegistry`.
*
* @publicApi
*/
function createCustomElement(component, config) {
const inputs = getComponentInputs(component, config.injector);
const strategyFactory = config.strategyFactory || new ComponentNgElementStrategyFactory(component, config.injector);
const attributeToPropertyInputs = getDefaultAttributeToPropertyInputs(inputs);
class NgElementImpl extends NgElement {
// Work around a bug in closure typed optimizations(b/79557487) where it is not honoring static
// field externs. So using quoted access to explicitly prevent renaming.
static { this['observedAttributes'] = Object.keys(attributeToPropertyInputs); }
get ngElementStrategy() {
// TODO(andrewseguin): Add e2e tests that cover cases where the constructor isn't called. For
// now this is tested using a Google internal test suite.
if (!this._ngElementStrategy) {
const strategy = (this._ngElementStrategy = strategyFactory.create(this.injector || config.injector));
// Re-apply pre-existing input values (set as properties on the element) through the
// strategy.
inputs.forEach(({ propName, transform }) => {
if (!this.hasOwnProperty(propName)) {
// No pre-existing value for `propName`.
return;
}
// Delete the property from the instance and re-apply it through the strategy.
const value = this[propName];
delete this[propName];
strategy.setInputValue(propName, value, transform);
});
}
return this._ngElementStrategy;
}
constructor(injector) {
super();
this.injector = injector;
}
attributeChangedCallback(attrName, oldValue, newValue, namespace) {
const [propName, transform] = attributeToPropertyInputs[attrName];
this.ngElementStrategy.setInputValue(propName, newValue, transform);
}
connectedCallback() {
// For historical reasons, some strategies may not have initialized the `events` property
// until after `connect()` is run. Subscribe to `events` if it is available before running
// `connect()` (in order to capture events emitted during initialization), otherwise subscribe
// afterwards.
//
// TODO: Consider deprecating/removing the post-connect subscription in a future major version
// (e.g. v11).
let subscribedToEvents = false;
if (this.ngElementStrategy.events) {
// `events` are already available: Subscribe to it asap.
this.subscribeToEvents();
subscribedToEvents = true;
}
this.ngElementStrategy.connect(this);
if (!subscribedToEvents) {
// `events` were not initialized before running `connect()`: Subscribe to them now.
// The events emitted during the component initialization have been missed, but at least
// future events will be captured.
this.subscribeToEvents();
}
}
disconnectedCallback() {
// Not using `this.ngElementStrategy` to avoid unnecessarily creating the `NgElementStrategy`.
if (this._ngElementStrategy) {
this._ngElementStrategy.disconnect();
}
if (this.ngElementEventsSubscription) {
this.ngElementEventsSubscription.unsubscribe();
this.ngElementEventsSubscription = null;
}
}
subscribeToEvents() {
// Listen for events from the strategy and dispatch them as custom events.
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe((e) => {
const customEvent = new CustomEvent(e.name, { detail: e.value });
this.dispatchEvent(customEvent);
});
}
}
// Add getters and setters to the prototype for each property input.
inputs.forEach(({ propName, transform }) => {
Object.defineProperty(NgElementImpl.prototype, propName, {
get() {
return this.ngElementStrategy.getInputValue(propName);
},
set(newValue) {
this.ngElementStrategy.setInputValue(propName, newValue, transform);
},
configurable: true,
enumerable: true,
});
});
return NgElementImpl;
}
/**
* @publicApi
*/
const VERSION = new Version('18.2.7');
/**
* @module
* @description
* Entry point for all public APIs of the `elements` package.
*/
// This file only reexports content of the `src` folder. Keep it that way.
// This file is not used to build this module. It is only used during editing
/**
* Generated bundle index. Do not edit.
*/
export { NgElement, VERSION, createCustomElement };
//# sourceMappingURL=elements.mjs.map