UNPKG

23.8 kBJavaScriptView Raw
1/**
2 * @license Angular v13.3.9
3 * (c) 2010-2022 Google LLC. https://angular.io/
4 * License: MIT
5 */
6
7import { ComponentFactoryResolver, NgZone, Injector, ChangeDetectorRef, ApplicationRef, SimpleChange, Version } from '@angular/core';
8import { ReplaySubject, merge } from 'rxjs';
9import { switchMap, map } from 'rxjs/operators';
10
11/**
12 * @license
13 * Copyright Google LLC All Rights Reserved.
14 *
15 * Use of this source code is governed by an MIT-style license that can be
16 * found in the LICENSE file at https://angular.io/license
17 */
18/**
19 * Provide methods for scheduling the execution of a callback.
20 */
21const scheduler = {
22 /**
23 * Schedule a callback to be called after some delay.
24 *
25 * Returns a function that when executed will cancel the scheduled function.
26 */
27 schedule(taskFn, delay) {
28 const id = setTimeout(taskFn, delay);
29 return () => clearTimeout(id);
30 },
31 /**
32 * Schedule a callback to be called before the next render.
33 * (If `window.requestAnimationFrame()` is not available, use `scheduler.schedule()` instead.)
34 *
35 * Returns a function that when executed will cancel the scheduled function.
36 */
37 scheduleBeforeRender(taskFn) {
38 // TODO(gkalpak): Implement a better way of accessing `requestAnimationFrame()`
39 // (e.g. accounting for vendor prefix, SSR-compatibility, etc).
40 if (typeof window === 'undefined') {
41 // For SSR just schedule immediately.
42 return scheduler.schedule(taskFn, 0);
43 }
44 if (typeof window.requestAnimationFrame === 'undefined') {
45 const frameMs = 16;
46 return scheduler.schedule(taskFn, frameMs);
47 }
48 const id = window.requestAnimationFrame(taskFn);
49 return () => window.cancelAnimationFrame(id);
50 },
51};
52/**
53 * Convert a camelCased string to kebab-cased.
54 */
55function camelToDashCase(input) {
56 return input.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
57}
58/**
59 * Check whether the input is an `Element`.
60 */
61function isElement(node) {
62 return !!node && node.nodeType === Node.ELEMENT_NODE;
63}
64/**
65 * Check whether the input is a function.
66 */
67function isFunction(value) {
68 return typeof value === 'function';
69}
70/**
71 * Convert a kebab-cased string to camelCased.
72 */
73function kebabToCamelCase(input) {
74 return input.replace(/-([a-z\d])/g, (_, char) => char.toUpperCase());
75}
76let _matches;
77/**
78 * Check whether an `Element` matches a CSS selector.
79 * NOTE: this is duplicated from @angular/upgrade, and can
80 * be consolidated in the future
81 */
82function matchesSelector(el, selector) {
83 if (!_matches) {
84 const elProto = Element.prototype;
85 _matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
86 elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector;
87 }
88 return el.nodeType === Node.ELEMENT_NODE ? _matches.call(el, selector) : false;
89}
90/**
91 * Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
92 */
93function strictEquals(value1, value2) {
94 return value1 === value2 || (value1 !== value1 && value2 !== value2);
95}
96/** Gets a map of default set of attributes to observe and the properties they affect. */
97function getDefaultAttributeToPropertyInputs(inputs) {
98 const attributeToPropertyInputs = {};
99 inputs.forEach(({ propName, templateName }) => {
100 attributeToPropertyInputs[camelToDashCase(templateName)] = propName;
101 });
102 return attributeToPropertyInputs;
103}
104/**
105 * Gets a component's set of inputs. Uses the injector to get the component factory where the inputs
106 * are defined.
107 */
108function getComponentInputs(component, injector) {
109 const componentFactoryResolver = injector.get(ComponentFactoryResolver);
110 const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
111 return componentFactory.inputs;
112}
113
114/**
115 * @license
116 * Copyright Google LLC All Rights Reserved.
117 *
118 * Use of this source code is governed by an MIT-style license that can be
119 * found in the LICENSE file at https://angular.io/license
120 */
121function extractProjectableNodes(host, ngContentSelectors) {
122 const nodes = host.childNodes;
123 const projectableNodes = ngContentSelectors.map(() => []);
124 let wildcardIndex = -1;
125 ngContentSelectors.some((selector, i) => {
126 if (selector === '*') {
127 wildcardIndex = i;
128 return true;
129 }
130 return false;
131 });
132 for (let i = 0, ii = nodes.length; i < ii; ++i) {
133 const node = nodes[i];
134 const ngContentIndex = findMatchingIndex(node, ngContentSelectors, wildcardIndex);
135 if (ngContentIndex !== -1) {
136 projectableNodes[ngContentIndex].push(node);
137 }
138 }
139 return projectableNodes;
140}
141function findMatchingIndex(node, selectors, defaultIndex) {
142 let matchingIndex = defaultIndex;
143 if (isElement(node)) {
144 selectors.some((selector, i) => {
145 if ((selector !== '*') && matchesSelector(node, selector)) {
146 matchingIndex = i;
147 return true;
148 }
149 return false;
150 });
151 }
152 return matchingIndex;
153}
154
155/**
156 * @license
157 * Copyright Google LLC All Rights Reserved.
158 *
159 * Use of this source code is governed by an MIT-style license that can be
160 * found in the LICENSE file at https://angular.io/license
161 */
162/** Time in milliseconds to wait before destroying the component ref when disconnected. */
163const DESTROY_DELAY = 10;
164/**
165 * Factory that creates new ComponentNgElementStrategy instance. Gets the component factory with the
166 * constructor's injector's factory resolver and passes that factory to each strategy.
167 *
168 * @publicApi
169 */
170class ComponentNgElementStrategyFactory {
171 constructor(component, injector) {
172 this.componentFactory =
173 injector.get(ComponentFactoryResolver).resolveComponentFactory(component);
174 }
175 create(injector) {
176 return new ComponentNgElementStrategy(this.componentFactory, injector);
177 }
178}
179/**
180 * Creates and destroys a component ref using a component factory and handles change detection
181 * in response to input changes.
182 *
183 * @publicApi
184 */
185class ComponentNgElementStrategy {
186 constructor(componentFactory, injector) {
187 this.componentFactory = componentFactory;
188 this.injector = injector;
189 // Subject of `NgElementStrategyEvent` observables corresponding to the component's outputs.
190 this.eventEmitters = new ReplaySubject(1);
191 /** Merged stream of the component's output events. */
192 this.events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters)));
193 /** Reference to the component that was created on connect. */
194 this.componentRef = null;
195 /** Reference to the component view's `ChangeDetectorRef`. */
196 this.viewChangeDetectorRef = null;
197 /**
198 * Changes that have been made to component inputs since the last change detection run.
199 * (NOTE: These are only recorded if the component implements the `OnChanges` interface.)
200 */
201 this.inputChanges = null;
202 /** Whether changes have been made to component inputs since the last change detection run. */
203 this.hasInputChanges = false;
204 /** Whether the created component implements the `OnChanges` interface. */
205 this.implementsOnChanges = false;
206 /** Whether a change detection has been scheduled to run on the component. */
207 this.scheduledChangeDetectionFn = null;
208 /** Callback function that when called will cancel a scheduled destruction on the component. */
209 this.scheduledDestroyFn = null;
210 /** Initial input values that were set before the component was created. */
211 this.initialInputValues = new Map();
212 /**
213 * Set of component inputs that have not yet changed, i.e. for which `recordInputChange()` has not
214 * fired.
215 * (This helps detect the first change of an input, even if it is explicitly set to `undefined`.)
216 */
217 this.unchangedInputs = new Set(this.componentFactory.inputs.map(({ propName }) => propName));
218 /** Service for setting zone context. */
219 this.ngZone = this.injector.get(NgZone);
220 /** The zone the element was created in or `null` if Zone.js is not loaded. */
221 this.elementZone = (typeof Zone === 'undefined') ? null : this.ngZone.run(() => Zone.current);
222 }
223 /**
224 * Initializes a new component if one has not yet been created and cancels any scheduled
225 * destruction.
226 */
227 connect(element) {
228 this.runInZone(() => {
229 // If the element is marked to be destroyed, cancel the task since the component was
230 // reconnected
231 if (this.scheduledDestroyFn !== null) {
232 this.scheduledDestroyFn();
233 this.scheduledDestroyFn = null;
234 return;
235 }
236 if (this.componentRef === null) {
237 this.initializeComponent(element);
238 }
239 });
240 }
241 /**
242 * Schedules the component to be destroyed after some small delay in case the element is just
243 * being moved across the DOM.
244 */
245 disconnect() {
246 this.runInZone(() => {
247 // Return if there is no componentRef or the component is already scheduled for destruction
248 if (this.componentRef === null || this.scheduledDestroyFn !== null) {
249 return;
250 }
251 // Schedule the component to be destroyed after a small timeout in case it is being
252 // moved elsewhere in the DOM
253 this.scheduledDestroyFn = scheduler.schedule(() => {
254 if (this.componentRef !== null) {
255 this.componentRef.destroy();
256 this.componentRef = null;
257 this.viewChangeDetectorRef = null;
258 }
259 }, DESTROY_DELAY);
260 });
261 }
262 /**
263 * Returns the component property value. If the component has not yet been created, the value is
264 * retrieved from the cached initialization values.
265 */
266 getInputValue(property) {
267 return this.runInZone(() => {
268 if (this.componentRef === null) {
269 return this.initialInputValues.get(property);
270 }
271 return this.componentRef.instance[property];
272 });
273 }
274 /**
275 * Sets the input value for the property. If the component has not yet been created, the value is
276 * cached and set when the component is created.
277 */
278 setInputValue(property, value) {
279 this.runInZone(() => {
280 if (this.componentRef === null) {
281 this.initialInputValues.set(property, value);
282 return;
283 }
284 // Ignore the value if it is strictly equal to the current value, except if it is `undefined`
285 // and this is the first change to the value (because an explicit `undefined` _is_ strictly
286 // equal to not having a value set at all, but we still need to record this as a change).
287 if (strictEquals(value, this.getInputValue(property)) &&
288 !((value === undefined) && this.unchangedInputs.has(property))) {
289 return;
290 }
291 // Record the changed value and update internal state to reflect the fact that this input has
292 // changed.
293 this.recordInputChange(property, value);
294 this.unchangedInputs.delete(property);
295 this.hasInputChanges = true;
296 // Update the component instance and schedule change detection.
297 this.componentRef.instance[property] = value;
298 this.scheduleDetectChanges();
299 });
300 }
301 /**
302 * Creates a new component through the component factory with the provided element host and
303 * sets up its initial inputs, listens for outputs changes, and runs an initial change detection.
304 */
305 initializeComponent(element) {
306 const childInjector = Injector.create({ providers: [], parent: this.injector });
307 const projectableNodes = extractProjectableNodes(element, this.componentFactory.ngContentSelectors);
308 this.componentRef = this.componentFactory.create(childInjector, projectableNodes, element);
309 this.viewChangeDetectorRef = this.componentRef.injector.get(ChangeDetectorRef);
310 this.implementsOnChanges = isFunction(this.componentRef.instance.ngOnChanges);
311 this.initializeInputs();
312 this.initializeOutputs(this.componentRef);
313 this.detectChanges();
314 const applicationRef = this.injector.get(ApplicationRef);
315 applicationRef.attachView(this.componentRef.hostView);
316 }
317 /** Set any stored initial inputs on the component's properties. */
318 initializeInputs() {
319 this.componentFactory.inputs.forEach(({ propName }) => {
320 if (this.initialInputValues.has(propName)) {
321 // Call `setInputValue()` now that the component has been instantiated to update its
322 // properties and fire `ngOnChanges()`.
323 this.setInputValue(propName, this.initialInputValues.get(propName));
324 }
325 });
326 this.initialInputValues.clear();
327 }
328 /** Sets up listeners for the component's outputs so that the events stream emits the events. */
329 initializeOutputs(componentRef) {
330 const eventEmitters = this.componentFactory.outputs.map(({ propName, templateName }) => {
331 const emitter = componentRef.instance[propName];
332 return emitter.pipe(map(value => ({ name: templateName, value })));
333 });
334 this.eventEmitters.next(eventEmitters);
335 }
336 /** Calls ngOnChanges with all the inputs that have changed since the last call. */
337 callNgOnChanges(componentRef) {
338 if (!this.implementsOnChanges || this.inputChanges === null) {
339 return;
340 }
341 // Cache the changes and set inputChanges to null to capture any changes that might occur
342 // during ngOnChanges.
343 const inputChanges = this.inputChanges;
344 this.inputChanges = null;
345 componentRef.instance.ngOnChanges(inputChanges);
346 }
347 /**
348 * Marks the component view for check, if necessary.
349 * (NOTE: This is required when the `ChangeDetectionStrategy` is set to `OnPush`.)
350 */
351 markViewForCheck(viewChangeDetectorRef) {
352 if (this.hasInputChanges) {
353 this.hasInputChanges = false;
354 viewChangeDetectorRef.markForCheck();
355 }
356 }
357 /**
358 * Schedules change detection to run on the component.
359 * Ignores subsequent calls if already scheduled.
360 */
361 scheduleDetectChanges() {
362 if (this.scheduledChangeDetectionFn) {
363 return;
364 }
365 this.scheduledChangeDetectionFn = scheduler.scheduleBeforeRender(() => {
366 this.scheduledChangeDetectionFn = null;
367 this.detectChanges();
368 });
369 }
370 /**
371 * Records input changes so that the component receives SimpleChanges in its onChanges function.
372 */
373 recordInputChange(property, currentValue) {
374 // Do not record the change if the component does not implement `OnChanges`.
375 if (!this.implementsOnChanges) {
376 return;
377 }
378 if (this.inputChanges === null) {
379 this.inputChanges = {};
380 }
381 // If there already is a change, modify the current value to match but leave the values for
382 // `previousValue` and `isFirstChange`.
383 const pendingChange = this.inputChanges[property];
384 if (pendingChange) {
385 pendingChange.currentValue = currentValue;
386 return;
387 }
388 const isFirstChange = this.unchangedInputs.has(property);
389 const previousValue = isFirstChange ? undefined : this.getInputValue(property);
390 this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange);
391 }
392 /** Runs change detection on the component. */
393 detectChanges() {
394 if (this.componentRef === null) {
395 return;
396 }
397 this.callNgOnChanges(this.componentRef);
398 this.markViewForCheck(this.viewChangeDetectorRef);
399 this.componentRef.changeDetectorRef.detectChanges();
400 }
401 /** Runs in the angular zone, if present. */
402 runInZone(fn) {
403 return (this.elementZone && Zone.current !== this.elementZone) ? this.ngZone.run(fn) : fn();
404 }
405}
406
407/**
408 * @license
409 * Copyright Google LLC All Rights Reserved.
410 *
411 * Use of this source code is governed by an MIT-style license that can be
412 * found in the LICENSE file at https://angular.io/license
413 */
414/**
415 * Implements the functionality needed for a custom element.
416 *
417 * @publicApi
418 */
419class NgElement extends HTMLElement {
420 constructor() {
421 super(...arguments);
422 /**
423 * A subscription to change, connect, and disconnect events in the custom element.
424 */
425 this.ngElementEventsSubscription = null;
426 }
427}
428/**
429 * @description Creates a custom element class based on an Angular component.
430 *
431 * Builds a class that encapsulates the functionality of the provided component and
432 * uses the configuration information to provide more context to the class.
433 * Takes the component factory's inputs and outputs to convert them to the proper
434 * custom element API and add hooks to input changes.
435 *
436 * The configuration's injector is the initial injector set on the class,
437 * and used by default for each created instance.This behavior can be overridden with the
438 * static property to affect all newly created instances, or as a constructor argument for
439 * one-off creations.
440 *
441 * @see [Angular Elements Overview](guide/elements "Turning Angular components into custom elements")
442 *
443 * @param component The component to transform.
444 * @param config A configuration that provides initialization information to the created class.
445 * @returns The custom-element construction class, which can be registered with
446 * a browser's `CustomElementRegistry`.
447 *
448 * @publicApi
449 */
450function createCustomElement(component, config) {
451 const inputs = getComponentInputs(component, config.injector);
452 const strategyFactory = config.strategyFactory || new ComponentNgElementStrategyFactory(component, config.injector);
453 const attributeToPropertyInputs = getDefaultAttributeToPropertyInputs(inputs);
454 class NgElementImpl extends NgElement {
455 constructor(injector) {
456 super();
457 this.injector = injector;
458 }
459 get ngElementStrategy() {
460 // NOTE:
461 // Some polyfills (e.g. `document-register-element`) do not call the constructor, therefore
462 // it is not safe to set `ngElementStrategy` in the constructor and assume it will be
463 // available inside the methods.
464 //
465 // TODO(andrewseguin): Add e2e tests that cover cases where the constructor isn't called. For
466 // now this is tested using a Google internal test suite.
467 if (!this._ngElementStrategy) {
468 const strategy = this._ngElementStrategy =
469 strategyFactory.create(this.injector || config.injector);
470 // Re-apply pre-existing input values (set as properties on the element) through the
471 // strategy.
472 inputs.forEach(({ propName }) => {
473 if (!this.hasOwnProperty(propName)) {
474 // No pre-existing value for `propName`.
475 return;
476 }
477 // Delete the property from the instance and re-apply it through the strategy.
478 const value = this[propName];
479 delete this[propName];
480 strategy.setInputValue(propName, value);
481 });
482 }
483 return this._ngElementStrategy;
484 }
485 attributeChangedCallback(attrName, oldValue, newValue, namespace) {
486 const propName = attributeToPropertyInputs[attrName];
487 this.ngElementStrategy.setInputValue(propName, newValue);
488 }
489 connectedCallback() {
490 // For historical reasons, some strategies may not have initialized the `events` property
491 // until after `connect()` is run. Subscribe to `events` if it is available before running
492 // `connect()` (in order to capture events emitted during initialization), otherwise subscribe
493 // afterwards.
494 //
495 // TODO: Consider deprecating/removing the post-connect subscription in a future major version
496 // (e.g. v11).
497 let subscribedToEvents = false;
498 if (this.ngElementStrategy.events) {
499 // `events` are already available: Subscribe to it asap.
500 this.subscribeToEvents();
501 subscribedToEvents = true;
502 }
503 this.ngElementStrategy.connect(this);
504 if (!subscribedToEvents) {
505 // `events` were not initialized before running `connect()`: Subscribe to them now.
506 // The events emitted during the component initialization have been missed, but at least
507 // future events will be captured.
508 this.subscribeToEvents();
509 }
510 }
511 disconnectedCallback() {
512 // Not using `this.ngElementStrategy` to avoid unnecessarily creating the `NgElementStrategy`.
513 if (this._ngElementStrategy) {
514 this._ngElementStrategy.disconnect();
515 }
516 if (this.ngElementEventsSubscription) {
517 this.ngElementEventsSubscription.unsubscribe();
518 this.ngElementEventsSubscription = null;
519 }
520 }
521 subscribeToEvents() {
522 // Listen for events from the strategy and dispatch them as custom events.
523 this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
524 const customEvent = new CustomEvent(e.name, { detail: e.value });
525 this.dispatchEvent(customEvent);
526 });
527 }
528 }
529 // Work around a bug in closure typed optimizations(b/79557487) where it is not honoring static
530 // field externs. So using quoted access to explicitly prevent renaming.
531 NgElementImpl['observedAttributes'] = Object.keys(attributeToPropertyInputs);
532 // Add getters and setters to the prototype for each property input.
533 inputs.forEach(({ propName }) => {
534 Object.defineProperty(NgElementImpl.prototype, propName, {
535 get() {
536 return this.ngElementStrategy.getInputValue(propName);
537 },
538 set(newValue) {
539 this.ngElementStrategy.setInputValue(propName, newValue);
540 },
541 configurable: true,
542 enumerable: true,
543 });
544 });
545 return NgElementImpl;
546}
547
548/**
549 * @license
550 * Copyright Google LLC All Rights Reserved.
551 *
552 * Use of this source code is governed by an MIT-style license that can be
553 * found in the LICENSE file at https://angular.io/license
554 */
555/**
556 * @publicApi
557 */
558const VERSION = new Version('13.3.9');
559
560/**
561 * @license
562 * Copyright Google LLC All Rights Reserved.
563 *
564 * Use of this source code is governed by an MIT-style license that can be
565 * found in the LICENSE file at https://angular.io/license
566 */
567// This file only reexports content of the `src` folder. Keep it that way.
568
569/**
570 * @license
571 * Copyright Google LLC All Rights Reserved.
572 *
573 * Use of this source code is governed by an MIT-style license that can be
574 * found in the LICENSE file at https://angular.io/license
575 */
576
577/**
578 * Generated bundle index. Do not edit.
579 */
580
581export { NgElement, VERSION, createCustomElement };
582//# sourceMappingURL=elements.mjs.map