@angular/elements
Version:
Angular - library for using Angular Components as Custom Elements
292 lines (284 loc) • 9.43 kB
JavaScript
/**
* @license Angular v21.0.6
* (c) 2010-2025 Google LLC. https://angular.dev/
* License: MIT
*/
import { ComponentFactoryResolver, NgZone, ApplicationRef, ɵChangeDetectionScheduler as _ChangeDetectionScheduler, ɵisViewDirty as _isViewDirty, ɵmarkForRefresh as _markForRefresh, Injector, isSignal, Version } from '@angular/core';
import { ReplaySubject, merge, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
const scheduler = {
schedule(taskFn, delay) {
const id = setTimeout(taskFn, delay);
return () => clearTimeout(id);
}
};
function camelToDashCase(input) {
return input.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
}
function isElement(node) {
return !!node && node.nodeType === Node.ELEMENT_NODE;
}
let _matches;
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;
}
function getDefaultAttributeToPropertyInputs(inputs) {
const attributeToPropertyInputs = {};
inputs.forEach(({
propName,
templateName,
transform
}) => {
attributeToPropertyInputs[camelToDashCase(templateName)] = [propName, transform];
});
return attributeToPropertyInputs;
}
function getComponentInputs(component, injector) {
const componentFactoryResolver = injector.get(ComponentFactoryResolver);
const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
return componentFactory.inputs;
}
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;
}
const DESTROY_DELAY = 10;
class ComponentNgElementStrategyFactory {
componentFactory;
inputMap = new Map();
constructor(component, injector) {
this.componentFactory = injector.get(ComponentFactoryResolver).resolveComponentFactory(component);
for (const input of this.componentFactory.inputs) {
this.inputMap.set(input.propName, input.templateName);
}
}
create(injector) {
return new ComponentNgElementStrategy(this.componentFactory, injector, this.inputMap);
}
}
class ComponentNgElementStrategy {
componentFactory;
injector;
inputMap;
eventEmitters = new ReplaySubject(1);
events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters)));
componentRef = null;
scheduledDestroyFn = null;
initialInputValues = new Map();
ngZone;
elementZone;
appRef;
cdScheduler;
constructor(componentFactory, injector, inputMap) {
this.componentFactory = componentFactory;
this.injector = injector;
this.inputMap = inputMap;
this.ngZone = this.injector.get(NgZone);
this.appRef = this.injector.get(ApplicationRef);
this.cdScheduler = injector.get(_ChangeDetectionScheduler);
this.elementZone = typeof Zone === 'undefined' ? null : this.ngZone.run(() => Zone.current);
}
connect(element) {
this.runInZone(() => {
if (this.scheduledDestroyFn !== null) {
this.scheduledDestroyFn();
this.scheduledDestroyFn = null;
return;
}
if (this.componentRef === null) {
this.initializeComponent(element);
}
});
}
disconnect() {
this.runInZone(() => {
if (this.componentRef === null || this.scheduledDestroyFn !== null) {
return;
}
this.scheduledDestroyFn = scheduler.schedule(() => {
if (this.componentRef !== null) {
this.componentRef.destroy();
this.componentRef = null;
}
}, DESTROY_DELAY);
});
}
getInputValue(property) {
return this.runInZone(() => {
if (this.componentRef === null) {
return this.initialInputValues.get(property);
}
return this.componentRef.instance[property];
});
}
setInputValue(property, value) {
if (this.componentRef === null) {
this.initialInputValues.set(property, value);
return;
}
this.runInZone(() => {
this.componentRef.setInput(this.inputMap.get(property) ?? property, value);
if (_isViewDirty(this.componentRef.hostView)) {
_markForRefresh(this.componentRef.changeDetectorRef);
this.cdScheduler.notify(6);
}
});
}
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.initializeInputs();
this.initializeOutputs(this.componentRef);
this.appRef.attachView(this.componentRef.hostView);
this.componentRef.hostView.detectChanges();
}
initializeInputs() {
for (const [propName, value] of this.initialInputValues) {
this.setInputValue(propName, value);
}
this.initialInputValues.clear();
}
initializeOutputs(componentRef) {
const eventEmitters = this.componentFactory.outputs.map(({
propName,
templateName
}) => {
const emitter = componentRef.instance[propName];
return new Observable(observer => {
const sub = emitter.subscribe(value => observer.next({
name: templateName,
value
}));
return () => sub.unsubscribe();
});
});
this.eventEmitters.next(eventEmitters);
}
runInZone(fn) {
return this.elementZone && Zone.current !== this.elementZone ? this.ngZone.run(fn) : fn();
}
}
class NgElement extends HTMLElement {
ngElementEventsSubscription = null;
}
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 {
injector;
static ['observedAttributes'] = Object.keys(attributeToPropertyInputs);
get ngElementStrategy() {
if (!this._ngElementStrategy) {
const strategy = this._ngElementStrategy = strategyFactory.create(this.injector || config.injector);
inputs.forEach(({
propName,
transform
}) => {
if (!this.hasOwnProperty(propName)) {
return;
}
const value = this[propName];
delete this[propName];
strategy.setInputValue(propName, value, transform);
});
}
return this._ngElementStrategy;
}
_ngElementStrategy;
constructor(injector) {
super();
this.injector = injector;
}
attributeChangedCallback(attrName, oldValue, newValue, namespace) {
const [propName, transform] = attributeToPropertyInputs[attrName];
this.ngElementStrategy.setInputValue(propName, newValue, transform);
}
connectedCallback() {
let subscribedToEvents = false;
if (this.ngElementStrategy.events) {
this.subscribeToEvents();
subscribedToEvents = true;
}
this.ngElementStrategy.connect(this);
if (!subscribedToEvents) {
this.subscribeToEvents();
}
}
disconnectedCallback() {
if (this._ngElementStrategy) {
this._ngElementStrategy.disconnect();
}
if (this.ngElementEventsSubscription) {
this.ngElementEventsSubscription.unsubscribe();
this.ngElementEventsSubscription = null;
}
}
subscribeToEvents() {
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
const customEvent = new CustomEvent(e.name, {
detail: e.value
});
this.dispatchEvent(customEvent);
});
}
}
inputs.forEach(({
propName,
transform,
isSignal: _isSignal
}) => {
Object.defineProperty(NgElementImpl.prototype, propName, {
get() {
const inputValue = this.ngElementStrategy.getInputValue(propName);
return _isSignal && isSignal(inputValue) ? inputValue() : inputValue;
},
set(newValue) {
this.ngElementStrategy.setInputValue(propName, newValue, transform);
},
configurable: true,
enumerable: true
});
});
return NgElementImpl;
}
const VERSION = /* @__PURE__ */new Version('21.0.6');
export { NgElement, VERSION, createCustomElement };
//# sourceMappingURL=elements.mjs.map