diff --git a/107.86415ee5e99c7048.js b/107.dbecfa80f642d9a0.js similarity index 99% rename from 107.86415ee5e99c7048.js rename to 107.dbecfa80f642d9a0.js index 3f7d3c53..8a413426 100644 --- a/107.86415ee5e99c7048.js +++ b/107.dbecfa80f642d9a0.js @@ -1660,7 +1660,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ "ɵɵviewQuerySignal": () => (/* binding */ ɵɵviewQuerySignal) /* harmony export */ }); /* harmony import */ var _home_runner_work_microzord_microzord_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(1528); -/* harmony import */ var _angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6008); +/* harmony import */ var _angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(536); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5657); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6928); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6700); @@ -1668,7 +1668,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(5084); /** - * @license Angular v17.2.3 + * @license Angular v17.2.4 * (c) 2010-2022 Google LLC. https://angular.io/ * License: MIT */ @@ -1777,7 +1777,12 @@ class EventEmitter_ extends rxjs__WEBPACK_IMPORTED_MODULE_1__/* .Subject */ .E { this.__isAsync = isAsync; } emit(value) { - super.next(value); + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); + try { + super.next(value); + } finally { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); + } } subscribe(observerOrNext, error, complete) { let nextFn = observerOrNext; @@ -2049,6 +2054,11 @@ function assertOneOf(value, ...validValues) { if (validValues.indexOf(value) !== -1) return true; throwError(`Expected value to be one of ${JSON.stringify(validValues)} but was ${JSON.stringify(value)}.`); } +function assertNotReactive(fn) { + if ((0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.getActiveConsumer)() !== null) { + throwError(`${fn}() should never be called in a reactive context.`); + } +} /** * The strategy that the default change detector uses to detect changes. @@ -6592,6 +6602,7 @@ class R3Injector extends EnvironmentInjector { this.assertNotDestroyed(); // Set destroyed = true first, in case lifecycle hooks re-enter destroy(). this._destroyed = true; + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); try { // Call all the lifecycle hooks. for (const service of this._ngOnDestroyHooks) { @@ -6609,6 +6620,7 @@ class R3Injector extends EnvironmentInjector { this.records.clear(); this._ngOnDestroyHooks.clear(); this.injectorDefTypes.clear(); + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); } } onDestroy(callback) { @@ -6709,6 +6721,7 @@ class R3Injector extends EnvironmentInjector { } /** @internal */ resolveInjectorInitializers() { + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); const previousInjector = setCurrentInjector(this); const previousInjectImplementation = setInjectImplementation(undefined); let prevInjectContext; @@ -6730,6 +6743,7 @@ class R3Injector extends EnvironmentInjector { setCurrentInjector(previousInjector); setInjectImplementation(previousInjectImplementation); ngDevMode && setInjectorProfilerContext(prevInjectContext); + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); } } toString() { @@ -6793,23 +6807,28 @@ class R3Injector extends EnvironmentInjector { this.records.set(token, record); } hydrate(token, record) { - if (ngDevMode && record.value === CIRCULAR) { - throwCyclicDependencyError(stringify(token)); - } else if (record.value === NOT_YET) { - record.value = CIRCULAR; - if (ngDevMode) { - runInInjectorProfilerContext(this, token, () => { + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); + try { + if (ngDevMode && record.value === CIRCULAR) { + throwCyclicDependencyError(stringify(token)); + } else if (record.value === NOT_YET) { + record.value = CIRCULAR; + if (ngDevMode) { + runInInjectorProfilerContext(this, token, () => { + record.value = record.factory(); + emitInstanceCreatedByInjectorEvent(record.value); + }); + } else { record.value = record.factory(); - emitInstanceCreatedByInjectorEvent(record.value); - }); - } else { - record.value = record.factory(); + } } + if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) { + this._ngOnDestroyHooks.add(record.value); + } + return record.value; + } finally { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); } - if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) { - this._ngOnDestroyHooks.add(record.value); - } - return record.value; } injectableDefInScope(def) { if (!def.providedIn) { @@ -11028,7 +11047,11 @@ function destroyLView(tView, lView) { * @param lView The LView to clean up */ function cleanUpView(tView, lView) { - if (!(lView[FLAGS] & 256 /* LViewFlags.Destroyed */)) { + if (lView[FLAGS] & 256 /* LViewFlags.Destroyed */) { + return; + } + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); + try { // Usually the Attached flag is removed when the view is detached from its parent, however // if it's a root view, the flag won't be unset hence why we're also removing on destroy. lView[FLAGS] &= ~128 /* LViewFlags.Attached */; @@ -11061,10 +11084,13 @@ function cleanUpView(tView, lView) { } // Unregister the view once everything else has been cleaned up. unregisterLView(lView); + } finally { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); } } /** Removes listeners and unsubscribes from output subscriptions */ function processCleanups(tView, lView) { + ngDevMode && assertNotReactive(processCleanups.name); const tCleanup = tView.cleanup; const lCleanup = lView[CLEANUP]; if (tCleanup !== null) { @@ -11106,6 +11132,7 @@ function processCleanups(tView, lView) { } /** Calls onDestroy hooks for this view */ function executeOnDestroys(tView, lView) { + ngDevMode && assertNotReactive(executeOnDestroys.name); let destroyHooks; if (tView != null && (destroyHooks = tView.destroyHooks) != null) { for (let i = 0; i < destroyHooks.length; i += 2) { @@ -13054,6 +13081,7 @@ function syncViewWithBlueprint(tView, lView) { */ function renderView(tView, lView, context) { ngDevMode && assertEqual(isCreationMode(lView), true, 'Should be run in creation mode'); + ngDevMode && assertNotReactive(renderView.name); enterView(lView); try { const viewQuery = tView.viewQuery; @@ -13114,23 +13142,28 @@ function renderChildComponents(hostLView, components) { } } function createAndRenderEmbeddedLView(declarationLView, templateTNode, context, options) { - const embeddedTView = templateTNode.tView; - ngDevMode && assertDefined(embeddedTView, 'TView must be defined for a template node.'); - ngDevMode && assertTNodeForLView(templateTNode, declarationLView); - // Embedded views follow the change detection strategy of the view they're declared in. - const isSignalView = declarationLView[FLAGS] & 4096 /* LViewFlags.SignalView */; - const viewFlags = isSignalView ? 4096 /* LViewFlags.SignalView */ : 16 /* LViewFlags.CheckAlways */; - const embeddedLView = createLView(declarationLView, embeddedTView, context, viewFlags, null, templateTNode, null, null, null, options?.injector ?? null, options?.dehydratedView ?? null); - const declarationLContainer = declarationLView[templateTNode.index]; - ngDevMode && assertLContainer(declarationLContainer); - embeddedLView[DECLARATION_LCONTAINER] = declarationLContainer; - const declarationViewLQueries = declarationLView[QUERIES]; - if (declarationViewLQueries !== null) { - embeddedLView[QUERIES] = declarationViewLQueries.createEmbeddedView(embeddedTView); - } - // execute creation mode of a view - renderView(embeddedTView, embeddedLView, context); - return embeddedLView; + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); + try { + const embeddedTView = templateTNode.tView; + ngDevMode && assertDefined(embeddedTView, 'TView must be defined for a template node.'); + ngDevMode && assertTNodeForLView(templateTNode, declarationLView); + // Embedded views follow the change detection strategy of the view they're declared in. + const isSignalView = declarationLView[FLAGS] & 4096 /* LViewFlags.SignalView */; + const viewFlags = isSignalView ? 4096 /* LViewFlags.SignalView */ : 16 /* LViewFlags.CheckAlways */; + const embeddedLView = createLView(declarationLView, embeddedTView, context, viewFlags, null, templateTNode, null, null, null, options?.injector ?? null, options?.dehydratedView ?? null); + const declarationLContainer = declarationLView[templateTNode.index]; + ngDevMode && assertLContainer(declarationLContainer); + embeddedLView[DECLARATION_LCONTAINER] = declarationLContainer; + const declarationViewLQueries = declarationLView[QUERIES]; + if (declarationViewLQueries !== null) { + embeddedLView[QUERIES] = declarationViewLQueries.createEmbeddedView(embeddedTView); + } + // execute creation mode of a view + renderView(embeddedTView, embeddedLView, context); + return embeddedLView; + } finally { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); + } } function getLViewFromLContainer(lContainer, index) { const adjustedIndex = CONTAINER_HEADER_OFFSET + index; @@ -15009,10 +15042,56 @@ let Sanitizer = /*#__PURE__*/(() => { const NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR = {}; /** - * Checks if the given `value` is a reactive `Signal`. + * `DestroyRef` lets you set callbacks to run for any cleanup or destruction behavior. + * The scope of this destruction depends on where `DestroyRef` is injected. If `DestroyRef` + * is injected in a component or directive, the callbacks run when that component or + * directive is destroyed. Otherwise the callbacks run when a corresponding injector is destroyed. + * + * @publicApi */ -function isSignal(value) { - return typeof value === 'function' && value[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL] !== undefined; +let DestroyRef = /*#__PURE__*/(() => { + class DestroyRef { + /** + * @internal + * @nocollapse + */ + static #_ = this.__NG_ELEMENT_ID__ = injectDestroyRef; + /** + * @internal + * @nocollapse + */ + static #_2 = this.__NG_ENV_ID__ = injector => injector; + } + return DestroyRef; +})(); +class NodeInjectorDestroyRef extends DestroyRef { + constructor(_lView) { + super(); + this._lView = _lView; + } + onDestroy(callback) { + storeLViewOnDestroy(this._lView, callback); + return () => removeLViewOnDestroy(this._lView, callback); + } +} +function injectDestroyRef() { + return new NodeInjectorDestroyRef(getLView()); +} + +/** + * Asserts that the current stack frame is not within a reactive context. Useful + * to disallow certain code from running inside a reactive context (see {@link toSignal}). + * + * @param debugFn a reference to the function making the assertion (used for the error message). + * + * @publicApi + */ +function assertNotInReactiveContext(debugFn, extraContext) { + // Taking a `Function` instead of a string name here prevents the un-minified name of the function + // from being retained in the bundle regardless of minification. + if ((0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.getActiveConsumer)() !== null) { + throw new RuntimeError(-602 /* RuntimeErrorCode.ASSERTION_NOT_INSIDE_REACTIVE_CONTEXT */, ngDevMode && `${debugFn.name}() cannot be called from within a reactive context.${extraContext ? ` ${extraContext}` : ''}`); + } } const markedFeatures = /*#__PURE__*/new Set(); // tslint:disable:ban @@ -15034,6993 +15113,5669 @@ function performanceMarkFeature(feature) { } }); } - -/** - * Create a computed `Signal` which derives a reactive value from an expression. - */ -function computed(computation, options) { - performanceMarkFeature('NgSignals'); - const getter = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.createComputed)(computation); - if (options?.equal) { - getter[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL].equal = options.equal; - } - if (ngDevMode) { - getter.toString = () => `[Computed: ${getter()}]`; - } - return getter; -} - -/** Symbol used distinguish `WritableSignal` from other non-writable signals and functions. */ -const WRITABLE_SIGNAL = /* @__PURE__ */(/* unused pure expression or super */ null && (Symbol('WRITABLE_SIGNAL'))); -/** - * Utility function used during template type checking to extract the value from a `WritableSignal`. - * @codeGenApi - */ -function ɵunwrapWritableSignal(value) { - // Note: needs to be kept in sync with the copy in `fake_core/index.ts`. - // Note: the function uses `WRITABLE_SIGNAL` as a brand instead of `WritableSignal`, - // because the latter incorrectly unwraps non-signal getter functions. - return null; -} -/** - * Create a `Signal` that can be set or updated directly. - */ -function signal(initialValue, options) { - performanceMarkFeature('NgSignals'); - const signalFn = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.createSignal)(initialValue); - const node = signalFn[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL]; - if (options?.equal) { - node.equal = options.equal; - } - signalFn.set = newValue => (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.signalSetFn)(node, newValue); - signalFn.update = updateFn => (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.signalUpdateFn)(node, updateFn); - signalFn.asReadonly = signalAsReadonlyFn.bind(signalFn); - if (ngDevMode) { - signalFn.toString = () => `[Signal: ${signalFn()}]`; - } - return signalFn; -} -function signalAsReadonlyFn() { - const node = this[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL]; - if (node.readonlyFn === undefined) { - const readonlyFn = () => this(); - readonlyFn[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL] = node; - node.readonlyFn = readonlyFn; - } - return node.readonlyFn; -} -/** - * Checks if the given `value` is a writeable signal. - */ -function isWritableSignal(value) { - return isSignal(value) && typeof value.set === 'function'; -} - -/** - * Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function - * can, optionally, return a value. - */ -function untracked(nonReactiveReadsFn) { - const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); - // We are not trying to catch any particular errors here, just making sure that the consumers - // stack is restored in case of errors. - try { - return nonReactiveReadsFn(); - } finally { - (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); - } -} -function isIterable(obj) { - return obj !== null && typeof obj === 'object' && obj[Symbol.iterator] !== undefined; -} -function isListLikeIterable(obj) { - if (!isJsObject(obj)) return false; - return Array.isArray(obj) || !(obj instanceof Map) && - // JS Map are iterables but return entries as [k, v] - Symbol.iterator in obj; // JS Iterable have a Symbol.iterator prop -} -function areIterablesEqual(a, b, comparator) { - const iterator1 = a[Symbol.iterator](); - const iterator2 = b[Symbol.iterator](); - while (true) { - const item1 = iterator1.next(); - const item2 = iterator2.next(); - if (item1.done && item2.done) return true; - if (item1.done || item2.done) return false; - if (!comparator(item1.value, item2.value)) return false; - } +function noop(...args) { + // Do nothing. } -function iterateListLike(obj, fn) { - if (Array.isArray(obj)) { - for (let i = 0; i < obj.length; i++) { - fn(obj[i]); +function getNativeRequestAnimationFrame() { + // Note: the `getNativeRequestAnimationFrame` is used in the `NgZone` class, but we cannot use the + // `inject` function. The `NgZone` instance may be created manually, and thus the injection + // context will be unavailable. This might be enough to check whether `requestAnimationFrame` is + // available because otherwise, we'll fall back to `setTimeout`. + const isBrowser = typeof _global['requestAnimationFrame'] === 'function'; + // Note: `requestAnimationFrame` is unavailable when the code runs in the Node.js environment. We + // use `setTimeout` because no changes are required other than checking if the current platform is + // the browser. `setTimeout` is a well-established API that is available in both environments. + // `requestAnimationFrame` is used in the browser to coalesce event tasks since event tasks are + // usually executed within the same rendering frame (but this is more implementation details of + // browsers). + let nativeRequestAnimationFrame = _global[isBrowser ? 'requestAnimationFrame' : 'setTimeout']; + let nativeCancelAnimationFrame = _global[isBrowser ? 'cancelAnimationFrame' : 'clearTimeout']; + if (typeof Zone !== 'undefined' && nativeRequestAnimationFrame && nativeCancelAnimationFrame) { + // Note: zone.js sets original implementations on patched APIs behind the + // `__zone_symbol__OriginalDelegate` key (see `attachOriginToPatched`). Given the following + // example: `window.requestAnimationFrame.__zone_symbol__OriginalDelegate`; this would return an + // unpatched implementation of the `requestAnimationFrame`, which isn't intercepted by the + // Angular zone. We use the unpatched implementation to avoid another change detection when + // coalescing tasks. + const unpatchedRequestAnimationFrame = nativeRequestAnimationFrame[Zone.__symbol__('OriginalDelegate')]; + if (unpatchedRequestAnimationFrame) { + nativeRequestAnimationFrame = unpatchedRequestAnimationFrame; } - } else { - const iterator = obj[Symbol.iterator](); - let item; - while (!(item = iterator.next()).done) { - fn(item.value); + const unpatchedCancelAnimationFrame = nativeCancelAnimationFrame[Zone.__symbol__('OriginalDelegate')]; + if (unpatchedCancelAnimationFrame) { + nativeCancelAnimationFrame = unpatchedCancelAnimationFrame; } } + return { + nativeRequestAnimationFrame, + nativeCancelAnimationFrame + }; } -function isJsObject(o) { - return o !== null && (typeof o === 'function' || typeof o === 'object'); -} -class DefaultIterableDifferFactory { - constructor() {} - supports(obj) { - return isListLikeIterable(obj); +class AsyncStackTaggingZoneSpec { + constructor(namePrefix, consoleAsyncStackTaggingImpl = console) { + this.name = 'asyncStackTagging for ' + namePrefix; + this.createTask = consoleAsyncStackTaggingImpl?.createTask ?? (() => null); } - create(trackByFn) { - return new DefaultIterableDiffer(trackByFn); + onScheduleTask(delegate, _current, target, task) { + task.consoleTask = this.createTask(`Zone - ${task.source || task.type}`); + return delegate.scheduleTask(target, task); + } + onInvokeTask(delegate, _currentZone, targetZone, task, applyThis, applyArgs) { + let ret; + if (task.consoleTask) { + ret = task.consoleTask.run(() => delegate.invokeTask(targetZone, task, applyThis, applyArgs)); + } else { + ret = delegate.invokeTask(targetZone, task, applyThis, applyArgs); + } + return ret; } } -const trackByIdentity = (index, item) => item; + /** - * @deprecated v4.0.0 - Should not be part of public API. + * An injectable service for executing work inside or outside of the Angular zone. + * + * The most common use of this service is to optimize performance when starting a work consisting of + * one or more asynchronous tasks that don't require UI updates or error handling to be handled by + * Angular. Such tasks can be kicked off via {@link #runOutsideAngular} and if needed, these tasks + * can reenter the Angular zone via {@link #run}. + * + * + * + * @usageNotes + * ### Example + * + * ``` + * import {Component, NgZone} from '@angular/core'; + * import {NgIf} from '@angular/common'; + * + * @Component({ + * selector: 'ng-zone-demo', + * template: ` + *

Demo: NgZone

+ * + *

Progress: {{progress}}%

+ *

Done processing {{label}} of Angular zone!

+ * + * + * + * `, + * }) + * export class NgZoneDemo { + * progress: number = 0; + * label: string; + * + * constructor(private _ngZone: NgZone) {} + * + * // Loop inside the Angular zone + * // so the UI DOES refresh after each setTimeout cycle + * processWithinAngularZone() { + * this.label = 'inside'; + * this.progress = 0; + * this._increaseProgress(() => console.log('Inside Done!')); + * } + * + * // Loop outside of the Angular zone + * // so the UI DOES NOT refresh after each setTimeout cycle + * processOutsideOfAngularZone() { + * this.label = 'outside'; + * this.progress = 0; + * this._ngZone.runOutsideAngular(() => { + * this._increaseProgress(() => { + * // reenter the Angular zone and display done + * this._ngZone.run(() => { console.log('Outside Done!'); }); + * }); + * }); + * } + * + * _increaseProgress(doneCallback: () => void) { + * this.progress += 1; + * console.log(`Current progress: ${this.progress}%`); + * + * if (this.progress < 100) { + * window.setTimeout(() => this._increaseProgress(doneCallback), 10); + * } else { + * doneCallback(); + * } + * } + * } + * ``` + * * @publicApi */ -class DefaultIterableDiffer { - constructor(trackByFn) { - this.length = 0; - // Keeps track of the used records at any point in time (during & across `_check()` calls) - this._linkedRecords = null; - // Keeps track of the removed records at any point in time during `_check()` calls. - this._unlinkedRecords = null; - this._previousItHead = null; - this._itHead = null; - this._itTail = null; - this._additionsHead = null; - this._additionsTail = null; - this._movesHead = null; - this._movesTail = null; - this._removalsHead = null; - this._removalsTail = null; - // Keeps track of records where custom track by is the same, but item identity has changed - this._identityChangesHead = null; - this._identityChangesTail = null; - this._trackByFn = trackByFn || trackByIdentity; - } - forEachItem(fn) { - let record; - for (record = this._itHead; record !== null; record = record._next) { - fn(record); +class NgZone { + constructor({ + enableLongStackTrace = false, + shouldCoalesceEventChangeDetection = false, + shouldCoalesceRunChangeDetection = false + }) { + this.hasPendingMacrotasks = false; + this.hasPendingMicrotasks = false; + /** + * Whether there are no outstanding microtasks or macrotasks. + */ + this.isStable = true; + /** + * Notifies when code enters Angular Zone. This gets fired first on VM Turn. + */ + this.onUnstable = new EventEmitter(false); + /** + * Notifies when there is no more microtasks enqueued in the current VM Turn. + * This is a hint for Angular to do change detection, which may enqueue more microtasks. + * For this reason this event can fire multiple times per VM Turn. + */ + this.onMicrotaskEmpty = new EventEmitter(false); + /** + * Notifies when the last `onMicrotaskEmpty` has run and there are no more microtasks, which + * implies we are about to relinquish VM turn. + * This event gets called just once. + */ + this.onStable = new EventEmitter(false); + /** + * Notifies that an error has been delivered. + */ + this.onError = new EventEmitter(false); + if (typeof Zone == 'undefined') { + throw new RuntimeError(908 /* RuntimeErrorCode.MISSING_ZONEJS */, ngDevMode && `In this configuration Angular requires Zone.js`); } - } - forEachOperation(fn) { - let nextIt = this._itHead; - let nextRemove = this._removalsHead; - let addRemoveOffset = 0; - let moveOffsets = null; - while (nextIt || nextRemove) { - // Figure out which is the next record to process - // Order: remove, add, move - const record = !nextRemove || nextIt && nextIt.currentIndex < getPreviousIndex(nextRemove, addRemoveOffset, moveOffsets) ? nextIt : nextRemove; - const adjPreviousIndex = getPreviousIndex(record, addRemoveOffset, moveOffsets); - const currentIndex = record.currentIndex; - // consume the item, and adjust the addRemoveOffset and update moveDistance if necessary - if (record === nextRemove) { - addRemoveOffset--; - nextRemove = nextRemove._nextRemoved; - } else { - nextIt = nextIt._next; - if (record.previousIndex == null) { - addRemoveOffset++; - } else { - // INVARIANT: currentIndex < previousIndex - if (!moveOffsets) moveOffsets = []; - const localMovePreviousIndex = adjPreviousIndex - addRemoveOffset; - const localCurrentIndex = currentIndex - addRemoveOffset; - if (localMovePreviousIndex != localCurrentIndex) { - for (let i = 0; i < localMovePreviousIndex; i++) { - const offset = i < moveOffsets.length ? moveOffsets[i] : moveOffsets[i] = 0; - const index = offset + i; - if (localCurrentIndex <= index && index < localMovePreviousIndex) { - moveOffsets[i] = offset + 1; - } - } - const previousIndex = record.previousIndex; - moveOffsets[previousIndex] = localCurrentIndex - localMovePreviousIndex; - } - } - } - if (adjPreviousIndex !== currentIndex) { - fn(record, adjPreviousIndex, currentIndex); - } + Zone.assertZonePatched(); + const self = this; + self._nesting = 0; + self._outer = self._inner = Zone.current; + // AsyncStackTaggingZoneSpec provides `linked stack traces` to show + // where the async operation is scheduled. For more details, refer + // to this article, https://developer.chrome.com/blog/devtools-better-angular-debugging/ + // And we only import this AsyncStackTaggingZoneSpec in development mode, + // in the production mode, the AsyncStackTaggingZoneSpec will be tree shaken away. + if (ngDevMode) { + self._inner = self._inner.fork(new AsyncStackTaggingZoneSpec('Angular')); } - } - forEachPreviousItem(fn) { - let record; - for (record = this._previousItHead; record !== null; record = record._nextPrevious) { - fn(record); + if (Zone['TaskTrackingZoneSpec']) { + self._inner = self._inner.fork(new Zone['TaskTrackingZoneSpec']()); } - } - forEachAddedItem(fn) { - let record; - for (record = this._additionsHead; record !== null; record = record._nextAdded) { - fn(record); + if (enableLongStackTrace && Zone['longStackTraceZoneSpec']) { + self._inner = self._inner.fork(Zone['longStackTraceZoneSpec']); } + // if shouldCoalesceRunChangeDetection is true, all tasks including event tasks will be + // coalesced, so shouldCoalesceEventChangeDetection option is not necessary and can be skipped. + self.shouldCoalesceEventChangeDetection = !shouldCoalesceRunChangeDetection && shouldCoalesceEventChangeDetection; + self.shouldCoalesceRunChangeDetection = shouldCoalesceRunChangeDetection; + self.lastRequestAnimationFrameId = -1; + self.nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame; + forkInnerZoneWithAngularBehavior(self); } - forEachMovedItem(fn) { - let record; - for (record = this._movesHead; record !== null; record = record._nextMoved) { - fn(record); - } + /** + This method checks whether the method call happens within an Angular Zone instance. + */ + static isInAngularZone() { + // Zone needs to be checked, because this method might be called even when NoopNgZone is used. + return typeof Zone !== 'undefined' && Zone.current.get('isAngularZone') === true; } - forEachRemovedItem(fn) { - let record; - for (record = this._removalsHead; record !== null; record = record._nextRemoved) { - fn(record); + /** + Assures that the method is called within the Angular Zone, otherwise throws an error. + */ + static assertInAngularZone() { + if (!NgZone.isInAngularZone()) { + throw new RuntimeError(909 /* RuntimeErrorCode.UNEXPECTED_ZONE_STATE */, ngDevMode && 'Expected to be in Angular Zone, but it is not!'); } } - forEachIdentityChange(fn) { - let record; - for (record = this._identityChangesHead; record !== null; record = record._nextIdentityChange) { - fn(record); + /** + Assures that the method is called outside of the Angular Zone, otherwise throws an error. + */ + static assertNotInAngularZone() { + if (NgZone.isInAngularZone()) { + throw new RuntimeError(909 /* RuntimeErrorCode.UNEXPECTED_ZONE_STATE */, ngDevMode && 'Expected to not be in Angular Zone, but it is!'); } } - diff(collection) { - if (collection == null) collection = []; - if (!isListLikeIterable(collection)) { - throw new RuntimeError(900 /* RuntimeErrorCode.INVALID_DIFFER_INPUT */, ngDevMode && `Error trying to diff '${stringify(collection)}'. Only arrays and iterables are allowed`); - } - if (this.check(collection)) { - return this; - } else { - return null; - } - } - onDestroy() {} - check(collection) { - this._reset(); - let record = this._itHead; - let mayBeDirty = false; - let index; - let item; - let itemTrackBy; - if (Array.isArray(collection)) { - this.length = collection.length; - for (let index = 0; index < this.length; index++) { - item = collection[index]; - itemTrackBy = this._trackByFn(index, item); - if (record === null || !Object.is(record.trackById, itemTrackBy)) { - record = this._mismatch(record, item, itemTrackBy, index); - mayBeDirty = true; - } else { - if (mayBeDirty) { - // TODO(misko): can we limit this to duplicates only? - record = this._verifyReinsertion(record, item, itemTrackBy, index); - } - if (!Object.is(record.item, item)) this._addIdentityChange(record, item); - } - record = record._next; - } - } else { - index = 0; - iterateListLike(collection, item => { - itemTrackBy = this._trackByFn(index, item); - if (record === null || !Object.is(record.trackById, itemTrackBy)) { - record = this._mismatch(record, item, itemTrackBy, index); - mayBeDirty = true; - } else { - if (mayBeDirty) { - // TODO(misko): can we limit this to duplicates only? - record = this._verifyReinsertion(record, item, itemTrackBy, index); - } - if (!Object.is(record.item, item)) this._addIdentityChange(record, item); - } - record = record._next; - index++; - }); - this.length = index; - } - this._truncate(record); - this.collection = collection; - return this.isDirty; - } - /* CollectionChanges is considered dirty if it has any additions, moves, removals, or identity - * changes. - */ - get isDirty() { - return this._additionsHead !== null || this._movesHead !== null || this._removalsHead !== null || this._identityChangesHead !== null; + /** + * Executes the `fn` function synchronously within the Angular zone and returns value returned by + * the function. + * + * Running functions via `run` allows you to reenter Angular zone from a task that was executed + * outside of the Angular zone (typically started via {@link #runOutsideAngular}). + * + * Any future tasks or microtasks scheduled from within this function will continue executing from + * within the Angular zone. + * + * If a synchronous error happens it will be rethrown and not reported via `onError`. + */ + run(fn, applyThis, applyArgs) { + return this._inner.run(fn, applyThis, applyArgs); } /** - * Reset the state of the change objects to show no changes. This means set previousKey to - * currentKey, and clear all of the queues (additions, moves, removals). - * Set the previousIndexes of moved and added items to their currentIndexes - * Reset the list of additions, moves and removals + * Executes the `fn` function synchronously within the Angular zone as a task and returns value + * returned by the function. * - * @internal + * Running functions via `run` allows you to reenter Angular zone from a task that was executed + * outside of the Angular zone (typically started via {@link #runOutsideAngular}). + * + * Any future tasks or microtasks scheduled from within this function will continue executing from + * within the Angular zone. + * + * If a synchronous error happens it will be rethrown and not reported via `onError`. */ - _reset() { - if (this.isDirty) { - let record; - for (record = this._previousItHead = this._itHead; record !== null; record = record._next) { - record._nextPrevious = record._next; - } - for (record = this._additionsHead; record !== null; record = record._nextAdded) { - record.previousIndex = record.currentIndex; - } - this._additionsHead = this._additionsTail = null; - for (record = this._movesHead; record !== null; record = record._nextMoved) { - record.previousIndex = record.currentIndex; - } - this._movesHead = this._movesTail = null; - this._removalsHead = this._removalsTail = null; - this._identityChangesHead = this._identityChangesTail = null; - // TODO(vicb): when assert gets supported - // assert(!this.isDirty); + runTask(fn, applyThis, applyArgs, name) { + const zone = this._inner; + const task = zone.scheduleEventTask('NgZoneEvent: ' + name, fn, EMPTY_PAYLOAD, noop, noop); + try { + return zone.runTask(task, applyThis, applyArgs); + } finally { + zone.cancelTask(task); } } /** - * This is the core function which handles differences between collections. - * - * - `record` is the record which we saw at this position last time. If null then it is a new - * item. - * - `item` is the current item in the collection - * - `index` is the position of the item in the collection - * - * @internal + * Same as `run`, except that synchronous errors are caught and forwarded via `onError` and not + * rethrown. */ - _mismatch(record, item, itemTrackBy, index) { - // The previous record after which we will append the current one. - let previousRecord; - if (record === null) { - previousRecord = this._itTail; - } else { - previousRecord = record._prev; - // Remove the record from the collection since we know it does not match the item. - this._remove(record); - } - // See if we have evicted the item, which used to be at some anterior position of _itHead list. - record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null); - if (record !== null) { - // It is an item which we have evicted earlier: reinsert it back into the list. - // But first we need to check if identity changed, so we can update in view if necessary. - if (!Object.is(record.item, item)) this._addIdentityChange(record, item); - this._reinsertAfter(record, previousRecord, index); - } else { - // Attempt to see if the item is at some posterior position of _itHead list. - record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index); - if (record !== null) { - // We have the item in _itHead at/after `index` position. We need to move it forward in the - // collection. - // But first we need to check if identity changed, so we can update in view if necessary. - if (!Object.is(record.item, item)) this._addIdentityChange(record, item); - this._moveAfter(record, previousRecord, index); - } else { - // It is a new item: add it. - record = this._addAfter(new IterableChangeRecord_(item, itemTrackBy), previousRecord, index); - } - } - return record; + runGuarded(fn, applyThis, applyArgs) { + return this._inner.runGuarded(fn, applyThis, applyArgs); } /** - * This check is only needed if an array contains duplicates. (Short circuit of nothing dirty) - * - * Use case: `[a, a]` => `[b, a, a]` - * - * If we did not have this check then the insertion of `b` would: - * 1) evict first `a` - * 2) insert `b` at `0` index. - * 3) leave `a` at index `1` as is. <-- this is wrong! - * 3) reinsert `a` at index 2. <-- this is wrong! - * - * The correct behavior is: - * 1) evict first `a` - * 2) insert `b` at `0` index. - * 3) reinsert `a` at index 1. - * 3) move `a` at from `1` to `2`. + * Executes the `fn` function synchronously in Angular's parent zone and returns value returned by + * the function. * + * Running functions via {@link #runOutsideAngular} allows you to escape Angular's zone and do + * work that + * doesn't trigger Angular change-detection or is subject to Angular's error handling. * - * Double check that we have not evicted a duplicate item. We need to check if the item type may - * have already been removed: - * The insertion of b will evict the first 'a'. If we don't reinsert it now it will be reinserted - * at the end. Which will show up as the two 'a's switching position. This is incorrect, since a - * better way to think of it is as insert of 'b' rather then switch 'a' with 'b' and then add 'a' - * at the end. + * Any future tasks or microtasks scheduled from within this function will continue executing from + * outside of the Angular zone. * - * @internal + * Use {@link #run} to reenter the Angular zone and do work that updates the application model. */ - _verifyReinsertion(record, item, itemTrackBy, index) { - let reinsertRecord = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null); - if (reinsertRecord !== null) { - record = this._reinsertAfter(reinsertRecord, record._prev, index); - } else if (record.currentIndex != index) { - record.currentIndex = index; - this._addToMoves(record, index); + runOutsideAngular(fn) { + return this._outer.run(fn); + } +} +const EMPTY_PAYLOAD = {}; +function checkStable(zone) { + // TODO: @JiaLiPassion, should check zone.isCheckStableRunning to prevent + // re-entry. The case is: + // + // @Component({...}) + // export class AppComponent { + // constructor(private ngZone: NgZone) { + // this.ngZone.onStable.subscribe(() => { + // this.ngZone.run(() => console.log('stable');); + // }); + // } + // + // The onStable subscriber run another function inside ngZone + // which causes `checkStable()` re-entry. + // But this fix causes some issues in g3, so this fix will be + // launched in another PR. + if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) { + try { + zone._nesting++; + zone.onMicrotaskEmpty.emit(null); + } finally { + zone._nesting--; + if (!zone.hasPendingMicrotasks) { + try { + zone.runOutsideAngular(() => zone.onStable.emit(null)); + } finally { + zone.isStable = true; + } + } } - return record; } +} +function delayChangeDetectionForEvents(zone) { /** - * Get rid of any excess {@link IterableChangeRecord_}s from the previous collection + * We also need to check _nesting here + * Consider the following case with shouldCoalesceRunChangeDetection = true * - * - `record` The first excess {@link IterableChangeRecord_}. + * ngZone.run(() => {}); + * ngZone.run(() => {}); * - * @internal + * We want the two `ngZone.run()` only trigger one change detection + * when shouldCoalesceRunChangeDetection is true. + * And because in this case, change detection run in async way(requestAnimationFrame), + * so we also need to check the _nesting here to prevent multiple + * change detections. */ - _truncate(record) { - // Anything after that needs to be removed; - while (record !== null) { - const nextRecord = record._next; - this._addToRemovals(this._unlink(record)); - record = nextRecord; - } - if (this._unlinkedRecords !== null) { - this._unlinkedRecords.clear(); - } - if (this._additionsTail !== null) { - this._additionsTail._nextAdded = null; - } - if (this._movesTail !== null) { - this._movesTail._nextMoved = null; - } - if (this._itTail !== null) { - this._itTail._next = null; - } - if (this._removalsTail !== null) { - this._removalsTail._nextRemoved = null; - } - if (this._identityChangesTail !== null) { - this._identityChangesTail._nextIdentityChange = null; - } + if (zone.isCheckStableRunning || zone.lastRequestAnimationFrameId !== -1) { + return; } - /** @internal */ - _reinsertAfter(record, prevRecord, index) { - if (this._unlinkedRecords !== null) { - this._unlinkedRecords.remove(record); - } - const prev = record._prevRemoved; - const next = record._nextRemoved; - if (prev === null) { - this._removalsHead = next; - } else { - prev._nextRemoved = next; + zone.lastRequestAnimationFrameId = zone.nativeRequestAnimationFrame.call(_global, () => { + // This is a work around for https://github.com/angular/angular/issues/36839. + // The core issue is that when event coalescing is enabled it is possible for microtasks + // to get flushed too early (As is the case with `Promise.then`) between the + // coalescing eventTasks. + // + // To workaround this we schedule a "fake" eventTask before we process the + // coalescing eventTasks. The benefit of this is that the "fake" container eventTask + // will prevent the microtasks queue from getting drained in between the coalescing + // eventTask execution. + if (!zone.fakeTopEventTask) { + zone.fakeTopEventTask = Zone.root.scheduleEventTask('fakeTopEventTask', () => { + zone.lastRequestAnimationFrameId = -1; + updateMicroTaskStatus(zone); + zone.isCheckStableRunning = true; + checkStable(zone); + zone.isCheckStableRunning = false; + }, undefined, () => {}, () => {}); } - if (next === null) { - this._removalsTail = prev; - } else { - next._prevRemoved = prev; + zone.fakeTopEventTask.invoke(); + }); + updateMicroTaskStatus(zone); +} +function forkInnerZoneWithAngularBehavior(zone) { + const delayChangeDetectionForEventsDelegate = () => { + delayChangeDetectionForEvents(zone); + }; + zone._inner = zone._inner.fork({ + name: 'angular', + properties: { + 'isAngularZone': true + }, + onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => { + if (shouldBeIgnoredByZone(applyArgs)) { + return delegate.invokeTask(target, task, applyThis, applyArgs); + } + try { + onEnter(zone); + return delegate.invokeTask(target, task, applyThis, applyArgs); + } finally { + if (zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask' || zone.shouldCoalesceRunChangeDetection) { + delayChangeDetectionForEventsDelegate(); + } + onLeave(zone); + } + }, + onInvoke: (delegate, current, target, callback, applyThis, applyArgs, source) => { + try { + onEnter(zone); + return delegate.invoke(target, callback, applyThis, applyArgs, source); + } finally { + if (zone.shouldCoalesceRunChangeDetection) { + delayChangeDetectionForEventsDelegate(); + } + onLeave(zone); + } + }, + onHasTask: (delegate, current, target, hasTaskState) => { + delegate.hasTask(target, hasTaskState); + if (current === target) { + // We are only interested in hasTask events which originate from our zone + // (A child hasTask event is not interesting to us) + if (hasTaskState.change == 'microTask') { + zone._hasPendingMicrotasks = hasTaskState.microTask; + updateMicroTaskStatus(zone); + checkStable(zone); + } else if (hasTaskState.change == 'macroTask') { + zone.hasPendingMacrotasks = hasTaskState.macroTask; + } + } + }, + onHandleError: (delegate, current, target, error) => { + delegate.handleError(target, error); + zone.runOutsideAngular(() => zone.onError.emit(error)); + return false; } - this._insertAfter(record, prevRecord, index); - this._addToMoves(record, index); - return record; + }); +} +function updateMicroTaskStatus(zone) { + if (zone._hasPendingMicrotasks || (zone.shouldCoalesceEventChangeDetection || zone.shouldCoalesceRunChangeDetection) && zone.lastRequestAnimationFrameId !== -1) { + zone.hasPendingMicrotasks = true; + } else { + zone.hasPendingMicrotasks = false; } - /** @internal */ - _moveAfter(record, prevRecord, index) { - this._unlink(record); - this._insertAfter(record, prevRecord, index); - this._addToMoves(record, index); - return record; +} +function onEnter(zone) { + zone._nesting++; + if (zone.isStable) { + zone.isStable = false; + zone.onUnstable.emit(null); } - /** @internal */ - _addAfter(record, prevRecord, index) { - this._insertAfter(record, prevRecord, index); - if (this._additionsTail === null) { - // TODO(vicb): - // assert(this._additionsHead === null); - this._additionsTail = this._additionsHead = record; - } else { - // TODO(vicb): - // assert(_additionsTail._nextAdded === null); - // assert(record._nextAdded === null); - this._additionsTail = this._additionsTail._nextAdded = record; - } - return record; +} +function onLeave(zone) { + zone._nesting--; + checkStable(zone); +} +/** + * Provides a noop implementation of `NgZone` which does nothing. This zone requires explicit calls + * to framework to perform rendering. + */ +class NoopNgZone { + constructor() { + this.hasPendingMicrotasks = false; + this.hasPendingMacrotasks = false; + this.isStable = true; + this.onUnstable = new EventEmitter(); + this.onMicrotaskEmpty = new EventEmitter(); + this.onStable = new EventEmitter(); + this.onError = new EventEmitter(); } - /** @internal */ - _insertAfter(record, prevRecord, index) { - // TODO(vicb): - // assert(record != prevRecord); - // assert(record._next === null); - // assert(record._prev === null); - const next = prevRecord === null ? this._itHead : prevRecord._next; - // TODO(vicb): - // assert(next != record); - // assert(prevRecord != record); - record._next = next; - record._prev = prevRecord; - if (next === null) { - this._itTail = record; - } else { - next._prev = record; - } - if (prevRecord === null) { - this._itHead = record; - } else { - prevRecord._next = record; - } - if (this._linkedRecords === null) { - this._linkedRecords = new _DuplicateMap(); - } - this._linkedRecords.put(record); - record.currentIndex = index; - return record; + run(fn, applyThis, applyArgs) { + return fn.apply(applyThis, applyArgs); } - /** @internal */ - _remove(record) { - return this._addToRemovals(this._unlink(record)); + runGuarded(fn, applyThis, applyArgs) { + return fn.apply(applyThis, applyArgs); } - /** @internal */ - _unlink(record) { - if (this._linkedRecords !== null) { - this._linkedRecords.remove(record); - } - const prev = record._prev; - const next = record._next; - // TODO(vicb): - // assert((record._prev = null) === null); - // assert((record._next = null) === null); - if (prev === null) { - this._itHead = next; - } else { - prev._next = next; - } - if (next === null) { - this._itTail = prev; - } else { - next._prev = prev; - } - return record; + runOutsideAngular(fn) { + return fn(); } - /** @internal */ - _addToMoves(record, toIndex) { - // TODO(vicb): - // assert(record._nextMoved === null); - if (record.previousIndex === toIndex) { - return record; - } - if (this._movesTail === null) { - // TODO(vicb): - // assert(_movesHead === null); - this._movesTail = this._movesHead = record; - } else { - // TODO(vicb): - // assert(_movesTail._nextMoved === null); - this._movesTail = this._movesTail._nextMoved = record; - } - return record; + runTask(fn, applyThis, applyArgs, name) { + return fn.apply(applyThis, applyArgs); } - _addToRemovals(record) { - if (this._unlinkedRecords === null) { - this._unlinkedRecords = new _DuplicateMap(); - } - this._unlinkedRecords.put(record); - record.currentIndex = null; - record._nextRemoved = null; - if (this._removalsTail === null) { - // TODO(vicb): - // assert(_removalsHead === null); - this._removalsTail = this._removalsHead = record; - record._prevRemoved = null; - } else { - // TODO(vicb): - // assert(_removalsTail._nextRemoved === null); - // assert(record._nextRemoved === null); - record._prevRemoved = this._removalsTail; - this._removalsTail = this._removalsTail._nextRemoved = record; - } - return record; +} +function shouldBeIgnoredByZone(applyArgs) { + if (!Array.isArray(applyArgs)) { + return false; } - /** @internal */ - _addIdentityChange(record, item) { - record.item = item; - if (this._identityChangesTail === null) { - this._identityChangesTail = this._identityChangesHead = record; - } else { - this._identityChangesTail = this._identityChangesTail._nextIdentityChange = record; - } - return record; + // We should only ever get 1 arg passed through to invokeTask. + // Short circuit here incase that behavior changes. + if (applyArgs.length !== 1) { + return false; } + // Prevent triggering change detection when the __ignore_ng_zone__ flag is detected. + return applyArgs[0].data?.['__ignore_ng_zone__'] === true; } -class IterableChangeRecord_ { - constructor(item, trackById) { - this.item = item; - this.trackById = trackById; - this.currentIndex = null; - this.previousIndex = null; - /** @internal */ - this._nextPrevious = null; - /** @internal */ - this._prev = null; - /** @internal */ - this._next = null; - /** @internal */ - this._prevDup = null; - /** @internal */ - this._nextDup = null; - /** @internal */ - this._prevRemoved = null; - /** @internal */ - this._nextRemoved = null; - /** @internal */ - this._nextAdded = null; - /** @internal */ - this._nextMoved = null; - /** @internal */ - this._nextIdentityChange = null; +function getNgZone(ngZoneToUse = 'zone.js', options) { + if (ngZoneToUse === 'noop') { + return new NoopNgZone(); } -} -// A linked list of IterableChangeRecords with the same IterableChangeRecord_.item -class _DuplicateItemRecordList { - constructor() { - /** @internal */ - this._head = null; - /** @internal */ - this._tail = null; + if (ngZoneToUse === 'zone.js') { + return new NgZone(options); } + return ngZoneToUse; +} + +/** + * The phase to run an `afterRender` or `afterNextRender` callback in. + * + * Callbacks in the same phase run in the order they are registered. Phases run in the + * following order after each render: + * + * 1. `AfterRenderPhase.EarlyRead` + * 2. `AfterRenderPhase.Write` + * 3. `AfterRenderPhase.MixedReadWrite` + * 4. `AfterRenderPhase.Read` + * + * Angular is unable to verify or enforce that phases are used correctly, and instead + * relies on each developer to follow the guidelines documented for each value and + * carefully choose the appropriate one, refactoring their code if necessary. By doing + * so, Angular is better able to minimize the performance degradation associated with + * manual DOM access, ensuring the best experience for the end users of your application + * or library. + * + * @developerPreview + */ +var AfterRenderPhase = /*#__PURE__*/function (AfterRenderPhase) { /** - * Append the record to the list of duplicates. + * Use `AfterRenderPhase.EarlyRead` for callbacks that only need to **read** from the + * DOM before a subsequent `AfterRenderPhase.Write` callback, for example to perform + * custom layout that the browser doesn't natively support. **Never** use this phase + * for callbacks that can write to the DOM or when `AfterRenderPhase.Read` is adequate. * - * Note: by design all records in the list of duplicates hold the same value in record.item. + *
+ * + * Using this value can degrade performance. + * Instead, prefer using built-in browser functionality when possible. + * + *
*/ - add(record) { - if (this._head === null) { - this._head = this._tail = record; - record._nextDup = null; - record._prevDup = null; - } else { - // TODO(vicb): - // assert(record.item == _head.item || - // record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN); - this._tail._nextDup = record; - record._prevDup = this._tail; - record._nextDup = null; - this._tail = record; - } - } - // Returns a IterableChangeRecord_ having IterableChangeRecord_.trackById == trackById and - // IterableChangeRecord_.currentIndex >= atOrAfterIndex - get(trackById, atOrAfterIndex) { - let record; - for (record = this._head; record !== null; record = record._nextDup) { - if ((atOrAfterIndex === null || atOrAfterIndex <= record.currentIndex) && Object.is(record.trackById, trackById)) { - return record; - } - } - return null; - } + AfterRenderPhase[AfterRenderPhase["EarlyRead"] = 0] = "EarlyRead"; /** - * Remove one {@link IterableChangeRecord_} from the list of duplicates. - * - * Returns whether the list of duplicates is empty. + * Use `AfterRenderPhase.Write` for callbacks that only **write** to the DOM. **Never** + * use this phase for callbacks that can read from the DOM. */ - remove(record) { - // TODO(vicb): - // assert(() { - // // verify that the record being removed is in the list. - // for (IterableChangeRecord_ cursor = _head; cursor != null; cursor = cursor._nextDup) { - // if (identical(cursor, record)) return true; - // } - // return false; - //}); - const prev = record._prevDup; - const next = record._nextDup; - if (prev === null) { - this._head = next; - } else { - prev._nextDup = next; - } - if (next === null) { - this._tail = prev; - } else { - next._prevDup = prev; - } - return this._head === null; - } -} -class _DuplicateMap { - constructor() { - this.map = new Map(); - } - put(record) { - const key = record.trackById; - let duplicates = this.map.get(key); - if (!duplicates) { - duplicates = new _DuplicateItemRecordList(); - this.map.set(key, duplicates); - } - duplicates.add(record); - } + AfterRenderPhase[AfterRenderPhase["Write"] = 1] = "Write"; /** - * Retrieve the `value` using key. Because the IterableChangeRecord_ value may be one which we - * have already iterated over, we use the `atOrAfterIndex` to pretend it is not there. + * Use `AfterRenderPhase.MixedReadWrite` for callbacks that read from or write to the + * DOM, that haven't been refactored to use a different phase. **Never** use this phase + * for callbacks that can use a different phase instead. * - * Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we - * have any more `a`s needs to return the second `a`. + *
+ * + * Using this value can **significantly** degrade performance. + * Instead, prefer refactoring into multiple callbacks using a more specific phase. + * + *
*/ - get(trackById, atOrAfterIndex) { - const key = trackById; - const recordList = this.map.get(key); - return recordList ? recordList.get(trackById, atOrAfterIndex) : null; - } + AfterRenderPhase[AfterRenderPhase["MixedReadWrite"] = 2] = "MixedReadWrite"; /** - * Removes a {@link IterableChangeRecord_} from the list of duplicates. - * - * The list of duplicates also is removed from the map if it gets empty. + * Use `AfterRenderPhase.Read` for callbacks that only **read** from the DOM. **Never** + * use this phase for callbacks that can write to the DOM. */ - remove(record) { - const key = record.trackById; - const recordList = this.map.get(key); - // Remove the list of duplicates when it gets empty - if (recordList.remove(record)) { - this.map.delete(key); - } - return record; - } - get isEmpty() { - return this.map.size === 0; - } - clear() { - this.map.clear(); + AfterRenderPhase[AfterRenderPhase["Read"] = 3] = "Read"; + return AfterRenderPhase; +}(AfterRenderPhase || {}); +/** `AfterRenderRef` that does nothing. */ +const NOOP_AFTER_RENDER_REF = { + destroy() {} +}; +/** + * Register a callback to run once before any userspace `afterRender` or + * `afterNextRender` callbacks. + * + * This function should almost always be used instead of `afterRender` or + * `afterNextRender` for implementing framework functionality. Consider: + * + * 1.) `AfterRenderPhase.EarlyRead` is intended to be used for implementing + * custom layout. If the framework itself mutates the DOM after *any* + * `AfterRenderPhase.EarlyRead` callbacks are run, the phase can no + * longer reliably serve its purpose. + * + * 2.) Importing `afterRender` in the framework can reduce the ability for it + * to be tree-shaken, and the framework shouldn't need much of the behavior. + */ +function internalAfterNextRender(callback, options) { + const injector = options?.injector ?? inject(Injector); + // Similarly to the public `afterNextRender` function, an internal one + // is only invoked in a browser as long as the runOnServer option is not set. + if (!options?.runOnServer && !isPlatformBrowser(injector)) return; + const afterRenderEventManager = injector.get(AfterRenderEventManager); + afterRenderEventManager.internalCallbacks.push(callback); +} +/** + * Register a callback to be invoked each time the application + * finishes rendering. + * + *
+ * + * You should always explicitly specify a non-default [phase](api/core/AfterRenderPhase), or you + * risk significant performance degradation. + * + *
+ * + * Note that the callback will run + * - in the order it was registered + * - once per render + * - on browser platforms only + * + *
+ * + * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs. + * You must use caution when directly reading or writing the DOM and layout. + * + *
+ * + * @param callback A callback function to register + * + * @usageNotes + * + * Use `afterRender` to read or write the DOM after each render. + * + * ### Example + * ```ts + * @Component({ + * selector: 'my-cmp', + * template: `{{ ... }}`, + * }) + * export class MyComponent { + * @ViewChild('content') contentRef: ElementRef; + * + * constructor() { + * afterRender(() => { + * console.log('content height: ' + this.contentRef.nativeElement.scrollHeight); + * }, {phase: AfterRenderPhase.Read}); + * } + * } + * ``` + * + * @developerPreview + */ +function afterRender(callback, options) { + ngDevMode && assertNotInReactiveContext(afterRender, 'Call `afterRender` outside of a reactive context. For example, schedule the render ' + 'callback inside the component constructor`.'); + !options && assertInInjectionContext(afterRender); + const injector = options?.injector ?? inject(Injector); + if (!isPlatformBrowser(injector)) { + return NOOP_AFTER_RENDER_REF; } + performanceMarkFeature('NgAfterRender'); + const afterRenderEventManager = injector.get(AfterRenderEventManager); + // Lazily initialize the handler implementation, if necessary. This is so that it can be + // tree-shaken if `afterRender` and `afterNextRender` aren't used. + const callbackHandler = afterRenderEventManager.handler ??= new AfterRenderCallbackHandlerImpl(); + const phase = options?.phase ?? AfterRenderPhase.MixedReadWrite; + const destroy = () => { + callbackHandler.unregister(instance); + unregisterFn(); + }; + const unregisterFn = injector.get(DestroyRef).onDestroy(destroy); + const instance = new AfterRenderCallback(injector, phase, callback); + callbackHandler.register(instance); + return { + destroy + }; } -function getPreviousIndex(item, addRemoveOffset, moveOffsets) { - const previousIndex = item.previousIndex; - if (previousIndex === null) return previousIndex; - let moveOffset = 0; - if (moveOffsets && previousIndex < moveOffsets.length) { - moveOffset = moveOffsets[previousIndex]; +/** + * Register a callback to be invoked the next time the application + * finishes rendering. + * + *
+ * + * You should always explicitly specify a non-default [phase](api/core/AfterRenderPhase), or you + * risk significant performance degradation. + * + *
+ * + * Note that the callback will run + * - in the order it was registered + * - on browser platforms only + * + *
+ * + * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs. + * You must use caution when directly reading or writing the DOM and layout. + * + *
+ * + * @param callback A callback function to register + * + * @usageNotes + * + * Use `afterNextRender` to read or write the DOM once, + * for example to initialize a non-Angular library. + * + * ### Example + * ```ts + * @Component({ + * selector: 'my-chart-cmp', + * template: `
{{ ... }}
`, + * }) + * export class MyChartCmp { + * @ViewChild('chart') chartRef: ElementRef; + * chart: MyChart|null; + * + * constructor() { + * afterNextRender(() => { + * this.chart = new MyChart(this.chartRef.nativeElement); + * }, {phase: AfterRenderPhase.Write}); + * } + * } + * ``` + * + * @developerPreview + */ +function afterNextRender(callback, options) { + !options && assertInInjectionContext(afterNextRender); + const injector = options?.injector ?? inject(Injector); + if (!isPlatformBrowser(injector)) { + return NOOP_AFTER_RENDER_REF; } - return previousIndex + addRemoveOffset + moveOffset; + performanceMarkFeature('NgAfterNextRender'); + const afterRenderEventManager = injector.get(AfterRenderEventManager); + // Lazily initialize the handler implementation, if necessary. This is so that it can be + // tree-shaken if `afterRender` and `afterNextRender` aren't used. + const callbackHandler = afterRenderEventManager.handler ??= new AfterRenderCallbackHandlerImpl(); + const phase = options?.phase ?? AfterRenderPhase.MixedReadWrite; + const destroy = () => { + callbackHandler.unregister(instance); + unregisterFn(); + }; + const unregisterFn = injector.get(DestroyRef).onDestroy(destroy); + const instance = new AfterRenderCallback(injector, phase, () => { + destroy(); + callback(); + }); + callbackHandler.register(instance); + return { + destroy + }; } -class DefaultKeyValueDifferFactory { - constructor() {} - supports(obj) { - return obj instanceof Map || isJsObject(obj); +/** + * A wrapper around a function to be used as an after render callback. + */ +class AfterRenderCallback { + constructor(injector, phase, callbackFn) { + this.phase = phase; + this.callbackFn = callbackFn; + this.zone = injector.get(NgZone); + this.errorHandler = injector.get(ErrorHandler, null, { + optional: true + }); } - create() { - return new DefaultKeyValueDiffer(); + invoke() { + try { + this.zone.runOutsideAngular(this.callbackFn); + } catch (err) { + this.errorHandler?.handleError(err); + } } } -class DefaultKeyValueDiffer { +/** + * Core functionality for `afterRender` and `afterNextRender`. Kept separate from + * `AfterRenderEventManager` for tree-shaking. + */ +class AfterRenderCallbackHandlerImpl { constructor() { - this._records = new Map(); - this._mapHead = null; - // _appendAfter is used in the check loop - this._appendAfter = null; - this._previousMapHead = null; - this._changesHead = null; - this._changesTail = null; - this._additionsHead = null; - this._additionsTail = null; - this._removalsHead = null; - this._removalsTail = null; + this.executingCallbacks = false; + this.buckets = { + // Note: the order of these keys controls the order the phases are run. + [AfterRenderPhase.EarlyRead]: new Set(), + [AfterRenderPhase.Write]: new Set(), + [AfterRenderPhase.MixedReadWrite]: new Set(), + [AfterRenderPhase.Read]: new Set() + }; + this.deferredCallbacks = new Set(); } - get isDirty() { - return this._additionsHead !== null || this._changesHead !== null || this._removalsHead !== null; + register(callback) { + // If we're currently running callbacks, new callbacks should be deferred + // until the next render operation. + const target = this.executingCallbacks ? this.deferredCallbacks : this.buckets[callback.phase]; + target.add(callback); } - forEachItem(fn) { - let record; - for (record = this._mapHead; record !== null; record = record._next) { - fn(record); - } + unregister(callback) { + this.buckets[callback.phase].delete(callback); + this.deferredCallbacks.delete(callback); } - forEachPreviousItem(fn) { - let record; - for (record = this._previousMapHead; record !== null; record = record._nextPrevious) { - fn(record); + execute() { + this.executingCallbacks = true; + for (const bucket of Object.values(this.buckets)) { + for (const callback of bucket) { + callback.invoke(); + } } - } - forEachChangedItem(fn) { - let record; - for (record = this._changesHead; record !== null; record = record._nextChanged) { - fn(record); + this.executingCallbacks = false; + for (const callback of this.deferredCallbacks) { + this.buckets[callback.phase].add(callback); } + this.deferredCallbacks.clear(); } - forEachAddedItem(fn) { - let record; - for (record = this._additionsHead; record !== null; record = record._nextAdded) { - fn(record); + destroy() { + for (const bucket of Object.values(this.buckets)) { + bucket.clear(); } + this.deferredCallbacks.clear(); } - forEachRemovedItem(fn) { - let record; - for (record = this._removalsHead; record !== null; record = record._nextRemoved) { - fn(record); +} +/** + * Implements core timing for `afterRender` and `afterNextRender` events. + * Delegates to an optional `AfterRenderCallbackHandler` for implementation. + */ +let AfterRenderEventManager = /*#__PURE__*/(() => { + class AfterRenderEventManager { + constructor() { + /* @internal */ + this.handler = null; + /* @internal */ + this.internalCallbacks = []; } - } - diff(map) { - if (!map) { - map = new Map(); - } else if (!(map instanceof Map || isJsObject(map))) { - throw new RuntimeError(900 /* RuntimeErrorCode.INVALID_DIFFER_INPUT */, ngDevMode && `Error trying to diff '${stringify(map)}'. Only maps and objects are allowed`); + /** + * Executes internal and user-provided callbacks. + */ + execute() { + this.executeInternalCallbacks(); + this.handler?.execute(); } - return this.check(map) ? this : null; - } - onDestroy() {} - /** - * Check the current state of the map vs the previous. - * The algorithm is optimised for when the keys do no change. - */ - check(map) { - this._reset(); - let insertBefore = this._mapHead; - this._appendAfter = null; - this._forEach(map, (value, key) => { - if (insertBefore && insertBefore.key === key) { - this._maybeAddToChanges(insertBefore, value); - this._appendAfter = insertBefore; - insertBefore = insertBefore._next; - } else { - const record = this._getOrCreateRecordForKey(key, value); - insertBefore = this._insertBeforeOrAppend(insertBefore, record); - } - }); - // Items remaining at the end of the list have been deleted - if (insertBefore) { - if (insertBefore._prev) { - insertBefore._prev._next = null; + executeInternalCallbacks() { + // Note: internal callbacks power `internalAfterNextRender`. Since internal callbacks + // are fairly trivial, they are kept separate so that `AfterRenderCallbackHandlerImpl` + // can still be tree-shaken unless used by the application. + const callbacks = [...this.internalCallbacks]; + this.internalCallbacks.length = 0; + for (const callback of callbacks) { + callback(); } - this._removalsHead = insertBefore; - for (let record = insertBefore; record !== null; record = record._nextRemoved) { - if (record === this._mapHead) { - this._mapHead = null; - } - this._records.delete(record.key); - record._nextRemoved = record._next; - record.previousValue = record.currentValue; - record.currentValue = null; - record._prev = null; - record._next = null; + } + ngOnDestroy() { + this.handler?.destroy(); + this.handler = null; + this.internalCallbacks.length = 0; + } + /** @nocollapse */ + static #_ = this.ɵprov = ɵɵdefineInjectable({ + token: AfterRenderEventManager, + providedIn: 'root', + factory: () => new AfterRenderEventManager() + }); + } + return AfterRenderEventManager; +})(); +function isModuleWithProviders(value) { + return value.ngModule !== undefined; +} +function isNgModule(value) { + return !!getNgModuleDef(value); +} +function isPipe(value) { + return !!getPipeDef$1(value); +} +function isDirective(value) { + return !!getDirectiveDef(value); +} +function isComponent(value) { + return !!getComponentDef(value); +} +function getDependencyTypeForError(type) { + if (getComponentDef(type)) return 'component'; + if (getDirectiveDef(type)) return 'directive'; + if (getPipeDef$1(type)) return 'pipe'; + return 'type'; +} +function verifyStandaloneImport(depType, importingType) { + if (isForwardRef(depType)) { + depType = resolveForwardRef(depType); + if (!depType) { + throw new Error(`Expected forwardRef function, imported from "${stringifyForError(importingType)}", to return a standalone entity or NgModule but got "${stringifyForError(depType) || depType}".`); + } + } + if (getNgModuleDef(depType) == null) { + const def = getComponentDef(depType) || getDirectiveDef(depType) || getPipeDef$1(depType); + if (def != null) { + // if a component, directive or pipe is imported make sure that it is standalone + if (!def.standalone) { + throw new Error(`The "${stringifyForError(depType)}" ${getDependencyTypeForError(depType)}, imported from "${stringifyForError(importingType)}", is not standalone. Did you forget to add the standalone: true flag?`); + } + } else { + // it can be either a module with provider or an unknown (not annotated) type + if (isModuleWithProviders(depType)) { + throw new Error(`A module with providers was imported from "${stringifyForError(importingType)}". Modules with providers are not supported in standalone components imports.`); + } else { + throw new Error(`The "${stringifyForError(depType)}" type, imported from "${stringifyForError(importingType)}", must be a standalone component / directive / pipe or an NgModule. Did you forget to add the required @Component / @Directive / @Pipe or @NgModule annotation?`); } } - // Make sure tails have no next records from previous runs - if (this._changesTail) this._changesTail._nextChanged = null; - if (this._additionsTail) this._additionsTail._nextAdded = null; - return this.isDirty; + } +} + +/** + * Indicates whether to use the runtime dependency tracker for scope calculation in JIT compilation. + * The value "false" means the old code path based on patching scope info into the types will be + * used. + * + * @deprecated For migration purposes only, to be removed soon. + */ +const USE_RUNTIME_DEPS_TRACKER_FOR_JIT = true; +/** + * An implementation of DepsTrackerApi which will be used for JIT and local compilation. + */ +class DepsTracker { + constructor() { + this.ownerNgModule = new Map(); + this.ngModulesWithSomeUnresolvedDecls = new Set(); + this.ngModulesScopeCache = new Map(); + this.standaloneComponentsScopeCache = new Map(); } /** - * Inserts a record before `before` or append at the end of the list when `before` is null. - * - * Notes: - * - This method appends at `this._appendAfter`, - * - This method updates `this._appendAfter`, - * - The return value is the new value for the insertion pointer. + * Attempts to resolve ng module's forward ref declarations as much as possible and add them to + * the `ownerNgModule` map. This method normally should be called after the initial parsing when + * all the forward refs are resolved (e.g., when trying to render a component) */ - _insertBeforeOrAppend(before, record) { - if (before) { - const prev = before._prev; - record._next = before; - record._prev = prev; - before._prev = record; - if (prev) { - prev._next = record; - } - if (before === this._mapHead) { - this._mapHead = record; - } - this._appendAfter = before; - return before; + resolveNgModulesDecls() { + if (this.ngModulesWithSomeUnresolvedDecls.size === 0) { + return; } - if (this._appendAfter) { - this._appendAfter._next = record; - record._prev = this._appendAfter; - } else { - this._mapHead = record; + for (const moduleType of this.ngModulesWithSomeUnresolvedDecls) { + const def = getNgModuleDef(moduleType); + if (def?.declarations) { + for (const decl of maybeUnwrapFn(def.declarations)) { + if (isComponent(decl)) { + this.ownerNgModule.set(decl, moduleType); + } + } + } } - this._appendAfter = record; - return null; + this.ngModulesWithSomeUnresolvedDecls.clear(); } - _getOrCreateRecordForKey(key, value) { - if (this._records.has(key)) { - const record = this._records.get(key); - this._maybeAddToChanges(record, value); - const prev = record._prev; - const next = record._next; - if (prev) { - prev._next = next; + /** @override */ + getComponentDependencies(type, rawImports) { + this.resolveNgModulesDecls(); + const def = getComponentDef(type); + if (def === null) { + throw new Error(`Attempting to get component dependencies for a type that is not a component: ${type}`); + } + if (def.standalone) { + const scope = this.getStandaloneComponentScope(type, rawImports); + if (scope.compilation.isPoisoned) { + return { + dependencies: [] + }; } - if (next) { - next._prev = prev; + return { + dependencies: [...scope.compilation.directives, ...scope.compilation.pipes, ...scope.compilation.ngModules] + }; + } else { + if (!this.ownerNgModule.has(type)) { + // This component is orphan! No need to handle the error since the component rendering + // pipeline (e.g., view_container_ref) will check for this error based on configs. + return { + dependencies: [] + }; } - record._next = null; - record._prev = null; - return record; + const scope = this.getNgModuleScope(this.ownerNgModule.get(type)); + if (scope.compilation.isPoisoned) { + return { + dependencies: [] + }; + } + return { + dependencies: [...scope.compilation.directives, ...scope.compilation.pipes] + }; } - const record = new KeyValueChangeRecord_(key); - this._records.set(key, record); - record.currentValue = value; - this._addToAdditions(record); - return record; } - /** @internal */ - _reset() { - if (this.isDirty) { - let record; - // let `_previousMapHead` contain the state of the map before the changes - this._previousMapHead = this._mapHead; - for (record = this._previousMapHead; record !== null; record = record._next) { - record._nextPrevious = record._next; + /** + * @override + * This implementation does not make use of param scopeInfo since it assumes the scope info is + * already added to the type itself through methods like {@link ɵɵsetNgModuleScope} + */ + registerNgModule(type, scopeInfo) { + if (!isNgModule(type)) { + throw new Error(`Attempting to register a Type which is not NgModule as NgModule: ${type}`); + } + // Lazily process the NgModules later when needed. + this.ngModulesWithSomeUnresolvedDecls.add(type); + } + /** @override */ + clearScopeCacheFor(type) { + this.ngModulesScopeCache.delete(type); + this.standaloneComponentsScopeCache.delete(type); + } + /** @override */ + getNgModuleScope(type) { + if (this.ngModulesScopeCache.has(type)) { + return this.ngModulesScopeCache.get(type); + } + const scope = this.computeNgModuleScope(type); + this.ngModulesScopeCache.set(type, scope); + return scope; + } + /** Compute NgModule scope afresh. */ + computeNgModuleScope(type) { + const def = getNgModuleDef(type, true); + const scope = { + exported: { + directives: new Set(), + pipes: new Set() + }, + compilation: { + directives: new Set(), + pipes: new Set() } - // Update `record.previousValue` with the value of the item before the changes - // We need to update all changed items (that's those which have been added and changed) - for (record = this._changesHead; record !== null; record = record._nextChanged) { - record.previousValue = record.currentValue; + }; + // Analyzing imports + for (const imported of maybeUnwrapFn(def.imports)) { + if (isNgModule(imported)) { + const importedScope = this.getNgModuleScope(imported); + // When this module imports another, the imported module's exported directives and pipes + // are added to the compilation scope of this module. + addSet(importedScope.exported.directives, scope.compilation.directives); + addSet(importedScope.exported.pipes, scope.compilation.pipes); + } else if (isStandalone(imported)) { + if (isDirective(imported) || isComponent(imported)) { + scope.compilation.directives.add(imported); + } else if (isPipe(imported)) { + scope.compilation.pipes.add(imported); + } else { + // The standalone thing is neither a component nor a directive nor a pipe ... (what?) + throw new RuntimeError(1000 /* RuntimeErrorCode.RUNTIME_DEPS_INVALID_IMPORTED_TYPE */, 'The standalone imported type is neither a component nor a directive nor a pipe'); + } + } else { + // The import is neither a module nor a module-with-providers nor a standalone thing. This + // is going to be an error. So we short circuit. + scope.compilation.isPoisoned = true; + break; } - for (record = this._additionsHead; record != null; record = record._nextAdded) { - record.previousValue = record.currentValue; + } + // Analyzing declarations + if (!scope.compilation.isPoisoned) { + for (const decl of maybeUnwrapFn(def.declarations)) { + // Cannot declare another NgModule or a standalone thing + if (isNgModule(decl) || isStandalone(decl)) { + scope.compilation.isPoisoned = true; + break; + } + if (isPipe(decl)) { + scope.compilation.pipes.add(decl); + } else { + // decl is either a directive or a component. The component may not yet have the ɵcmp due + // to async compilation. + scope.compilation.directives.add(decl); + } } - this._changesHead = this._changesTail = null; - this._additionsHead = this._additionsTail = null; - this._removalsHead = null; } - } - // Add the record or a given key to the list of changes only when the value has actually changed - _maybeAddToChanges(record, newValue) { - if (!Object.is(newValue, record.currentValue)) { - record.previousValue = record.currentValue; - record.currentValue = newValue; - this._addToChanges(record); + // Analyzing exports + for (const exported of maybeUnwrapFn(def.exports)) { + if (isNgModule(exported)) { + // When this module exports another, the exported module's exported directives and pipes + // are added to both the compilation and exported scopes of this module. + const exportedScope = this.getNgModuleScope(exported); + // Based on the current logic there is no way to have poisoned exported scope. So no need to + // check for it. + addSet(exportedScope.exported.directives, scope.exported.directives); + addSet(exportedScope.exported.pipes, scope.exported.pipes); + // Some test toolings which run in JIT mode depend on this behavior that the exported scope + // should also be present in the compilation scope, even though AoT does not support this + // and it is also in odds with NgModule metadata definitions. Without this some tests in + // Google will fail. + addSet(exportedScope.exported.directives, scope.compilation.directives); + addSet(exportedScope.exported.pipes, scope.compilation.pipes); + } else if (isPipe(exported)) { + scope.exported.pipes.add(exported); + } else { + scope.exported.directives.add(exported); + } } + return scope; } - _addToAdditions(record) { - if (this._additionsHead === null) { - this._additionsHead = this._additionsTail = record; - } else { - this._additionsTail._nextAdded = record; - this._additionsTail = record; + /** @override */ + getStandaloneComponentScope(type, rawImports) { + if (this.standaloneComponentsScopeCache.has(type)) { + return this.standaloneComponentsScopeCache.get(type); } + const ans = this.computeStandaloneComponentScope(type, rawImports); + this.standaloneComponentsScopeCache.set(type, ans); + return ans; } - _addToChanges(record) { - if (this._changesHead === null) { - this._changesHead = this._changesTail = record; - } else { - this._changesTail._nextChanged = record; - this._changesTail = record; + computeStandaloneComponentScope(type, rawImports) { + const ans = { + compilation: { + // Standalone components are always able to self-reference. + directives: new Set([type]), + pipes: new Set(), + ngModules: new Set() + } + }; + for (const rawImport of flatten(rawImports ?? [])) { + const imported = resolveForwardRef(rawImport); + try { + verifyStandaloneImport(imported, type); + } catch (e) { + // Short-circuit if an import is not valid + ans.compilation.isPoisoned = true; + return ans; + } + if (isNgModule(imported)) { + ans.compilation.ngModules.add(imported); + const importedScope = this.getNgModuleScope(imported); + // Short-circuit if an imported NgModule has corrupted exported scope. + if (importedScope.exported.isPoisoned) { + ans.compilation.isPoisoned = true; + return ans; + } + addSet(importedScope.exported.directives, ans.compilation.directives); + addSet(importedScope.exported.pipes, ans.compilation.pipes); + } else if (isPipe(imported)) { + ans.compilation.pipes.add(imported); + } else if (isDirective(imported) || isComponent(imported)) { + ans.compilation.directives.add(imported); + } else { + // The imported thing is not module/pipe/directive/component, so we error and short-circuit + // here + ans.compilation.isPoisoned = true; + return ans; + } } + return ans; } - /** @internal */ - _forEach(obj, fn) { - if (obj instanceof Map) { - obj.forEach(fn); - } else { - Object.keys(obj).forEach(k => fn(obj[k], k)); + /** @override */ + isOrphanComponent(cmp) { + const def = getComponentDef(cmp); + if (!def || def.standalone) { + return false; } + this.resolveNgModulesDecls(); + return !this.ownerNgModule.has(cmp); } } -class KeyValueChangeRecord_ { - constructor(key) { - this.key = key; - this.previousValue = null; - this.currentValue = null; - /** @internal */ - this._nextPrevious = null; - /** @internal */ - this._next = null; - /** @internal */ - this._prev = null; - /** @internal */ - this._nextAdded = null; - /** @internal */ - this._nextRemoved = null; - /** @internal */ - this._nextChanged = null; +function addSet(sourceSet, targetSet) { + for (const m of sourceSet) { + targetSet.add(m); } } -function defaultIterableDiffersFactory() { - return new IterableDiffers([new DefaultIterableDifferFactory()]); -} +/** The deps tracker to be used in the current Angular app in dev mode. */ +const depsTracker = /*#__PURE__*/new DepsTracker(); +const TEST_ONLY = { + DepsTracker +}; + /** - * A repository of different iterable diffing strategies used by NgFor, NgClass, and others. + * Compute the static styling (class/style) from `TAttributes`. * - * @publicApi + * This function should be called during `firstCreatePass` only. + * + * @param tNode The `TNode` into which the styling information should be loaded. + * @param attrs `TAttributes` containing the styling information. + * @param writeToHost Where should the resulting static styles be written? + * - `false` Write to `TNode.stylesWithoutHost` / `TNode.classesWithoutHost` + * - `true` Write to `TNode.styles` / `TNode.classes` */ -let IterableDiffers = /*#__PURE__*/(() => { - class IterableDiffers { - /** @nocollapse */ - static #_ = this.ɵprov = ɵɵdefineInjectable({ - token: IterableDiffers, - providedIn: 'root', - factory: defaultIterableDiffersFactory - }); - constructor(factories) { - this.factories = factories; - } - static create(factories, parent) { - if (parent != null) { - const copied = parent.factories.slice(); - factories = factories.concat(copied); - } - return new IterableDiffers(factories); - } - /** - * Takes an array of {@link IterableDifferFactory} and returns a provider used to extend the - * inherited {@link IterableDiffers} instance with the provided factories and return a new - * {@link IterableDiffers} instance. - * - * @usageNotes - * ### Example - * - * The following example shows how to extend an existing list of factories, - * which will only be applied to the injector for this component and its children. - * This step is all that's required to make a new {@link IterableDiffer} available. - * - * ``` - * @Component({ - * viewProviders: [ - * IterableDiffers.extend([new ImmutableListDiffer()]) - * ] - * }) - * ``` - */ - static extend(factories) { - return { - provide: IterableDiffers, - useFactory: parent => { - // if parent is null, it means that we are in the root injector and we have just overridden - // the default injection mechanism for IterableDiffers, in such a case just assume - // `defaultIterableDiffersFactory`. - return IterableDiffers.create(factories, parent || defaultIterableDiffersFactory()); - }, - // Dependency technically isn't optional, but we can provide a better error message this way. - deps: [[IterableDiffers, new SkipSelf(), new Optional()]] - }; - } - find(iterable) { - const factory = this.factories.find(f => f.supports(iterable)); - if (factory != null) { - return factory; - } else { - throw new RuntimeError(901 /* RuntimeErrorCode.NO_SUPPORTING_DIFFER_FACTORY */, ngDevMode && `Cannot find a differ supporting object '${iterable}' of type '${getTypeNameForDebugging(iterable)}'`); +function computeStaticStyling(tNode, attrs, writeToHost) { + ngDevMode && assertFirstCreatePass(getTView(), 'Expecting to be called in first template pass only'); + let styles = writeToHost ? tNode.styles : null; + let classes = writeToHost ? tNode.classes : null; + let mode = 0; + if (attrs !== null) { + for (let i = 0; i < attrs.length; i++) { + const value = attrs[i]; + if (typeof value === 'number') { + mode = value; + } else if (mode == 1 /* AttributeMarker.Classes */) { + classes = concatStringsWithSpace(classes, value); + } else if (mode == 2 /* AttributeMarker.Styles */) { + const style = value; + const styleValue = attrs[++i]; + styles = concatStringsWithSpace(styles, style + ': ' + styleValue + ';'); } } } - return IterableDiffers; -})(); -function getTypeNameForDebugging(type) { - return type['name'] || typeof type; + writeToHost ? tNode.styles = styles : tNode.stylesWithoutHost = styles; + writeToHost ? tNode.classes = classes : tNode.classesWithoutHost = classes; } -function defaultKeyValueDiffersFactory() { - return new KeyValueDiffers([new DefaultKeyValueDifferFactory()]); +class ComponentFactoryResolver extends ComponentFactoryResolver$1 { + /** + * @param ngModule The NgModuleRef to which all resolved factories are bound. + */ + constructor(ngModule) { + super(); + this.ngModule = ngModule; + } + resolveComponentFactory(component) { + ngDevMode && assertComponentType(component); + const componentDef = getComponentDef(component); + return new ComponentFactory(componentDef, this.ngModule); + } } -/** - * A repository of different Map diffing strategies used by NgClass, NgStyle, and others. - * - * @publicApi - */ -let KeyValueDiffers = /*#__PURE__*/(() => { - class KeyValueDiffers { - /** @nocollapse */ - static #_ = this.ɵprov = ɵɵdefineInjectable({ - token: KeyValueDiffers, - providedIn: 'root', - factory: defaultKeyValueDiffersFactory +function toRefArray(map) { + const array = []; + for (const publicName in map) { + if (!map.hasOwnProperty(publicName)) { + continue; + } + const value = map[publicName]; + if (value === undefined) { + continue; + } + array.push({ + propName: Array.isArray(value) ? value[0] : value, + templateName: publicName }); - constructor(factories) { - this.factories = factories; + } + return array; +} +function getNamespace(elementName) { + const name = elementName.toLowerCase(); + return name === 'svg' ? SVG_NAMESPACE : name === 'math' ? MATH_ML_NAMESPACE : null; +} +/** + * Injector that looks up a value using a specific injector, before falling back to the module + * injector. Used primarily when creating components or embedded views dynamically. + */ +class ChainedInjector { + constructor(injector, parentInjector) { + this.injector = injector; + this.parentInjector = parentInjector; + } + get(token, notFoundValue, flags) { + flags = convertToBitFlags(flags); + const value = this.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR, flags); + if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR || notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) { + // Return the value from the root element injector when + // - it provides it + // (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) + // - the module injector should not be checked + // (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) + return value; } - static create(factories, parent) { - if (parent) { - const copied = parent.factories.slice(); - factories = factories.concat(copied); + return this.parentInjector.get(token, notFoundValue, flags); + } +} +/** + * ComponentFactory interface implementation. + */ +class ComponentFactory extends ComponentFactory$1 { + get inputs() { + const componentDef = this.componentDef; + const inputTransforms = componentDef.inputTransforms; + const refArray = toRefArray(componentDef.inputs); + if (inputTransforms !== null) { + for (const input of refArray) { + if (inputTransforms.hasOwnProperty(input.propName)) { + input.transform = inputTransforms[input.propName]; + } } - return new KeyValueDiffers(factories); } - /** - * Takes an array of {@link KeyValueDifferFactory} and returns a provider used to extend the - * inherited {@link KeyValueDiffers} instance with the provided factories and return a new - * {@link KeyValueDiffers} instance. - * - * @usageNotes - * ### Example - * - * The following example shows how to extend an existing list of factories, - * which will only be applied to the injector for this component and its children. - * This step is all that's required to make a new {@link KeyValueDiffer} available. - * - * ``` - * @Component({ - * viewProviders: [ - * KeyValueDiffers.extend([new ImmutableMapDiffer()]) - * ] - * }) - * ``` - */ - static extend(factories) { - return { - provide: KeyValueDiffers, - useFactory: parent => { - // if parent is null, it means that we are in the root injector and we have just overridden - // the default injection mechanism for KeyValueDiffers, in such a case just assume - // `defaultKeyValueDiffersFactory`. - return KeyValueDiffers.create(factories, parent || defaultKeyValueDiffersFactory()); - }, - // Dependency technically isn't optional, but we can provide a better error message this way. - deps: [[KeyValueDiffers, new SkipSelf(), new Optional()]] + return refArray; + } + get outputs() { + return toRefArray(this.componentDef.outputs); + } + /** + * @param componentDef The component definition. + * @param ngModule The NgModuleRef to which the factory is bound. + */ + constructor(componentDef, ngModule) { + super(); + this.componentDef = componentDef; + this.ngModule = ngModule; + this.componentType = componentDef.type; + this.selector = stringifyCSSSelectorList(componentDef.selectors); + this.ngContentSelectors = componentDef.ngContentSelectors ? componentDef.ngContentSelectors : []; + this.isBoundToModule = !!ngModule; + } + create(injector, projectableNodes, rootSelectorOrNode, environmentInjector) { + const prevConsumer = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(null); + try { + // Check if the component is orphan + if (ngDevMode && (typeof ngJitMode === 'undefined' || ngJitMode) && this.componentDef.debugInfo?.forbidOrphanRendering) { + if (depsTracker.isOrphanComponent(this.componentType)) { + throw new RuntimeError(1001 /* RuntimeErrorCode.RUNTIME_DEPS_ORPHAN_COMPONENT */, `Orphan component found! Trying to render the component ${debugStringifyTypeForError(this.componentType)} without first loading the NgModule that declares it. It is recommended to make this component standalone in order to avoid this error. If this is not possible now, import the component's NgModule in the appropriate NgModule, or the standalone component in which you are trying to render this component. If this is a lazy import, load the NgModule lazily as well and use its module injector.`); + } + } + environmentInjector = environmentInjector || this.ngModule; + let realEnvironmentInjector = environmentInjector instanceof EnvironmentInjector ? environmentInjector : environmentInjector?.injector; + if (realEnvironmentInjector && this.componentDef.getStandaloneInjector !== null) { + realEnvironmentInjector = this.componentDef.getStandaloneInjector(realEnvironmentInjector) || realEnvironmentInjector; + } + const rootViewInjector = realEnvironmentInjector ? new ChainedInjector(injector, realEnvironmentInjector) : injector; + const rendererFactory = rootViewInjector.get(RendererFactory2, null); + if (rendererFactory === null) { + throw new RuntimeError(407 /* RuntimeErrorCode.RENDERER_NOT_FOUND */, ngDevMode && 'Angular was not able to inject a renderer (RendererFactory2). ' + 'Likely this is due to a broken DI hierarchy. ' + 'Make sure that any injector used to create this component has a correct parent.'); + } + const sanitizer = rootViewInjector.get(Sanitizer, null); + const afterRenderEventManager = rootViewInjector.get(AfterRenderEventManager, null); + const changeDetectionScheduler = rootViewInjector.get(ChangeDetectionScheduler, null); + const environment = { + rendererFactory, + sanitizer, + // We don't use inline effects (yet). + inlineEffectRunner: null, + afterRenderEventManager, + changeDetectionScheduler }; - } - find(kv) { - const factory = this.factories.find(f => f.supports(kv)); - if (factory) { - return factory; + const hostRenderer = rendererFactory.createRenderer(null, this.componentDef); + // Determine a tag name used for creating host elements when this component is created + // dynamically. Default to 'div' if this component did not specify any tag name in its + // selector. + const elementName = this.componentDef.selectors[0][0] || 'div'; + const hostRNode = rootSelectorOrNode ? locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation, rootViewInjector) : createElementNode(hostRenderer, elementName, getNamespace(elementName)); + let rootFlags = 512 /* LViewFlags.IsRoot */; + if (this.componentDef.signals) { + rootFlags |= 4096 /* LViewFlags.SignalView */; + } else if (!this.componentDef.onPush) { + rootFlags |= 16 /* LViewFlags.CheckAlways */; + } + let hydrationInfo = null; + if (hostRNode !== null) { + hydrationInfo = retrieveHydrationInfo(hostRNode, rootViewInjector, true /* isRootView */); + } + // Create the root view. Uses empty TView and ContentTemplate. + const rootTView = createTView(0 /* TViewType.Root */, null, null, 1, 0, null, null, null, null, null, null); + const rootLView = createLView(null, rootTView, null, rootFlags, null, null, environment, hostRenderer, rootViewInjector, null, hydrationInfo); + // rootView is the parent when bootstrapping + // TODO(misko): it looks like we are entering view here but we don't really need to as + // `renderView` does that. However as the code is written it is needed because + // `createRootComponentView` and `createRootComponent` both read global state. Fixing those + // issues would allow us to drop this. + enterView(rootLView); + let component; + let tElementNode; + try { + const rootComponentDef = this.componentDef; + let rootDirectives; + let hostDirectiveDefs = null; + if (rootComponentDef.findHostDirectiveDefs) { + rootDirectives = []; + hostDirectiveDefs = new Map(); + rootComponentDef.findHostDirectiveDefs(rootComponentDef, rootDirectives, hostDirectiveDefs); + rootDirectives.push(rootComponentDef); + ngDevMode && assertNoDuplicateDirectives(rootDirectives); + } else { + rootDirectives = [rootComponentDef]; + } + const hostTNode = createRootComponentTNode(rootLView, hostRNode); + const componentView = createRootComponentView(hostTNode, hostRNode, rootComponentDef, rootDirectives, rootLView, environment, hostRenderer); + tElementNode = getTNode(rootTView, HEADER_OFFSET); + // TODO(crisbeto): in practice `hostRNode` should always be defined, but there are some + // tests where the renderer is mocked out and `undefined` is returned. We should update the + // tests so that this check can be removed. + if (hostRNode) { + setRootNodeAttributes(hostRenderer, rootComponentDef, hostRNode, rootSelectorOrNode); + } + if (projectableNodes !== undefined) { + projectNodes(tElementNode, this.ngContentSelectors, projectableNodes); + } + // TODO: should LifecycleHooksFeature and other host features be generated by the compiler + // and executed here? Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref + component = createRootComponent(componentView, rootComponentDef, rootDirectives, hostDirectiveDefs, rootLView, [LifecycleHooksFeature]); + renderView(rootTView, rootLView, null); + } finally { + leaveView(); } - throw new RuntimeError(901 /* RuntimeErrorCode.NO_SUPPORTING_DIFFER_FACTORY */, ngDevMode && `Cannot find a differ supporting object '${kv}'`); + return new ComponentRef(this.componentType, component, createElementRef(tElementNode, rootLView), rootLView, tElementNode); + } finally { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.setActiveConsumer)(prevConsumer); } } - return KeyValueDiffers; -})(); -function devModeEqual(a, b) { - const isListLikeIterableA = isListLikeIterable(a); - const isListLikeIterableB = isListLikeIterable(b); - if (isListLikeIterableA && isListLikeIterableB) { - return areIterablesEqual(a, b, devModeEqual); - } else { - const isAObject = a && (typeof a === 'object' || typeof a === 'function'); - const isBObject = b && (typeof b === 'object' || typeof b === 'function'); - if (!isListLikeIterableA && isAObject && !isListLikeIterableB && isBObject) { - return true; +} +/** + * Represents an instance of a Component created via a {@link ComponentFactory}. + * + * `ComponentRef` provides access to the Component Instance as well other objects related to this + * Component Instance and allows you to destroy the Component Instance via the {@link #destroy} + * method. + * + */ +class ComponentRef extends ComponentRef$1 { + constructor(componentType, instance, location, _rootLView, _tNode) { + super(); + this.location = location; + this._rootLView = _rootLView; + this._tNode = _tNode; + this.previousInputValues = null; + this.instance = instance; + this.hostView = this.changeDetectorRef = new ViewRef$1(_rootLView, undefined, /* _cdRefInjectingView */false); + this.componentType = componentType; + } + setInput(name, value) { + const inputData = this._tNode.inputs; + let dataValue; + if (inputData !== null && (dataValue = inputData[name])) { + this.previousInputValues ??= new Map(); + // Do not set the input if it is the same as the last value + // This behavior matches `bindingUpdated` when binding inputs in templates. + if (this.previousInputValues.has(name) && Object.is(this.previousInputValues.get(name), value)) { + return; + } + const lView = this._rootLView; + setInputsForProperty(lView[TVIEW], lView, dataValue, name, value); + this.previousInputValues.set(name, value); + const childComponentLView = getComponentLViewByIndex(this._tNode.index, lView); + markViewDirty(childComponentLView); } else { - return Object.is(a, b); + if (ngDevMode) { + const cmpNameForError = stringifyForError(this.componentType); + let message = `Can't set value of the '${name}' input on the '${cmpNameForError}' component. `; + message += `Make sure that the '${name}' property is annotated with @Input() or a mapped @Input('${name}') exists.`; + reportUnknownPropertyError(message); + } } } + get injector() { + return new NodeInjector(this._tNode, this._rootLView); + } + destroy() { + this.hostView.destroy(); + } + onDestroy(callback) { + this.hostView.onDestroy(callback); + } +} +/** Creates a TNode that can be used to instantiate a root component. */ +function createRootComponentTNode(lView, rNode) { + const tView = lView[TVIEW]; + const index = HEADER_OFFSET; + ngDevMode && assertIndexInRange(lView, index); + lView[index] = rNode; + // '#host' is added here as we don't know the real host DOM name (we don't want to read it) and at + // the same time we want to communicate the debug `TNode` that this is a special `TNode` + // representing a host element. + return getOrCreateTNode(tView, index, 2 /* TNodeType.Element */, '#host', null); } - /** - * Base class that provides change detection functionality. - * A change-detection tree collects all views that are to be checked for changes. - * Use the methods to add and remove views from the tree, initiate change-detection, - * and explicitly mark views as _dirty_, meaning that they have changed and need to be re-rendered. + * Creates the root component view and the root component node. * - * @see [Using change detection hooks](guide/lifecycle-hooks#using-change-detection-hooks) - * @see [Defining custom change detection](guide/lifecycle-hooks#defining-custom-change-detection) + * @param hostRNode Render host element. + * @param rootComponentDef ComponentDef + * @param rootView The parent view where the host node is stored + * @param rendererFactory Factory to be used for creating child renderers. + * @param hostRenderer The current renderer + * @param sanitizer The sanitizer, if provided * - * @usageNotes + * @returns Component view created + */ +function createRootComponentView(tNode, hostRNode, rootComponentDef, rootDirectives, rootView, environment, hostRenderer) { + const tView = rootView[TVIEW]; + applyRootComponentStyling(rootDirectives, tNode, hostRNode, hostRenderer); + // Hydration info is on the host element and needs to be retrieved + // and passed to the component LView. + let hydrationInfo = null; + if (hostRNode !== null) { + hydrationInfo = retrieveHydrationInfo(hostRNode, rootView[INJECTOR$1]); + } + const viewRenderer = environment.rendererFactory.createRenderer(hostRNode, rootComponentDef); + let lViewFlags = 16 /* LViewFlags.CheckAlways */; + if (rootComponentDef.signals) { + lViewFlags = 4096 /* LViewFlags.SignalView */; + } else if (rootComponentDef.onPush) { + lViewFlags = 64 /* LViewFlags.Dirty */; + } + const componentView = createLView(rootView, getOrCreateComponentTView(rootComponentDef), null, lViewFlags, rootView[tNode.index], tNode, environment, viewRenderer, null, null, hydrationInfo); + if (tView.firstCreatePass) { + markAsComponentHost(tView, tNode, rootDirectives.length - 1); + } + addToViewTree(rootView, componentView); + // Store component view at node index, with node as the HOST + return rootView[tNode.index] = componentView; +} +/** Sets up the styling information on a root component. */ +function applyRootComponentStyling(rootDirectives, tNode, rNode, hostRenderer) { + for (const def of rootDirectives) { + tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs); + } + if (tNode.mergedAttrs !== null) { + computeStaticStyling(tNode, tNode.mergedAttrs, true); + if (rNode !== null) { + setupStaticAttributes(hostRenderer, rNode, tNode); + } + } +} +/** + * Creates a root component and sets it up with features and host bindings.Shared by + * renderComponent() and ViewContainerRef.createComponent(). + */ +function createRootComponent(componentView, rootComponentDef, rootDirectives, hostDirectiveDefs, rootLView, hostFeatures) { + const rootTNode = getCurrentTNode(); + ngDevMode && assertDefined(rootTNode, 'tNode should have been already created'); + const tView = rootLView[TVIEW]; + const native = getNativeByTNode(rootTNode, rootLView); + initializeDirectives(tView, rootLView, rootTNode, rootDirectives, null, hostDirectiveDefs); + for (let i = 0; i < rootDirectives.length; i++) { + const directiveIndex = rootTNode.directiveStart + i; + const directiveInstance = getNodeInjectable(rootLView, tView, directiveIndex, rootTNode); + attachPatchData(directiveInstance, rootLView); + } + invokeDirectivesHostBindings(tView, rootLView, rootTNode); + if (native) { + attachPatchData(native, rootLView); + } + // We're guaranteed for the `componentOffset` to be positive here + // since a root component always matches a component def. + ngDevMode && assertGreaterThan(rootTNode.componentOffset, -1, 'componentOffset must be great than -1'); + const component = getNodeInjectable(rootLView, tView, rootTNode.directiveStart + rootTNode.componentOffset, rootTNode); + componentView[CONTEXT] = rootLView[CONTEXT] = component; + if (hostFeatures !== null) { + for (const feature of hostFeatures) { + feature(component, rootComponentDef); + } + } + // We want to generate an empty QueryList for root content queries for backwards + // compatibility with ViewEngine. + executeContentQueries(tView, rootTNode, rootLView); + return component; +} +/** Sets the static attributes on a root component. */ +function setRootNodeAttributes(hostRenderer, componentDef, hostRNode, rootSelectorOrNode) { + if (rootSelectorOrNode) { + // The placeholder will be replaced with the actual version at build time. + setUpAttributes(hostRenderer, hostRNode, ['ng-version', '17.2.4']); + } else { + // If host element is created as a part of this function call (i.e. `rootSelectorOrNode` + // is not defined), also apply attributes and classes extracted from component selector. + // Extract attributes and classes from the first selector only to match VE behavior. + const { + attrs, + classes + } = extractAttrsAndClassesFromSelector(componentDef.selectors[0]); + if (attrs) { + setUpAttributes(hostRenderer, hostRNode, attrs); + } + if (classes && classes.length > 0) { + writeDirectClass(hostRenderer, hostRNode, classes.join(' ')); + } + } +} +/** Projects the `projectableNodes` that were specified when creating a root component. */ +function projectNodes(tNode, ngContentSelectors, projectableNodes) { + const projection = tNode.projection = []; + for (let i = 0; i < ngContentSelectors.length; i++) { + const nodesforSlot = projectableNodes[i]; + // Projectable nodes can be passed as array of arrays or an array of iterables (ngUpgrade + // case). Here we do normalize passed data structure to be an array of arrays to avoid + // complex checks down the line. + // We also normalize the length of the passed in projectable nodes (to match the number of + // slots defined by a component). + projection.push(nodesforSlot != null ? Array.from(nodesforSlot) : null); + } +} +/** + * Used to enable lifecycle hooks on the root component. * - * The following examples demonstrate how to modify default change-detection behavior - * to perform explicit detection when needed. + * Include this feature when calling `renderComponent` if the root component + * you are rendering has lifecycle hooks defined. Otherwise, the hooks won't + * be called properly. * - * ### Use `markForCheck()` with `CheckOnce` strategy + * Example: * - * The following example sets the `OnPush` change-detection strategy for a component - * (`CheckOnce`, rather than the default `CheckAlways`), then forces a second check - * after an interval. + * ``` + * renderComponent(AppComponent, {hostFeatures: [LifecycleHooksFeature]}); + * ``` + */ +function LifecycleHooksFeature() { + const tNode = getCurrentTNode(); + ngDevMode && assertDefined(tNode, 'TNode is required'); + registerPostOrderHooks(getLView()[TVIEW], tNode); +} + +/** + * Represents a container where one or more views can be attached to a component. * - * + * Can contain *host views* (created by instantiating a + * component with the `createComponent()` method), and *embedded views* + * (created by instantiating a `TemplateRef` with the `createEmbeddedView()` method). * - * ### Detach change detector to limit how often check occurs + * A view container instance can contain other view containers, + * creating a [view hierarchy](guide/glossary#view-hierarchy). * - * The following example defines a component with a large list of read-only data - * that is expected to change constantly, many times per second. - * To improve performance, we want to check and update the list - * less often than the changes actually occur. To do that, we detach - * the component's change detector and perform an explicit local check every five seconds. + * @usageNotes * - * + * The example below demonstrates how the `createComponent` function can be used + * to create an instance of a ComponentRef dynamically and attach it to an ApplicationRef, + * so that it gets included into change detection cycles. * + * Note: the example uses standalone components, but the function can also be used for + * non-standalone components (declared in an NgModule) as well. * - * ### Reattaching a detached component + * ```typescript + * @Component({ + * standalone: true, + * selector: 'dynamic', + * template: `This is a content of a dynamic component.`, + * }) + * class DynamicComponent { + * vcr = inject(ViewContainerRef); + * } * - * The following example creates a component displaying live data. - * The component detaches its change detector from the main change detector tree - * when the `live` property is set to false, and reattaches it when the property - * becomes true. + * @Component({ + * standalone: true, + * selector: 'app', + * template: `
Hi! This is the main content.
`, + * }) + * class AppComponent { + * vcr = inject(ViewContainerRef); * - * + * ngAfterViewInit() { + * const compRef = this.vcr.createComponent(DynamicComponent); + * compRef.changeDetectorRef.detectChanges(); + * } + * } + * ``` + * + * @see {@link ComponentRef} + * @see {@link EmbeddedViewRef} * * @publicApi */ -let ChangeDetectorRef = /*#__PURE__*/(() => { - class ChangeDetectorRef { +let ViewContainerRef = /*#__PURE__*/(() => { + class ViewContainerRef { /** * @internal * @nocollapse */ - static #_ = this.__NG_ELEMENT_ID__ = injectChangeDetectorRef; + static #_ = this.__NG_ELEMENT_ID__ = injectViewContainerRef; } - return ChangeDetectorRef; + return ViewContainerRef; })(); -/** Returns a ChangeDetectorRef (a.k.a. a ViewRef) */ -function injectChangeDetectorRef(flags) { - return createViewRef(getCurrentTNode(), getLView(), (flags & 16 /* InternalInjectFlags.ForPipe */) === 16 /* InternalInjectFlags.ForPipe */); -} /** - * Creates a ViewRef and stores it on the injector as ChangeDetectorRef (public alias). + * Creates a ViewContainerRef and stores it on the injector. Or, if the ViewContainerRef + * already exists, retrieves the existing ViewContainerRef. * - * @param tNode The node that is requesting a ChangeDetectorRef - * @param lView The view to which the node belongs - * @param isPipe Whether the view is being injected into a pipe. - * @returns The ChangeDetectorRef to use + * @returns The ViewContainerRef instance to use */ -function createViewRef(tNode, lView, isPipe) { - if (isComponentHost(tNode) && !isPipe) { - // The LView represents the location where the component is declared. - // Instead we want the LView for the component View and so we need to look it up. - const componentView = getComponentLViewByIndex(tNode.index, lView); // look down - return new ViewRef$1(componentView, componentView); - } else if (tNode.type & (3 /* TNodeType.AnyRNode */ | 12 /* TNodeType.AnyContainer */ | 32 /* TNodeType.Icu */)) { - // The LView represents the location where the injection is requested from. - // We need to locate the containing LView (in case where the `lView` is an embedded view) - const hostComponentView = lView[DECLARATION_COMPONENT_VIEW]; // look up - return new ViewRef$1(hostComponentView, lView); - } - return null; +function injectViewContainerRef() { + const previousTNode = getCurrentTNode(); + return createContainerRef(previousTNode, getLView()); } - -/** - * Structural diffing for `Object`s and `Map`s. - */ -const keyValDiff = [/*#__PURE__*/new DefaultKeyValueDifferFactory()]; -/** - * Structural diffing for `Iterable` types such as `Array`s. - */ -const iterableDiff = [/*#__PURE__*/new DefaultIterableDifferFactory()]; -const defaultIterableDiffers = /*#__PURE__*/new IterableDiffers(iterableDiff); -const defaultKeyValueDiffers = /*#__PURE__*/new KeyValueDiffers(keyValDiff); - -/** - * @module - * @description - * Change detection enables data binding in Angular. - */ - -/** - * `DestroyRef` lets you set callbacks to run for any cleanup or destruction behavior. - * The scope of this destruction depends on where `DestroyRef` is injected. If `DestroyRef` - * is injected in a component or directive, the callbacks run when that component or - * directive is destroyed. Otherwise the callbacks run when a corresponding injector is destroyed. - * - * @publicApi - */ -let DestroyRef = /*#__PURE__*/(() => { - class DestroyRef { - /** - * @internal - * @nocollapse - */ - static #_ = this.__NG_ELEMENT_ID__ = injectDestroyRef; - /** - * @internal - * @nocollapse - */ - static #_2 = this.__NG_ENV_ID__ = injector => injector; - } - return DestroyRef; -})(); -class NodeInjectorDestroyRef extends DestroyRef { - constructor(_lView) { +const VE_ViewContainerRef = ViewContainerRef; +// TODO(alxhub): cleaning up this indirection triggers a subtle bug in Closure in g3. Once the fix +// for that lands, this can be cleaned up. +const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef { + constructor(_lContainer, _hostTNode, _hostLView) { super(); - this._lView = _lView; + this._lContainer = _lContainer; + this._hostTNode = _hostTNode; + this._hostLView = _hostLView; } - onDestroy(callback) { - storeLViewOnDestroy(this._lView, callback); - return () => removeLViewOnDestroy(this._lView, callback); + get element() { + return createElementRef(this._hostTNode, this._hostLView); } -} -function injectDestroyRef() { - return new NodeInjectorDestroyRef(getLView()); -} - -/** - * Asserts that the current stack frame is not within a reactive context. Useful - * to disallow certain code from running inside a reactive context (see {@link toSignal}). - * - * @param debugFn a reference to the function making the assertion (used for the error message). - * - * @publicApi - */ -function assertNotInReactiveContext(debugFn, extraContext) { - // Taking a `Function` instead of a string name here prevents the un-minified name of the function - // from being retained in the bundle regardless of minification. - if ((0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.getActiveConsumer)() !== null) { - throw new RuntimeError(-602 /* RuntimeErrorCode.ASSERTION_NOT_INSIDE_REACTIVE_CONTEXT */, ngDevMode && `${debugFn.name}() cannot be called from within a reactive context.${extraContext ? ` ${extraContext}` : ''}`); + get injector() { + return new NodeInjector(this._hostTNode, this._hostLView); } -} - -/** - * Not public API, which guarantees `EffectScheduler` only ever comes from the application root - * injector. - */ -const APP_EFFECT_SCHEDULER = /*#__PURE__*/new InjectionToken('', { - providedIn: 'root', - factory: () => inject(EffectScheduler) -}); -/** - * A scheduler which manages the execution of effects. - */ -let EffectScheduler = /*#__PURE__*/(() => { - class EffectScheduler { - /** @nocollapse */ - static #_ = this.ɵprov = ɵɵdefineInjectable({ - token: EffectScheduler, - providedIn: 'root', - factory: () => new ZoneAwareEffectScheduler() - }); + /** @deprecated No replacement */ + get parentInjector() { + const parentLocation = getParentInjectorLocation(this._hostTNode, this._hostLView); + if (hasParentInjector(parentLocation)) { + const parentView = getParentInjectorView(parentLocation, this._hostLView); + const injectorIndex = getParentInjectorIndex(parentLocation); + ngDevMode && assertNodeInjector(parentView, injectorIndex); + const parentTNode = parentView[TVIEW].data[injectorIndex + 8 /* NodeInjectorOffset.TNODE */]; + return new NodeInjector(parentTNode, parentView); + } else { + return new NodeInjector(null, this._hostLView); + } } - return EffectScheduler; -})(); -/** - * A wrapper around `ZoneAwareQueueingScheduler` that schedules flushing via the microtask queue - * when. - */ -class ZoneAwareEffectScheduler { - constructor() { - this.hasQueuedFlush = false; - this.queuedEffectCount = 0; - this.queues = new Map(); + clear() { + while (this.length > 0) { + this.remove(this.length - 1); + } } - scheduleEffect(handle) { - this.enqueue(handle); - if (!this.hasQueuedFlush) { - queueMicrotask(() => this.flush()); - // Leave `hasQueuedFlush` as `true` so we don't queue another microtask if more effects are - // scheduled during flushing. We are guaranteed to empty the whole queue during flush. - this.hasQueuedFlush = false; + get(index) { + const viewRefs = getViewRefs(this._lContainer); + return viewRefs !== null && viewRefs[index] || null; + } + get length() { + return this._lContainer.length - CONTAINER_HEADER_OFFSET; + } + createEmbeddedView(templateRef, context, indexOrOptions) { + let index; + let injector; + if (typeof indexOrOptions === 'number') { + index = indexOrOptions; + } else if (indexOrOptions != null) { + index = indexOrOptions.index; + injector = indexOrOptions.injector; } + const dehydratedView = findMatchingDehydratedView(this._lContainer, templateRef.ssrId); + const viewRef = templateRef.createEmbeddedViewImpl(context || {}, injector, dehydratedView); + this.insertImpl(viewRef, index, shouldAddViewToDom(this._hostTNode, dehydratedView)); + return viewRef; } - enqueue(handle) { - const zone = handle.creationZone; - if (!this.queues.has(zone)) { - this.queues.set(zone, new Set()); + createComponent(componentFactoryOrType, indexOrOptions, injector, projectableNodes, environmentInjector) { + const isComponentFactory = componentFactoryOrType && !isType(componentFactoryOrType); + let index; + // This function supports 2 signatures and we need to handle options correctly for both: + // 1. When first argument is a Component type. This signature also requires extra + // options to be provided as object (more ergonomic option). + // 2. First argument is a Component factory. In this case extra options are represented as + // positional arguments. This signature is less ergonomic and will be deprecated. + if (isComponentFactory) { + if (ngDevMode) { + assertEqual(typeof indexOrOptions !== 'object', true, 'It looks like Component factory was provided as the first argument ' + 'and an options object as the second argument. This combination of arguments ' + 'is incompatible. You can either change the first argument to provide Component ' + 'type or change the second argument to be a number (representing an index at ' + 'which to insert the new component\'s host view into this container)'); + } + index = indexOrOptions; + } else { + if (ngDevMode) { + assertDefined(getComponentDef(componentFactoryOrType), `Provided Component class doesn't contain Component definition. ` + `Please check whether provided class has @Component decorator.`); + assertEqual(typeof indexOrOptions !== 'number', true, 'It looks like Component type was provided as the first argument ' + 'and a number (representing an index at which to insert the new component\'s ' + 'host view into this container as the second argument. This combination of arguments ' + 'is incompatible. Please use an object as the second argument instead.'); + } + const options = indexOrOptions || {}; + if (ngDevMode && options.environmentInjector && options.ngModuleRef) { + throwError(`Cannot pass both environmentInjector and ngModuleRef options to createComponent().`); + } + index = options.index; + injector = options.injector; + projectableNodes = options.projectableNodes; + environmentInjector = options.environmentInjector || options.ngModuleRef; } - const queue = this.queues.get(zone); - if (queue.has(handle)) { - return; + const componentFactory = isComponentFactory ? componentFactoryOrType : new ComponentFactory(getComponentDef(componentFactoryOrType)); + const contextInjector = injector || this.parentInjector; + // If an `NgModuleRef` is not provided explicitly, try retrieving it from the DI tree. + if (!environmentInjector && componentFactory.ngModule == null) { + // For the `ComponentFactory` case, entering this logic is very unlikely, since we expect that + // an instance of a `ComponentFactory`, resolved via `ComponentFactoryResolver` would have an + // `ngModule` field. This is possible in some test scenarios and potentially in some JIT-based + // use-cases. For the `ComponentFactory` case we preserve backwards-compatibility and try + // using a provided injector first, then fall back to the parent injector of this + // `ViewContainerRef` instance. + // + // For the factory-less case, it's critical to establish a connection with the module + // injector tree (by retrieving an instance of an `NgModuleRef` and accessing its injector), + // so that a component can use DI tokens provided in MgModules. For this reason, we can not + // rely on the provided injector, since it might be detached from the DI tree (for example, if + // it was created via `Injector.create` without specifying a parent injector, or if an + // injector is retrieved from an `NgModuleRef` created via `createNgModule` using an + // NgModule outside of a module tree). Instead, we always use `ViewContainerRef`'s parent + // injector, which is normally connected to the DI tree, which includes module injector + // subtree. + const _injector = isComponentFactory ? contextInjector : this.parentInjector; + // DO NOT REFACTOR. The code here used to have a `injector.get(NgModuleRef, null) || + // undefined` expression which seems to cause internal google apps to fail. This is documented + // in the following internal bug issue: go/b/142967802 + const result = _injector.get(EnvironmentInjector, null); + if (result) { + environmentInjector = result; + } } - this.queuedEffectCount++; - queue.add(handle); + const componentDef = getComponentDef(componentFactory.componentType ?? {}); + const dehydratedView = findMatchingDehydratedView(this._lContainer, componentDef?.id ?? null); + const rNode = dehydratedView?.firstChild ?? null; + const componentRef = componentFactory.create(contextInjector, projectableNodes, rNode, environmentInjector); + this.insertImpl(componentRef.hostView, index, shouldAddViewToDom(this._hostTNode, dehydratedView)); + return componentRef; } - /** - * Run all scheduled effects. - * - * Execution order of effects within the same zone is guaranteed to be FIFO, but there is no - * ordering guarantee between effects scheduled in different zones. - */ - flush() { - while (this.queuedEffectCount > 0) { - for (const [zone, queue] of this.queues) { - // `zone` here must be defined. - if (zone === null) { - this.flushQueue(queue); - } else { - zone.run(() => this.flushQueue(queue)); - } + insert(viewRef, index) { + return this.insertImpl(viewRef, index, true); + } + insertImpl(viewRef, index, addToDOM) { + const lView = viewRef._lView; + if (ngDevMode && viewRef.destroyed) { + throw new Error('Cannot insert a destroyed View in a ViewContainer!'); + } + if (viewAttachedToContainer(lView)) { + // If view is already attached, detach it first so we clean up references appropriately. + const prevIdx = this.indexOf(viewRef); + // A view might be attached either to this or a different container. The `prevIdx` for + // those cases will be: + // equal to -1 for views attached to this ViewContainerRef + // >= 0 for views attached to a different ViewContainerRef + if (prevIdx !== -1) { + this.detach(prevIdx); + } else { + const prevLContainer = lView[PARENT]; + ngDevMode && assertEqual(isLContainer(prevLContainer), true, 'An attached view should have its PARENT point to a container.'); + // We need to re-create a R3ViewContainerRef instance since those are not stored on + // LView (nor anywhere else). + const prevVCRef = new R3ViewContainerRef(prevLContainer, prevLContainer[T_HOST], prevLContainer[PARENT]); + prevVCRef.detach(prevVCRef.indexOf(viewRef)); } } + // Logical operation of adding `LView` to `LContainer` + const adjustedIdx = this._adjustIndex(index); + const lContainer = this._lContainer; + addLViewToLContainer(lContainer, lView, adjustedIdx, addToDOM); + viewRef.attachToViewContainerRef(); + addToArray(getOrCreateViewRefs(lContainer), adjustedIdx, viewRef); + return viewRef; } - flushQueue(queue) { - for (const handle of queue) { - queue.delete(handle); - this.queuedEffectCount--; - // TODO: what happens if this throws an error? - handle.run(); + move(viewRef, newIndex) { + if (ngDevMode && viewRef.destroyed) { + throw new Error('Cannot move a destroyed View in a ViewContainer!'); } + return this.insert(viewRef, newIndex); } -} -/** - * Core reactive node for an Angular effect. - * - * `EffectHandle` combines the reactive graph's `Watch` base node for effects with the framework's - * scheduling abstraction (`EffectScheduler`) as well as automatic cleanup via `DestroyRef` if - * available/requested. - */ -class EffectHandle { - constructor(scheduler, effectFn, creationZone, destroyRef, injector, allowSignalWrites) { - this.scheduler = scheduler; - this.effectFn = effectFn; - this.creationZone = creationZone; - this.injector = injector; - this.watcher = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.createWatch)(onCleanup => this.runEffect(onCleanup), () => this.schedule(), allowSignalWrites); - this.unregisterOnDestroy = destroyRef?.onDestroy(() => this.destroy()); + indexOf(viewRef) { + const viewRefsArr = getViewRefs(this._lContainer); + return viewRefsArr !== null ? viewRefsArr.indexOf(viewRef) : -1; } - runEffect(onCleanup) { - try { - this.effectFn(onCleanup); - } catch (err) { - // Inject the `ErrorHandler` here in order to avoid circular DI error - // if the effect is used inside of a custom `ErrorHandler`. - const errorHandler = this.injector.get(ErrorHandler, null, { - optional: true - }); - errorHandler?.handleError(err); + remove(index) { + const adjustedIdx = this._adjustIndex(index, -1); + const detachedView = detachView(this._lContainer, adjustedIdx); + if (detachedView) { + // Before destroying the view, remove it from the container's array of `ViewRef`s. + // This ensures the view container length is updated before calling + // `destroyLView`, which could recursively call view container methods that + // rely on an accurate container length. + // (e.g. a method on this view container being called by a child directive's OnDestroy + // lifecycle hook) + removeFromArray(getOrCreateViewRefs(this._lContainer), adjustedIdx); + destroyLView(detachedView[TVIEW], detachedView); } } - run() { - this.watcher.run(); + detach(index) { + const adjustedIdx = this._adjustIndex(index, -1); + const view = detachView(this._lContainer, adjustedIdx); + const wasDetached = view && removeFromArray(getOrCreateViewRefs(this._lContainer), adjustedIdx) != null; + return wasDetached ? new ViewRef$1(view) : null; } - schedule() { - this.scheduler.scheduleEffect(this); + _adjustIndex(index, shift = 0) { + if (index == null) { + return this.length + shift; + } + if (ngDevMode) { + assertGreaterThan(index, -1, `ViewRef index must be positive, got ${index}`); + // +1 because it's legal to insert at the end. + assertLessThan(index, this.length + 1 + shift, 'index'); + } + return index; } - destroy() { - this.watcher.destroy(); - this.unregisterOnDestroy?.(); - // Note: if the effect is currently scheduled, it's not un-scheduled, and so the scheduler will - // retain a reference to it. Attempting to execute it will be a no-op. +}; +function getViewRefs(lContainer) { + return lContainer[VIEW_REFS]; +} +function getOrCreateViewRefs(lContainer) { + return lContainer[VIEW_REFS] || (lContainer[VIEW_REFS] = []); +} +/** + * Creates a ViewContainerRef and stores it on the injector. + * + * @param hostTNode The node that is requesting a ViewContainerRef + * @param hostLView The view to which the node belongs + * @returns The ViewContainerRef instance to use + */ +function createContainerRef(hostTNode, hostLView) { + ngDevMode && assertTNodeType(hostTNode, 12 /* TNodeType.AnyContainer */ | 3 /* TNodeType.AnyRNode */); + let lContainer; + const slotValue = hostLView[hostTNode.index]; + if (isLContainer(slotValue)) { + // If the host is a container, we don't need to create a new LContainer + lContainer = slotValue; + } else { + // An LContainer anchor can not be `null`, but we set it here temporarily + // and update to the actual value later in this function (see + // `_locateOrCreateAnchorNode`). + lContainer = createLContainer(slotValue, hostLView, null, hostTNode); + hostLView[hostTNode.index] = lContainer; + addToViewTree(hostLView, lContainer); } + _locateOrCreateAnchorNode(lContainer, hostLView, hostTNode, slotValue); + return new R3ViewContainerRef(lContainer, hostTNode, hostLView); } /** - * Create a global `Effect` for the given reactive function. + * Creates and inserts a comment node that acts as an anchor for a view container. * - * @developerPreview + * If the host is a regular element, we have to insert a comment node manually which will + * be used as an anchor when inserting elements. In this specific case we use low-level DOM + * manipulation to insert it. */ -function effect(effectFn, options) { - performanceMarkFeature('NgSignals'); - ngDevMode && assertNotInReactiveContext(effect, 'Call `effect` outside of a reactive context. For example, schedule the ' + 'effect inside the component constructor.'); - !options?.injector && assertInInjectionContext(effect); - const injector = options?.injector ?? inject(Injector); - const destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null; - const handle = new EffectHandle(injector.get(APP_EFFECT_SCHEDULER), effectFn, typeof Zone === 'undefined' ? null : Zone.current, destroyRef, injector, options?.allowSignalWrites ?? false); - // Effects need to be marked dirty manually to trigger their initial run. The timing of this - // marking matters, because the effects may read signals that track component inputs, which are - // only available after those components have had their first update pass. - // - // We inject `ChangeDetectorRef` optionally, to determine whether this effect is being created in - // the context of a component or not. If it is, then we check whether the component has already - // run its update pass, and defer the effect's initial scheduling until the update pass if it - // hasn't already run. - const cdr = injector.get(ChangeDetectorRef, null, { - optional: true - }); - if (!cdr || !(cdr._lView[FLAGS] & 8 /* LViewFlags.FirstLViewPass */)) { - // This effect is either not running in a view injector, or the view has already - // undergone its first change detection pass, which is necessary for any required inputs to be - // set. - handle.watcher.notify(); +function insertAnchorNode(hostLView, hostTNode) { + const renderer = hostLView[RENDERER]; + ngDevMode && ngDevMode.rendererCreateComment++; + const commentNode = renderer.createComment(ngDevMode ? 'container' : ''); + const hostNative = getNativeByTNode(hostTNode, hostLView); + const parentOfHostNative = nativeParentNode(renderer, hostNative); + nativeInsertBefore(renderer, parentOfHostNative, commentNode, nativeNextSibling(renderer, hostNative), false); + return commentNode; +} +let _locateOrCreateAnchorNode = createAnchorNode; +let _populateDehydratedViewsInLContainer = () => false; // noop by default +/** + * Looks up dehydrated views that belong to a given LContainer and populates + * this information into the `LContainer[DEHYDRATED_VIEWS]` slot. When running + * in client-only mode, this function is a noop. + * + * @param lContainer LContainer that should be populated. + * @param tNode Corresponding TNode. + * @param hostLView LView that hosts LContainer. + * @returns a boolean flag that indicates whether a populating operation + * was successful. The operation might be unsuccessful in case is has completed + * previously, we are rendering in client-only mode or this content is located + * in a skip hydration section. + */ +function populateDehydratedViewsInLContainer(lContainer, tNode, hostLView) { + return _populateDehydratedViewsInLContainer(lContainer, tNode, hostLView); +} +/** + * Regular creation mode: an anchor is created and + * assigned to the `lContainer[NATIVE]` slot. + */ +function createAnchorNode(lContainer, hostLView, hostTNode, slotValue) { + // We already have a native element (anchor) set, return. + if (lContainer[NATIVE]) return; + let commentNode; + // If the host is an element container, the native host element is guaranteed to be a + // comment and we can reuse that comment as anchor element for the new LContainer. + // The comment node in question is already part of the DOM structure so we don't need to append + // it again. + if (hostTNode.type & 8 /* TNodeType.ElementContainer */) { + commentNode = unwrapRNode(slotValue); } else { - // Delay the initialization of the effect until the view is fully initialized. - (cdr._lView[EFFECTS_TO_SCHEDULE] ??= []).push(handle.watcher.notify); + commentNode = insertAnchorNode(hostLView, hostTNode); } - return handle; + lContainer[NATIVE] = commentNode; } - -// clang-format off -// clang-format on - -function noop(...args) { - // Do nothing. +/** + * Hydration logic that looks up all dehydrated views in this container + * and puts them into `lContainer[DEHYDRATED_VIEWS]` slot. + * + * @returns a boolean flag that indicates whether a populating operation + * was successful. The operation might be unsuccessful in case is has completed + * previously, we are rendering in client-only mode or this content is located + * in a skip hydration section. + */ +function populateDehydratedViewsInLContainerImpl(lContainer, tNode, hostLView) { + // We already have a native element (anchor) set and the process + // of finding dehydrated views happened (so the `lContainer[DEHYDRATED_VIEWS]` + // is not null), exit early. + if (lContainer[NATIVE] && lContainer[DEHYDRATED_VIEWS]) { + return true; + } + const hydrationInfo = hostLView[HYDRATION]; + const noOffsetIndex = tNode.index - HEADER_OFFSET; + const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock(tNode) || isDisconnectedNode$1(hydrationInfo, noOffsetIndex); + // Regular creation mode. + if (isNodeCreationMode) { + return false; + } + // Hydration mode, looking up an anchor node and dehydrated views in DOM. + const currentRNode = getSegmentHead(hydrationInfo, noOffsetIndex); + const serializedViews = hydrationInfo.data[CONTAINERS]?.[noOffsetIndex]; + ngDevMode && assertDefined(serializedViews, 'Unexpected state: no hydration info available for a given TNode, ' + 'which represents a view container.'); + const [commentNode, dehydratedViews] = locateDehydratedViewsInContainer(currentRNode, serializedViews); + if (ngDevMode) { + validateMatchingNode(commentNode, Node.COMMENT_NODE, null, hostLView, tNode, true); + // Do not throw in case this node is already claimed (thus `false` as a second + // argument). If this container is created based on an ``, the comment + // node would be already claimed from the `template` instruction. If an element acts + // as an anchor (e.g.
), a separate comment node would be created/located, + // so we need to claim it here. + markRNodeAsClaimedByHydration(commentNode, false); + } + lContainer[NATIVE] = commentNode; + lContainer[DEHYDRATED_VIEWS] = dehydratedViews; + return true; } -function getNativeRequestAnimationFrame() { - // Note: the `getNativeRequestAnimationFrame` is used in the `NgZone` class, but we cannot use the - // `inject` function. The `NgZone` instance may be created manually, and thus the injection - // context will be unavailable. This might be enough to check whether `requestAnimationFrame` is - // available because otherwise, we'll fall back to `setTimeout`. - const isBrowser = typeof _global['requestAnimationFrame'] === 'function'; - // Note: `requestAnimationFrame` is unavailable when the code runs in the Node.js environment. We - // use `setTimeout` because no changes are required other than checking if the current platform is - // the browser. `setTimeout` is a well-established API that is available in both environments. - // `requestAnimationFrame` is used in the browser to coalesce event tasks since event tasks are - // usually executed within the same rendering frame (but this is more implementation details of - // browsers). - let nativeRequestAnimationFrame = _global[isBrowser ? 'requestAnimationFrame' : 'setTimeout']; - let nativeCancelAnimationFrame = _global[isBrowser ? 'cancelAnimationFrame' : 'clearTimeout']; - if (typeof Zone !== 'undefined' && nativeRequestAnimationFrame && nativeCancelAnimationFrame) { - // Note: zone.js sets original implementations on patched APIs behind the - // `__zone_symbol__OriginalDelegate` key (see `attachOriginToPatched`). Given the following - // example: `window.requestAnimationFrame.__zone_symbol__OriginalDelegate`; this would return an - // unpatched implementation of the `requestAnimationFrame`, which isn't intercepted by the - // Angular zone. We use the unpatched implementation to avoid another change detection when - // coalescing tasks. - const unpatchedRequestAnimationFrame = nativeRequestAnimationFrame[Zone.__symbol__('OriginalDelegate')]; - if (unpatchedRequestAnimationFrame) { - nativeRequestAnimationFrame = unpatchedRequestAnimationFrame; - } - const unpatchedCancelAnimationFrame = nativeCancelAnimationFrame[Zone.__symbol__('OriginalDelegate')]; - if (unpatchedCancelAnimationFrame) { - nativeCancelAnimationFrame = unpatchedCancelAnimationFrame; - } +function locateOrCreateAnchorNode(lContainer, hostLView, hostTNode, slotValue) { + if (!_populateDehydratedViewsInLContainer(lContainer, hostTNode, hostLView)) { + // Populating dehydrated views operation returned `false`, which indicates + // that the logic was running in client-only mode, this an anchor comment + // node should be created for this container. + createAnchorNode(lContainer, hostLView, hostTNode, slotValue); } - return { - nativeRequestAnimationFrame, - nativeCancelAnimationFrame - }; } -class AsyncStackTaggingZoneSpec { - constructor(namePrefix, consoleAsyncStackTaggingImpl = console) { - this.name = 'asyncStackTagging for ' + namePrefix; - this.createTask = consoleAsyncStackTaggingImpl?.createTask ?? (() => null); +function enableLocateOrCreateContainerRefImpl() { + _locateOrCreateAnchorNode = locateOrCreateAnchorNode; + _populateDehydratedViewsInLContainer = populateDehydratedViewsInLContainerImpl; +} +class LQuery_ { + constructor(queryList) { + this.queryList = queryList; + this.matches = null; } - onScheduleTask(delegate, _current, target, task) { - task.consoleTask = this.createTask(`Zone - ${task.source || task.type}`); - return delegate.scheduleTask(target, task); + clone() { + return new LQuery_(this.queryList); } - onInvokeTask(delegate, _currentZone, targetZone, task, applyThis, applyArgs) { - let ret; - if (task.consoleTask) { - ret = task.consoleTask.run(() => delegate.invokeTask(targetZone, task, applyThis, applyArgs)); - } else { - ret = delegate.invokeTask(targetZone, task, applyThis, applyArgs); - } - return ret; + setDirty() { + this.queryList.setDirty(); } } - -/** - * An injectable service for executing work inside or outside of the Angular zone. - * - * The most common use of this service is to optimize performance when starting a work consisting of - * one or more asynchronous tasks that don't require UI updates or error handling to be handled by - * Angular. Such tasks can be kicked off via {@link #runOutsideAngular} and if needed, these tasks - * can reenter the Angular zone via {@link #run}. - * - * - * - * @usageNotes - * ### Example - * - * ``` - * import {Component, NgZone} from '@angular/core'; - * import {NgIf} from '@angular/common'; - * - * @Component({ - * selector: 'ng-zone-demo', - * template: ` - *

Demo: NgZone

- * - *

Progress: {{progress}}%

- *

Done processing {{label}} of Angular zone!

- * - * - * - * `, - * }) - * export class NgZoneDemo { - * progress: number = 0; - * label: string; - * - * constructor(private _ngZone: NgZone) {} - * - * // Loop inside the Angular zone - * // so the UI DOES refresh after each setTimeout cycle - * processWithinAngularZone() { - * this.label = 'inside'; - * this.progress = 0; - * this._increaseProgress(() => console.log('Inside Done!')); - * } - * - * // Loop outside of the Angular zone - * // so the UI DOES NOT refresh after each setTimeout cycle - * processOutsideOfAngularZone() { - * this.label = 'outside'; - * this.progress = 0; - * this._ngZone.runOutsideAngular(() => { - * this._increaseProgress(() => { - * // reenter the Angular zone and display done - * this._ngZone.run(() => { console.log('Outside Done!'); }); - * }); - * }); - * } - * - * _increaseProgress(doneCallback: () => void) { - * this.progress += 1; - * console.log(`Current progress: ${this.progress}%`); - * - * if (this.progress < 100) { - * window.setTimeout(() => this._increaseProgress(doneCallback), 10); - * } else { - * doneCallback(); - * } - * } - * } - * ``` - * - * @publicApi - */ -class NgZone { - constructor({ - enableLongStackTrace = false, - shouldCoalesceEventChangeDetection = false, - shouldCoalesceRunChangeDetection = false - }) { - this.hasPendingMacrotasks = false; - this.hasPendingMicrotasks = false; - /** - * Whether there are no outstanding microtasks or macrotasks. - */ - this.isStable = true; - /** - * Notifies when code enters Angular Zone. This gets fired first on VM Turn. - */ - this.onUnstable = new EventEmitter(false); - /** - * Notifies when there is no more microtasks enqueued in the current VM Turn. - * This is a hint for Angular to do change detection, which may enqueue more microtasks. - * For this reason this event can fire multiple times per VM Turn. - */ - this.onMicrotaskEmpty = new EventEmitter(false); - /** - * Notifies when the last `onMicrotaskEmpty` has run and there are no more microtasks, which - * implies we are about to relinquish VM turn. - * This event gets called just once. - */ - this.onStable = new EventEmitter(false); - /** - * Notifies that an error has been delivered. - */ - this.onError = new EventEmitter(false); - if (typeof Zone == 'undefined') { - throw new RuntimeError(908 /* RuntimeErrorCode.MISSING_ZONEJS */, ngDevMode && `In this configuration Angular requires Zone.js`); - } - Zone.assertZonePatched(); - const self = this; - self._nesting = 0; - self._outer = self._inner = Zone.current; - // AsyncStackTaggingZoneSpec provides `linked stack traces` to show - // where the async operation is scheduled. For more details, refer - // to this article, https://developer.chrome.com/blog/devtools-better-angular-debugging/ - // And we only import this AsyncStackTaggingZoneSpec in development mode, - // in the production mode, the AsyncStackTaggingZoneSpec will be tree shaken away. - if (ngDevMode) { - self._inner = self._inner.fork(new AsyncStackTaggingZoneSpec('Angular')); +class LQueries_ { + constructor(queries = []) { + this.queries = queries; + } + createEmbeddedView(tView) { + const tQueries = tView.queries; + if (tQueries !== null) { + const noOfInheritedQueries = tView.contentQueries !== null ? tView.contentQueries[0] : tQueries.length; + const viewLQueries = []; + // An embedded view has queries propagated from a declaration view at the beginning of the + // TQueries collection and up until a first content query declared in the embedded view. Only + // propagated LQueries are created at this point (LQuery corresponding to declared content + // queries will be instantiated from the content query instructions for each directive). + for (let i = 0; i < noOfInheritedQueries; i++) { + const tQuery = tQueries.getByIndex(i); + const parentLQuery = this.queries[tQuery.indexInDeclarationView]; + viewLQueries.push(parentLQuery.clone()); + } + return new LQueries_(viewLQueries); } - if (Zone['TaskTrackingZoneSpec']) { - self._inner = self._inner.fork(new Zone['TaskTrackingZoneSpec']()); + return null; + } + insertView(tView) { + this.dirtyQueriesWithMatches(tView); + } + detachView(tView) { + this.dirtyQueriesWithMatches(tView); + } + finishViewCreation(tView) { + this.dirtyQueriesWithMatches(tView); + } + dirtyQueriesWithMatches(tView) { + for (let i = 0; i < this.queries.length; i++) { + if (getTQuery(tView, i).matches !== null) { + this.queries[i].setDirty(); + } } - if (enableLongStackTrace && Zone['longStackTraceZoneSpec']) { - self._inner = self._inner.fork(Zone['longStackTraceZoneSpec']); + } +} +class TQueryMetadata_ { + constructor(predicate, flags, read = null) { + this.flags = flags; + this.read = read; + // Compiler might not be able to pre-optimize and split multiple selectors. + if (typeof predicate === 'string') { + this.predicate = splitQueryMultiSelectors(predicate); + } else { + this.predicate = predicate; } - // if shouldCoalesceRunChangeDetection is true, all tasks including event tasks will be - // coalesced, so shouldCoalesceEventChangeDetection option is not necessary and can be skipped. - self.shouldCoalesceEventChangeDetection = !shouldCoalesceRunChangeDetection && shouldCoalesceEventChangeDetection; - self.shouldCoalesceRunChangeDetection = shouldCoalesceRunChangeDetection; - self.lastRequestAnimationFrameId = -1; - self.nativeRequestAnimationFrame = getNativeRequestAnimationFrame().nativeRequestAnimationFrame; - forkInnerZoneWithAngularBehavior(self); } - /** - This method checks whether the method call happens within an Angular Zone instance. - */ - static isInAngularZone() { - // Zone needs to be checked, because this method might be called even when NoopNgZone is used. - return typeof Zone !== 'undefined' && Zone.current.get('isAngularZone') === true; +} +class TQueries_ { + constructor(queries = []) { + this.queries = queries; } - /** - Assures that the method is called within the Angular Zone, otherwise throws an error. - */ - static assertInAngularZone() { - if (!NgZone.isInAngularZone()) { - throw new RuntimeError(909 /* RuntimeErrorCode.UNEXPECTED_ZONE_STATE */, ngDevMode && 'Expected to be in Angular Zone, but it is not!'); + elementStart(tView, tNode) { + ngDevMode && assertFirstCreatePass(tView, 'Queries should collect results on the first template pass only'); + for (let i = 0; i < this.queries.length; i++) { + this.queries[i].elementStart(tView, tNode); } } - /** - Assures that the method is called outside of the Angular Zone, otherwise throws an error. - */ - static assertNotInAngularZone() { - if (NgZone.isInAngularZone()) { - throw new RuntimeError(909 /* RuntimeErrorCode.UNEXPECTED_ZONE_STATE */, ngDevMode && 'Expected to not be in Angular Zone, but it is!'); + elementEnd(tNode) { + for (let i = 0; i < this.queries.length; i++) { + this.queries[i].elementEnd(tNode); } } - /** - * Executes the `fn` function synchronously within the Angular zone and returns value returned by - * the function. - * - * Running functions via `run` allows you to reenter Angular zone from a task that was executed - * outside of the Angular zone (typically started via {@link #runOutsideAngular}). - * - * Any future tasks or microtasks scheduled from within this function will continue executing from - * within the Angular zone. - * - * If a synchronous error happens it will be rethrown and not reported via `onError`. - */ - run(fn, applyThis, applyArgs) { - return this._inner.run(fn, applyThis, applyArgs); + embeddedTView(tNode) { + let queriesForTemplateRef = null; + for (let i = 0; i < this.length; i++) { + const childQueryIndex = queriesForTemplateRef !== null ? queriesForTemplateRef.length : 0; + const tqueryClone = this.getByIndex(i).embeddedTView(tNode, childQueryIndex); + if (tqueryClone) { + tqueryClone.indexInDeclarationView = i; + if (queriesForTemplateRef !== null) { + queriesForTemplateRef.push(tqueryClone); + } else { + queriesForTemplateRef = [tqueryClone]; + } + } + } + return queriesForTemplateRef !== null ? new TQueries_(queriesForTemplateRef) : null; } - /** - * Executes the `fn` function synchronously within the Angular zone as a task and returns value - * returned by the function. - * - * Running functions via `run` allows you to reenter Angular zone from a task that was executed - * outside of the Angular zone (typically started via {@link #runOutsideAngular}). - * - * Any future tasks or microtasks scheduled from within this function will continue executing from - * within the Angular zone. - * - * If a synchronous error happens it will be rethrown and not reported via `onError`. - */ - runTask(fn, applyThis, applyArgs, name) { - const zone = this._inner; - const task = zone.scheduleEventTask('NgZoneEvent: ' + name, fn, EMPTY_PAYLOAD, noop, noop); - try { - return zone.runTask(task, applyThis, applyArgs); - } finally { - zone.cancelTask(task); + template(tView, tNode) { + ngDevMode && assertFirstCreatePass(tView, 'Queries should collect results on the first template pass only'); + for (let i = 0; i < this.queries.length; i++) { + this.queries[i].template(tView, tNode); } } - /** - * Same as `run`, except that synchronous errors are caught and forwarded via `onError` and not - * rethrown. - */ - runGuarded(fn, applyThis, applyArgs) { - return this._inner.runGuarded(fn, applyThis, applyArgs); + getByIndex(index) { + ngDevMode && assertIndexInRange(this.queries, index); + return this.queries[index]; } - /** - * Executes the `fn` function synchronously in Angular's parent zone and returns value returned by - * the function. - * - * Running functions via {@link #runOutsideAngular} allows you to escape Angular's zone and do - * work that - * doesn't trigger Angular change-detection or is subject to Angular's error handling. - * - * Any future tasks or microtasks scheduled from within this function will continue executing from - * outside of the Angular zone. - * - * Use {@link #run} to reenter the Angular zone and do work that updates the application model. - */ - runOutsideAngular(fn) { - return this._outer.run(fn); + get length() { + return this.queries.length; + } + track(tquery) { + this.queries.push(tquery); } } -const EMPTY_PAYLOAD = {}; -function checkStable(zone) { - // TODO: @JiaLiPassion, should check zone.isCheckStableRunning to prevent - // re-entry. The case is: - // - // @Component({...}) - // export class AppComponent { - // constructor(private ngZone: NgZone) { - // this.ngZone.onStable.subscribe(() => { - // this.ngZone.run(() => console.log('stable');); - // }); - // } - // - // The onStable subscriber run another function inside ngZone - // which causes `checkStable()` re-entry. - // But this fix causes some issues in g3, so this fix will be - // launched in another PR. - if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) { - try { - zone._nesting++; - zone.onMicrotaskEmpty.emit(null); - } finally { - zone._nesting--; - if (!zone.hasPendingMicrotasks) { - try { - zone.runOutsideAngular(() => zone.onStable.emit(null)); - } finally { - zone.isStable = true; - } - } +class TQuery_ { + constructor(metadata, nodeIndex = -1) { + this.metadata = metadata; + this.matches = null; + this.indexInDeclarationView = -1; + this.crossesNgTemplate = false; + /** + * A flag indicating if a given query still applies to nodes it is crossing. We use this flag + * (alongside with _declarationNodeIndex) to know when to stop applying content queries to + * elements in a template. + */ + this._appliesToNextNode = true; + this._declarationNodeIndex = nodeIndex; + } + elementStart(tView, tNode) { + if (this.isApplyingToNode(tNode)) { + this.matchTNode(tView, tNode); } } -} -function delayChangeDetectionForEvents(zone) { - /** - * We also need to check _nesting here - * Consider the following case with shouldCoalesceRunChangeDetection = true - * - * ngZone.run(() => {}); - * ngZone.run(() => {}); - * - * We want the two `ngZone.run()` only trigger one change detection - * when shouldCoalesceRunChangeDetection is true. - * And because in this case, change detection run in async way(requestAnimationFrame), - * so we also need to check the _nesting here to prevent multiple - * change detections. - */ - if (zone.isCheckStableRunning || zone.lastRequestAnimationFrameId !== -1) { - return; + elementEnd(tNode) { + if (this._declarationNodeIndex === tNode.index) { + this._appliesToNextNode = false; + } } - zone.lastRequestAnimationFrameId = zone.nativeRequestAnimationFrame.call(_global, () => { - // This is a work around for https://github.com/angular/angular/issues/36839. - // The core issue is that when event coalescing is enabled it is possible for microtasks - // to get flushed too early (As is the case with `Promise.then`) between the - // coalescing eventTasks. - // - // To workaround this we schedule a "fake" eventTask before we process the - // coalescing eventTasks. The benefit of this is that the "fake" container eventTask - // will prevent the microtasks queue from getting drained in between the coalescing - // eventTask execution. - if (!zone.fakeTopEventTask) { - zone.fakeTopEventTask = Zone.root.scheduleEventTask('fakeTopEventTask', () => { - zone.lastRequestAnimationFrameId = -1; - updateMicroTaskStatus(zone); - zone.isCheckStableRunning = true; - checkStable(zone); - zone.isCheckStableRunning = false; - }, undefined, () => {}, () => {}); + template(tView, tNode) { + this.elementStart(tView, tNode); + } + embeddedTView(tNode, childQueryIndex) { + if (this.isApplyingToNode(tNode)) { + this.crossesNgTemplate = true; + // A marker indicating a `` element (a placeholder for query results from + // embedded views created based on this ``). + this.addMatch(-tNode.index, childQueryIndex); + return new TQuery_(this.metadata); } - zone.fakeTopEventTask.invoke(); - }); - updateMicroTaskStatus(zone); -} -function forkInnerZoneWithAngularBehavior(zone) { - const delayChangeDetectionForEventsDelegate = () => { - delayChangeDetectionForEvents(zone); - }; - zone._inner = zone._inner.fork({ - name: 'angular', - properties: { - 'isAngularZone': true - }, - onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => { - if (shouldBeIgnoredByZone(applyArgs)) { - return delegate.invokeTask(target, task, applyThis, applyArgs); + return null; + } + isApplyingToNode(tNode) { + if (this._appliesToNextNode && (this.metadata.flags & 1 /* QueryFlags.descendants */) !== 1 /* QueryFlags.descendants */) { + const declarationNodeIdx = this._declarationNodeIndex; + let parent = tNode.parent; + // Determine if a given TNode is a "direct" child of a node on which a content query was + // declared (only direct children of query's host node can match with the descendants: false + // option). There are 3 main use-case / conditions to consider here: + // - : here parent node is a query + // host node; + // - : + // here parent node is null; + // - : here we need + // to go past `` to determine parent node (but we shouldn't traverse + // up past the query's host node!). + while (parent !== null && parent.type & 8 /* TNodeType.ElementContainer */ && parent.index !== declarationNodeIdx) { + parent = parent.parent; } - try { - onEnter(zone); - return delegate.invokeTask(target, task, applyThis, applyArgs); - } finally { - if (zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask' || zone.shouldCoalesceRunChangeDetection) { - delayChangeDetectionForEventsDelegate(); - } - onLeave(zone); + return declarationNodeIdx === (parent !== null ? parent.index : -1); + } + return this._appliesToNextNode; + } + matchTNode(tView, tNode) { + const predicate = this.metadata.predicate; + if (Array.isArray(predicate)) { + for (let i = 0; i < predicate.length; i++) { + const name = predicate[i]; + this.matchTNodeWithReadOption(tView, tNode, getIdxOfMatchingSelector(tNode, name)); + // Also try matching the name to a provider since strings can be used as DI tokens too. + this.matchTNodeWithReadOption(tView, tNode, locateDirectiveOrProvider(tNode, tView, name, false, false)); } - }, - onInvoke: (delegate, current, target, callback, applyThis, applyArgs, source) => { - try { - onEnter(zone); - return delegate.invoke(target, callback, applyThis, applyArgs, source); - } finally { - if (zone.shouldCoalesceRunChangeDetection) { - delayChangeDetectionForEventsDelegate(); + } else { + if (predicate === TemplateRef) { + if (tNode.type & 4 /* TNodeType.Container */) { + this.matchTNodeWithReadOption(tView, tNode, -1); } - onLeave(zone); + } else { + this.matchTNodeWithReadOption(tView, tNode, locateDirectiveOrProvider(tNode, tView, predicate, false, false)); } - }, - onHasTask: (delegate, current, target, hasTaskState) => { - delegate.hasTask(target, hasTaskState); - if (current === target) { - // We are only interested in hasTask events which originate from our zone - // (A child hasTask event is not interesting to us) - if (hasTaskState.change == 'microTask') { - zone._hasPendingMicrotasks = hasTaskState.microTask; - updateMicroTaskStatus(zone); - checkStable(zone); - } else if (hasTaskState.change == 'macroTask') { - zone.hasPendingMacrotasks = hasTaskState.macroTask; + } + } + matchTNodeWithReadOption(tView, tNode, nodeMatchIdx) { + if (nodeMatchIdx !== null) { + const read = this.metadata.read; + if (read !== null) { + if (read === ElementRef || read === ViewContainerRef || read === TemplateRef && tNode.type & 4 /* TNodeType.Container */) { + this.addMatch(tNode.index, -2); + } else { + const directiveOrProviderIdx = locateDirectiveOrProvider(tNode, tView, read, false, false); + if (directiveOrProviderIdx !== null) { + this.addMatch(tNode.index, directiveOrProviderIdx); + } } + } else { + this.addMatch(tNode.index, nodeMatchIdx); } - }, - onHandleError: (delegate, current, target, error) => { - delegate.handleError(target, error); - zone.runOutsideAngular(() => zone.onError.emit(error)); - return false; } - }); -} -function updateMicroTaskStatus(zone) { - if (zone._hasPendingMicrotasks || (zone.shouldCoalesceEventChangeDetection || zone.shouldCoalesceRunChangeDetection) && zone.lastRequestAnimationFrameId !== -1) { - zone.hasPendingMicrotasks = true; - } else { - zone.hasPendingMicrotasks = false; } -} -function onEnter(zone) { - zone._nesting++; - if (zone.isStable) { - zone.isStable = false; - zone.onUnstable.emit(null); + addMatch(tNodeIdx, matchIdx) { + if (this.matches === null) { + this.matches = [tNodeIdx, matchIdx]; + } else { + this.matches.push(tNodeIdx, matchIdx); + } } } -function onLeave(zone) { - zone._nesting--; - checkStable(zone); -} /** - * Provides a noop implementation of `NgZone` which does nothing. This zone requires explicit calls - * to framework to perform rendering. + * Iterates over local names for a given node and returns directive index + * (or -1 if a local name points to an element). + * + * @param tNode static data of a node to check + * @param selector selector to match + * @returns directive index, -1 or null if a selector didn't match any of the local names */ -class NoopNgZone { - constructor() { - this.hasPendingMicrotasks = false; - this.hasPendingMacrotasks = false; - this.isStable = true; - this.onUnstable = new EventEmitter(); - this.onMicrotaskEmpty = new EventEmitter(); - this.onStable = new EventEmitter(); - this.onError = new EventEmitter(); - } - run(fn, applyThis, applyArgs) { - return fn.apply(applyThis, applyArgs); - } - runGuarded(fn, applyThis, applyArgs) { - return fn.apply(applyThis, applyArgs); +function getIdxOfMatchingSelector(tNode, selector) { + const localNames = tNode.localNames; + if (localNames !== null) { + for (let i = 0; i < localNames.length; i += 2) { + if (localNames[i] === selector) { + return localNames[i + 1]; + } + } } - runOutsideAngular(fn) { - return fn(); + return null; +} +function createResultByTNodeType(tNode, currentView) { + if (tNode.type & (3 /* TNodeType.AnyRNode */ | 8 /* TNodeType.ElementContainer */)) { + return createElementRef(tNode, currentView); + } else if (tNode.type & 4 /* TNodeType.Container */) { + return createTemplateRef(tNode, currentView); } - runTask(fn, applyThis, applyArgs, name) { - return fn.apply(applyThis, applyArgs); + return null; +} +function createResultForNode(lView, tNode, matchingIdx, read) { + if (matchingIdx === -1) { + // if read token and / or strategy is not specified, detect it using appropriate tNode type + return createResultByTNodeType(tNode, lView); + } else if (matchingIdx === -2) { + // read a special token from a node injector + return createSpecialToken(lView, tNode, read); + } else { + // read a token + return getNodeInjectable(lView, lView[TVIEW], matchingIdx, tNode); } } -function shouldBeIgnoredByZone(applyArgs) { - if (!Array.isArray(applyArgs)) { - return false; +function createSpecialToken(lView, tNode, read) { + if (read === ElementRef) { + return createElementRef(tNode, lView); + } else if (read === TemplateRef) { + return createTemplateRef(tNode, lView); + } else if (read === ViewContainerRef) { + ngDevMode && assertTNodeType(tNode, 3 /* TNodeType.AnyRNode */ | 12 /* TNodeType.AnyContainer */); + return createContainerRef(tNode, lView); + } else { + ngDevMode && throwError(`Special token to read should be one of ElementRef, TemplateRef or ViewContainerRef but got ${stringify(read)}.`); } - // We should only ever get 1 arg passed through to invokeTask. - // Short circuit here incase that behavior changes. - if (applyArgs.length !== 1) { - return false; +} +/** + * A helper function that creates query results for a given view. This function is meant to do the + * processing once and only once for a given view instance (a set of results for a given view + * doesn't change). + */ +function materializeViewResults(tView, lView, tQuery, queryIndex) { + const lQuery = lView[QUERIES].queries[queryIndex]; + if (lQuery.matches === null) { + const tViewData = tView.data; + const tQueryMatches = tQuery.matches; + const result = []; + for (let i = 0; tQueryMatches !== null && i < tQueryMatches.length; i += 2) { + const matchedNodeIdx = tQueryMatches[i]; + if (matchedNodeIdx < 0) { + // we at the marker which might have results in views created based on this + // - those results will be in separate views though, so here we just leave + // null as a placeholder + result.push(null); + } else { + ngDevMode && assertIndexInRange(tViewData, matchedNodeIdx); + const tNode = tViewData[matchedNodeIdx]; + result.push(createResultForNode(lView, tNode, tQueryMatches[i + 1], tQuery.metadata.read)); + } + } + lQuery.matches = result; } - // Prevent triggering change detection when the __ignore_ng_zone__ flag is detected. - return applyArgs[0].data?.['__ignore_ng_zone__'] === true; + return lQuery.matches; } -function getNgZone(ngZoneToUse = 'zone.js', options) { - if (ngZoneToUse === 'noop') { - return new NoopNgZone(); +/** + * A helper function that collects (already materialized) query results from a tree of views, + * starting with a provided LView. + */ +function collectQueryResults(tView, lView, queryIndex, result) { + const tQuery = tView.queries.getByIndex(queryIndex); + const tQueryMatches = tQuery.matches; + if (tQueryMatches !== null) { + const lViewResults = materializeViewResults(tView, lView, tQuery, queryIndex); + for (let i = 0; i < tQueryMatches.length; i += 2) { + const tNodeIdx = tQueryMatches[i]; + if (tNodeIdx > 0) { + result.push(lViewResults[i / 2]); + } else { + const childQueryIndex = tQueryMatches[i + 1]; + const declarationLContainer = lView[-tNodeIdx]; + ngDevMode && assertLContainer(declarationLContainer); + // collect matches for views inserted in this container + for (let i = CONTAINER_HEADER_OFFSET; i < declarationLContainer.length; i++) { + const embeddedLView = declarationLContainer[i]; + if (embeddedLView[DECLARATION_LCONTAINER] === embeddedLView[PARENT]) { + collectQueryResults(embeddedLView[TVIEW], embeddedLView, childQueryIndex, result); + } + } + // collect matches for views created from this declaration container and inserted into + // different containers + if (declarationLContainer[MOVED_VIEWS] !== null) { + const embeddedLViews = declarationLContainer[MOVED_VIEWS]; + for (let i = 0; i < embeddedLViews.length; i++) { + const embeddedLView = embeddedLViews[i]; + collectQueryResults(embeddedLView[TVIEW], embeddedLView, childQueryIndex, result); + } + } + } + } } - if (ngZoneToUse === 'zone.js') { - return new NgZone(options); + return result; +} +function loadQueryInternal(lView, queryIndex) { + ngDevMode && assertDefined(lView[QUERIES], 'LQueries should be defined when trying to load a query'); + ngDevMode && assertIndexInRange(lView[QUERIES].queries, queryIndex); + return lView[QUERIES].queries[queryIndex].queryList; +} +/** + * Creates a new instance of LQuery and returns its index in the collection of LQuery objects. + * + * @returns index in the collection of LQuery objects + */ +function createLQuery(tView, lView, flags) { + const queryList = new QueryList((flags & 4 /* QueryFlags.emitDistinctChangesOnly */) === 4 /* QueryFlags.emitDistinctChangesOnly */); + storeCleanupWithContext(tView, lView, queryList, queryList.destroy); + const lQueries = (lView[QUERIES] ??= new LQueries_()).queries; + return lQueries.push(new LQuery_(queryList)) - 1; +} +function createViewQuery(predicate, flags, read) { + ngDevMode && assertNumber(flags, 'Expecting flags'); + const tView = getTView(); + if (tView.firstCreatePass) { + createTQuery(tView, new TQueryMetadata_(predicate, flags, read), -1); + if ((flags & 2 /* QueryFlags.isStatic */) === 2 /* QueryFlags.isStatic */) { + tView.staticViewQueries = true; + } } - return ngZoneToUse; + return createLQuery(tView, getLView(), flags); +} +function createContentQuery(directiveIndex, predicate, flags, read) { + ngDevMode && assertNumber(flags, 'Expecting flags'); + const tView = getTView(); + if (tView.firstCreatePass) { + const tNode = getCurrentTNode(); + createTQuery(tView, new TQueryMetadata_(predicate, flags, read), tNode.index); + saveContentQueryAndDirectiveIndex(tView, directiveIndex); + if ((flags & 2 /* QueryFlags.isStatic */) === 2 /* QueryFlags.isStatic */) { + tView.staticContentQueries = true; + } + } + return createLQuery(tView, getLView(), flags); +} +/** Splits multiple selectors in the locator. */ +function splitQueryMultiSelectors(locator) { + return locator.split(',').map(s => s.trim()); +} +function createTQuery(tView, metadata, nodeIndex) { + if (tView.queries === null) tView.queries = new TQueries_(); + tView.queries.track(new TQuery_(metadata, nodeIndex)); +} +function saveContentQueryAndDirectiveIndex(tView, directiveIndex) { + const tViewContentQueries = tView.contentQueries || (tView.contentQueries = []); + const lastSavedDirectiveIndex = tViewContentQueries.length ? tViewContentQueries[tViewContentQueries.length - 1] : -1; + if (directiveIndex !== lastSavedDirectiveIndex) { + tViewContentQueries.push(tView.queries.length - 1, directiveIndex); + } +} +function getTQuery(tView, index) { + ngDevMode && assertDefined(tView.queries, 'TQueries must be defined to retrieve a TQuery'); + return tView.queries.getByIndex(index); +} +/** + * A helper function collecting results from all the views where a given query was active. + * @param lView + * @param queryIndex + */ +function getQueryResults(lView, queryIndex) { + const tView = lView[TVIEW]; + const tQuery = getTQuery(tView, queryIndex); + return tQuery.crossesNgTemplate ? collectQueryResults(tView, lView, queryIndex, []) : materializeViewResults(tView, lView, tQuery, queryIndex); } /** - * The phase to run an `afterRender` or `afterNextRender` callback in. + * Checks if the given `value` is a reactive `Signal`. + */ +function isSignal(value) { + return typeof value === 'function' && value[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL] !== undefined; +} + +/** Symbol used distinguish `WritableSignal` from other non-writable signals and functions. */ +const WRITABLE_SIGNAL = /* @__PURE__ */(/* unused pure expression or super */ null && (Symbol('WRITABLE_SIGNAL'))); +/** + * Utility function used during template type checking to extract the value from a `WritableSignal`. + * @codeGenApi + */ +function ɵunwrapWritableSignal(value) { + // Note: needs to be kept in sync with the copy in `fake_core/index.ts`. + // Note: the function uses `WRITABLE_SIGNAL` as a brand instead of `WritableSignal`, + // because the latter incorrectly unwraps non-signal getter functions. + return null; +} +/** + * Create a `Signal` that can be set or updated directly. + */ +function signal(initialValue, options) { + performanceMarkFeature('NgSignals'); + const signalFn = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.createSignal)(initialValue); + const node = signalFn[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL]; + if (options?.equal) { + node.equal = options.equal; + } + signalFn.set = newValue => (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.signalSetFn)(node, newValue); + signalFn.update = updateFn => (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.signalUpdateFn)(node, updateFn); + signalFn.asReadonly = signalAsReadonlyFn.bind(signalFn); + if (ngDevMode) { + signalFn.toString = () => `[Signal: ${signalFn()}]`; + } + return signalFn; +} +function signalAsReadonlyFn() { + const node = this[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL]; + if (node.readonlyFn === undefined) { + const readonlyFn = () => this(); + readonlyFn[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL] = node; + node.readonlyFn = readonlyFn; + } + return node.readonlyFn; +} +/** + * Checks if the given `value` is a writeable signal. + */ +function isWritableSignal(value) { + return isSignal(value) && typeof value.set === 'function'; +} + +/** + * A signal factory function in charge of creating a new computed signal capturing query + * results. This centralized creation function is used by all types of queries (child / children, + * required / optional). * - * Callbacks in the same phase run in the order they are registered. Phases run in the - * following order after each render: + * @param firstOnly indicates if all or only the first result should be returned + * @param required indicates if at least one result is required + * @returns a read-only signal with query results + */ +function createQuerySignalFn(firstOnly, required) { + let node; + const signalFn = (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.createComputed)(() => { + // A dedicated signal that increments its value every time a query changes its dirty status. By + // using this signal we can implement a query as computed and avoid creation of a specialized + // reactive node type. Please note that a query gets marked dirty under the following + // circumstances: + // - a view (where a query is active) finished its first creation pass; + // - a new view is inserted / deleted and it impacts query results. + node._dirtyCounter(); + const value = refreshSignalQuery(node, firstOnly); + if (required && value === undefined) { + throw new RuntimeError(-951 /* RuntimeErrorCode.REQUIRED_QUERY_NO_VALUE */, ngDevMode && 'Child query result is required but no value is available.'); + } + return value; + }); + node = signalFn[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL]; + node._dirtyCounter = signal(0); + node._flatValue = undefined; + if (ngDevMode) { + signalFn.toString = () => `[Query Signal]`; + } + return signalFn; +} +function createSingleResultOptionalQuerySignalFn() { + return createQuerySignalFn( /* firstOnly */true, /* required */false); +} +function createSingleResultRequiredQuerySignalFn() { + return createQuerySignalFn( /* firstOnly */true, /* required */true); +} +function createMultiResultQuerySignalFn() { + return createQuerySignalFn( /* firstOnly */false, /* required */false); +} +function bindQueryToSignal(target, queryIndex) { + const node = target[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL]; + node._lView = getLView(); + node._queryIndex = queryIndex; + node._queryList = loadQueryInternal(node._lView, queryIndex); + node._queryList.onDirty(() => node._dirtyCounter.update(v => v + 1)); +} +function refreshSignalQuery(node, firstOnly) { + const lView = node._lView; + const queryIndex = node._queryIndex; + // There are 2 conditions under which we want to return "empty" results instead of the ones + // collected by a query: + // + // 1) a given query wasn't created yet (this is a period of time between the directive creation + // and execution of the query creation function) - in this case a query doesn't exist yet and we + // don't have any results to return. + // + // 2) we are in the process of constructing a view (the first + // creation pass didn't finish) and a query might have partial results, but we don't want to + // return those - instead we do delay results collection until all nodes had a chance of matching + // and we can present consistent, "atomic" (on a view level) results. + if (lView === undefined || queryIndex === undefined || lView[FLAGS] & 4 /* LViewFlags.CreationMode */) { + return firstOnly ? undefined : EMPTY_ARRAY; + } + const queryList = loadQueryInternal(lView, queryIndex); + const results = getQueryResults(lView, queryIndex); + queryList.reset(results, unwrapElementRef); + if (firstOnly) { + return queryList.first; + } else { + // TODO: remove access to the private _changesDetected field by abstracting / removing usage of + // QueryList in the signal-based queries (perf follow-up) + const resultChanged = queryList._changesDetected; + if (resultChanged || node._flatValue === undefined) { + return node._flatValue = queryList.toArray(); + } + return node._flatValue; + } +} +function viewChildFn(locator, opts) { + return createSingleResultOptionalQuerySignalFn(); +} +function viewChildRequiredFn(locator, opts) { + return createSingleResultRequiredQuerySignalFn(); +} +/** + * Initializes a view child query. * - * 1. `AfterRenderPhase.EarlyRead` - * 2. `AfterRenderPhase.Write` - * 3. `AfterRenderPhase.MixedReadWrite` - * 4. `AfterRenderPhase.Read` + * Consider using `viewChild.required` for queries that should always match. * - * Angular is unable to verify or enforce that phases are used correctly, and instead - * relies on each developer to follow the guidelines documented for each value and - * carefully choose the appropriate one, refactoring their code if necessary. By doing - * so, Angular is better able to minimize the performance degradation associated with - * manual DOM access, ensuring the best experience for the end users of your application - * or library. + * @usageNotes + * Create a child query in your component by declaring a + * class field and initializing it with the `viewChild()` function. + * + * ```ts + * @Component({template: '
'}) + * export class TestComponent { + * divEl = viewChild('el'); // Signal + * divElRequired = viewChild.required('el'); // Signal + * cmp = viewChild(MyComponent); // Signal + * cmpRequired = viewChild.required(MyComponent); // Signal + * } + * ``` * * @developerPreview */ -var AfterRenderPhase = /*#__PURE__*/function (AfterRenderPhase) { - /** - * Use `AfterRenderPhase.EarlyRead` for callbacks that only need to **read** from the - * DOM before a subsequent `AfterRenderPhase.Write` callback, for example to perform - * custom layout that the browser doesn't natively support. **Never** use this phase - * for callbacks that can write to the DOM or when `AfterRenderPhase.Read` is adequate. - * - *
- * - * Using this value can degrade performance. - * Instead, prefer using built-in browser functionality when possible. - * - *
- */ - AfterRenderPhase[AfterRenderPhase["EarlyRead"] = 0] = "EarlyRead"; - /** - * Use `AfterRenderPhase.Write` for callbacks that only **write** to the DOM. **Never** - * use this phase for callbacks that can read from the DOM. - */ - AfterRenderPhase[AfterRenderPhase["Write"] = 1] = "Write"; - /** - * Use `AfterRenderPhase.MixedReadWrite` for callbacks that read from or write to the - * DOM, that haven't been refactored to use a different phase. **Never** use this phase - * for callbacks that can use a different phase instead. - * - *
- * - * Using this value can **significantly** degrade performance. - * Instead, prefer refactoring into multiple callbacks using a more specific phase. - * - *
- */ - AfterRenderPhase[AfterRenderPhase["MixedReadWrite"] = 2] = "MixedReadWrite"; - /** - * Use `AfterRenderPhase.Read` for callbacks that only **read** from the DOM. **Never** - * use this phase for callbacks that can write to the DOM. - */ - AfterRenderPhase[AfterRenderPhase["Read"] = 3] = "Read"; - return AfterRenderPhase; -}(AfterRenderPhase || {}); -/** `AfterRenderRef` that does nothing. */ -const NOOP_AFTER_RENDER_REF = { - destroy() {} -}; +const viewChild = /*#__PURE__*/(() => { + // Note: This may be considered a side-effect, but nothing will depend on + // this assignment, unless this `viewChild` constant export is accessed. It's a + // self-contained side effect that is local to the user facing `viewChild` export. + viewChildFn.required = viewChildRequiredFn; + return viewChildFn; +})(); /** - * Register a callback to run once before any userspace `afterRender` or - * `afterNextRender` callbacks. + * Initializes a view children query. * - * This function should almost always be used instead of `afterRender` or - * `afterNextRender` for implementing framework functionality. Consider: + * Query results are represented as a signal of a read-only collection containing all matched + * elements. * - * 1.) `AfterRenderPhase.EarlyRead` is intended to be used for implementing - * custom layout. If the framework itself mutates the DOM after *any* - * `AfterRenderPhase.EarlyRead` callbacks are run, the phase can no - * longer reliably serve its purpose. + * @usageNotes + * Create a children query in your component by declaring a + * class field and initializing it with the `viewChildren()` function. * - * 2.) Importing `afterRender` in the framework can reduce the ability for it - * to be tree-shaken, and the framework shouldn't need much of the behavior. + * ```ts + * @Component({...}) + * export class TestComponent { + * divEls = viewChildren('el'); // Signal> + * } + * ``` */ -function internalAfterNextRender(callback, options) { - const injector = options?.injector ?? inject(Injector); - // Similarly to the public `afterNextRender` function, an internal one - // is only invoked in a browser as long as the runOnServer option is not set. - if (!options?.runOnServer && !isPlatformBrowser(injector)) return; - const afterRenderEventManager = injector.get(AfterRenderEventManager); - afterRenderEventManager.internalCallbacks.push(callback); +function viewChildren(locator, opts) { + return createMultiResultQuerySignalFn(); +} +function contentChildFn(locator, opts) { + return createSingleResultOptionalQuerySignalFn(); +} +function contentChildRequiredFn(locator, opts) { + return createSingleResultRequiredQuerySignalFn(); } /** - * Register a callback to be invoked each time the application - * finishes rendering. - * - *
- * - * You should always explicitly specify a non-default [phase](api/core/AfterRenderPhase), or you - * risk significant performance degradation. - * - *
- * - * Note that the callback will run - * - in the order it was registered - * - once per render - * - on browser platforms only - * - *
+ * Initializes a content child query. Consider using `contentChild.required` for queries that should + * always match. * - * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs. - * You must use caution when directly reading or writing the DOM and layout. + * @usageNotes + * Create a child query in your component by declaring a + * class field and initializing it with the `contentChild()` function. * - *
+ * ```ts + * @Component({...}) + * export class TestComponent { + * headerEl = contentChild('h'); // Signal + * headerElElRequired = contentChild.required('h'); // Signal + * header = contentChild(MyHeader); // Signal + * headerRequired = contentChild.required(MyHeader); // Signal + * } + * ``` + */ +const contentChild = /*#__PURE__*/(() => { + // Note: This may be considered a side-effect, but nothing will depend on + // this assignment, unless this `viewChild` constant export is accessed. It's a + // self-contained side effect that is local to the user facing `viewChild` export. + contentChildFn.required = contentChildRequiredFn; + return contentChildFn; +})(); +/** + * Initializes a content children query. * - * @param callback A callback function to register + * Query results are represented as a signal of a read-only collection containing all matched + * elements. * * @usageNotes + * Create a children query in your component by declaring a + * class field and initializing it with the `contentChildren()` function. * - * Use `afterRender` to read or write the DOM after each render. - * - * ### Example * ```ts - * @Component({ - * selector: 'my-cmp', - * template: `{{ ... }}`, - * }) - * export class MyComponent { - * @ViewChild('content') contentRef: ElementRef; - * - * constructor() { - * afterRender(() => { - * console.log('content height: ' + this.contentRef.nativeElement.scrollHeight); - * }, {phase: AfterRenderPhase.Read}); - * } + * @Component({...}) + * export class TestComponent { + * headerEl = contentChildren('h'); // Signal> * } * ``` + */ +function contentChildren(locator, opts) { + return createMultiResultQuerySignalFn(); +} + +/** + * Creates a model signal. * - * @developerPreview + * @param initialValue The initial value. + * Can be set to {@link REQUIRED_UNSET_VALUE} for required model signals. + * @param options Additional options for the model. */ -function afterRender(callback, options) { - ngDevMode && assertNotInReactiveContext(afterRender, 'Call `afterRender` outside of a reactive context. For example, schedule the render ' + 'callback inside the component constructor`.'); - !options && assertInInjectionContext(afterRender); - const injector = options?.injector ?? inject(Injector); - if (!isPlatformBrowser(injector)) { - return NOOP_AFTER_RENDER_REF; +function createModelSignal(initialValue) { + const subscriptions = []; + const node = Object.create(INPUT_SIGNAL_NODE); + node.value = initialValue; + function getter() { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.producerAccessed)(node); + assertModelSet(node.value); + return node.value; } - performanceMarkFeature('NgAfterRender'); - const afterRenderEventManager = injector.get(AfterRenderEventManager); - // Lazily initialize the handler implementation, if necessary. This is so that it can be - // tree-shaken if `afterRender` and `afterNextRender` aren't used. - const callbackHandler = afterRenderEventManager.handler ??= new AfterRenderCallbackHandlerImpl(); - const phase = options?.phase ?? AfterRenderPhase.MixedReadWrite; - const destroy = () => { - callbackHandler.unregister(instance); - unregisterFn(); + function notifySubscribers(value) { + for (let i = 0; i < subscriptions.length; i++) { + subscriptions[i](value); + } + } + getter[_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.SIGNAL] = node; + getter.asReadonly = signalAsReadonlyFn.bind(getter); + getter.set = newValue => { + if (!node.equal(node.value, newValue)) { + (0,_angular_core_primitives_signals__WEBPACK_IMPORTED_MODULE_0__.signalSetFn)(node, newValue); + notifySubscribers(newValue); + } }; - const unregisterFn = injector.get(DestroyRef).onDestroy(destroy); - const instance = new AfterRenderCallback(injector, phase, callback); - callbackHandler.register(instance); - return { - destroy + getter.update = updateFn => { + assertModelSet(node.value); + getter.set(updateFn(node.value)); + }; + getter.subscribe = callback => { + subscriptions.push(callback); + return () => { + const index = subscriptions.indexOf(callback); + if (index > -1) { + subscriptions.splice(index, 1); + } + }; }; + if (ngDevMode) { + getter.toString = () => `[Model Signal: ${getter()}]`; + } + return getter; +} +/** Asserts that a model's value is set. */ +function assertModelSet(value) { + if (value === REQUIRED_UNSET_VALUE) { + throw new RuntimeError(-952 /* RuntimeErrorCode.REQUIRED_MODEL_NO_VALUE */, ngDevMode && 'Model is required but no value is available yet.'); + } +} +function modelFunction(initialValue) { + return createModelSignal(initialValue); +} +function modelRequiredFunction() { + return createModelSignal(REQUIRED_UNSET_VALUE); } /** - * Register a callback to be invoked the next time the application - * finishes rendering. - * - *
+ * `model` declares a writeable signal that is exposed as an input/output pair on the containing + * directive. The input name is taken either from the class member or from the `alias` option. + * The output name is generated by taking the input name and appending `Change`. * - * You should always explicitly specify a non-default [phase](api/core/AfterRenderPhase), or you - * risk significant performance degradation. + * Initializes a model with an initial value. If no explicit value + * is specified, Angular will use `undefined`. * - *
+ * Consider using `model.required` for models that don't need an + * initial value. * - * Note that the callback will run - * - in the order it was registered - * - on browser platforms only + * @usageNotes + * Initialize a model in your directive or component by declaring a + * class field and initializing it with the `model()` or `model.required()` + * function. * - *
+ * ```ts + * @Directive({..}) + * export class MyDir { + * firstName = model(); // string|undefined + * lastName = model.required(); // string + * age = model(0); // number + * } + * ``` * - * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs. - * You must use caution when directly reading or writing the DOM and layout. + * @developerPreview + */ +const model = /*#__PURE__*/(() => { + // Note: This may be considered a side-effect, but nothing will depend on + // this assignment, unless this `model` constant export is accessed. It's a + // self-contained side effect that is local to the user facing `model` export. + modelFunction.required = modelRequiredFunction; + return modelFunction; +})(); + +// Stores the default value of `emitDistinctChangesOnly` when the `emitDistinctChangesOnly` is not +// explicitly set. +const emitDistinctChangesOnlyDefaultValue = true; +/** + * Base class for query metadata. * - *
+ * @see {@link ContentChildren} + * @see {@link ContentChild} + * @see {@link ViewChildren} + * @see {@link ViewChild} * - * @param callback A callback function to register + * @publicApi + */ +class Query {} +/** + * ContentChildren decorator and metadata. * - * @usageNotes * - * Use `afterNextRender` to read or write the DOM once, - * for example to initialize a non-Angular library. + * @Annotation + * @publicApi + */ +const ContentChildren = /*#__PURE__*/makePropDecorator('ContentChildren', (selector, opts = {}) => ({ + selector, + first: false, + isViewQuery: false, + descendants: false, + emitDistinctChangesOnly: emitDistinctChangesOnlyDefaultValue, + ...opts +}), Query); +/** + * ContentChild decorator and metadata. * - * ### Example - * ```ts - * @Component({ - * selector: 'my-chart-cmp', - * template: `
{{ ... }}
`, - * }) - * export class MyChartCmp { - * @ViewChild('chart') chartRef: ElementRef; - * chart: MyChart|null; * - * constructor() { - * afterNextRender(() => { - * this.chart = new MyChart(this.chartRef.nativeElement); - * }, {phase: AfterRenderPhase.Write}); - * } - * } - * ``` + * @Annotation * - * @developerPreview + * @publicApi */ -function afterNextRender(callback, options) { - !options && assertInInjectionContext(afterNextRender); - const injector = options?.injector ?? inject(Injector); - if (!isPlatformBrowser(injector)) { - return NOOP_AFTER_RENDER_REF; - } - performanceMarkFeature('NgAfterNextRender'); - const afterRenderEventManager = injector.get(AfterRenderEventManager); - // Lazily initialize the handler implementation, if necessary. This is so that it can be - // tree-shaken if `afterRender` and `afterNextRender` aren't used. - const callbackHandler = afterRenderEventManager.handler ??= new AfterRenderCallbackHandlerImpl(); - const phase = options?.phase ?? AfterRenderPhase.MixedReadWrite; - const destroy = () => { - callbackHandler.unregister(instance); - unregisterFn(); - }; - const unregisterFn = injector.get(DestroyRef).onDestroy(destroy); - const instance = new AfterRenderCallback(injector, phase, () => { - destroy(); - callback(); - }); - callbackHandler.register(instance); - return { - destroy - }; -} +const ContentChild = /*#__PURE__*/makePropDecorator('ContentChild', (selector, opts = {}) => ({ + selector, + first: true, + isViewQuery: false, + descendants: true, + ...opts +}), Query); /** - * A wrapper around a function to be used as an after render callback. + * ViewChildren decorator and metadata. + * + * @Annotation + * @publicApi */ -class AfterRenderCallback { - constructor(injector, phase, callbackFn) { - this.phase = phase; - this.callbackFn = callbackFn; - this.zone = injector.get(NgZone); - this.errorHandler = injector.get(ErrorHandler, null, { - optional: true - }); - } - invoke() { - try { - this.zone.runOutsideAngular(this.callbackFn); - } catch (err) { - this.errorHandler?.handleError(err); - } - } -} +const ViewChildren = /*#__PURE__*/makePropDecorator('ViewChildren', (selector, opts = {}) => ({ + selector, + first: false, + isViewQuery: true, + descendants: true, + emitDistinctChangesOnly: emitDistinctChangesOnlyDefaultValue, + ...opts +}), Query); /** - * Core functionality for `afterRender` and `afterNextRender`. Kept separate from - * `AfterRenderEventManager` for tree-shaking. + * ViewChild decorator and metadata. + * + * @Annotation + * @publicApi */ -class AfterRenderCallbackHandlerImpl { - constructor() { - this.executingCallbacks = false; - this.buckets = { - // Note: the order of these keys controls the order the phases are run. - [AfterRenderPhase.EarlyRead]: new Set(), - [AfterRenderPhase.Write]: new Set(), - [AfterRenderPhase.MixedReadWrite]: new Set(), - [AfterRenderPhase.Read]: new Set() - }; - this.deferredCallbacks = new Set(); - } - register(callback) { - // If we're currently running callbacks, new callbacks should be deferred - // until the next render operation. - const target = this.executingCallbacks ? this.deferredCallbacks : this.buckets[callback.phase]; - target.add(callback); - } - unregister(callback) { - this.buckets[callback.phase].delete(callback); - this.deferredCallbacks.delete(callback); - } - execute() { - this.executingCallbacks = true; - for (const bucket of Object.values(this.buckets)) { - for (const callback of bucket) { - callback.invoke(); - } - } - this.executingCallbacks = false; - for (const callback of this.deferredCallbacks) { - this.buckets[callback.phase].add(callback); - } - this.deferredCallbacks.clear(); - } - destroy() { - for (const bucket of Object.values(this.buckets)) { - bucket.clear(); - } - this.deferredCallbacks.clear(); - } -} +const ViewChild = /*#__PURE__*/makePropDecorator('ViewChild', (selector, opts) => ({ + selector, + first: true, + isViewQuery: true, + descendants: true, + ...opts +}), Query); + /** - * Implements core timing for `afterRender` and `afterNextRender` events. - * Delegates to an optional `AfterRenderCallbackHandler` for implementation. + * Used to resolve resource URLs on `@Component` when used with JIT compilation. + * + * Example: + * ``` + * @Component({ + * selector: 'my-comp', + * templateUrl: 'my-comp.html', // This requires asynchronous resolution + * }) + * class MyComponent{ + * } + * + * // Calling `renderComponent` will fail because `renderComponent` is a synchronous process + * // and `MyComponent`'s `@Component.templateUrl` needs to be resolved asynchronously. + * + * // Calling `resolveComponentResources()` will resolve `@Component.templateUrl` into + * // `@Component.template`, which allows `renderComponent` to proceed in a synchronous manner. + * + * // Use browser's `fetch()` function as the default resource resolution strategy. + * resolveComponentResources(fetch).then(() => { + * // After resolution all URLs have been converted into `template` strings. + * renderComponent(MyComponent); + * }); + * + * ``` + * + * NOTE: In AOT the resolution happens during compilation, and so there should be no need + * to call this method outside JIT mode. + * + * @param resourceResolver a function which is responsible for returning a `Promise` to the + * contents of the resolved URL. Browser's `fetch()` method is a good default implementation. */ -let AfterRenderEventManager = /*#__PURE__*/(() => { - class AfterRenderEventManager { - constructor() { - /* @internal */ - this.handler = null; - /* @internal */ - this.internalCallbacks = []; - } - /** - * Executes internal and user-provided callbacks. - */ - execute() { - this.executeInternalCallbacks(); - this.handler?.execute(); +function resolveComponentResources(resourceResolver) { + // Store all promises which are fetching the resources. + const componentResolved = []; + // Cache so that we don't fetch the same resource more than once. + const urlMap = new Map(); + function cachedResourceResolve(url) { + let promise = urlMap.get(url); + if (!promise) { + const resp = resourceResolver(url); + urlMap.set(url, promise = resp.then(unwrapResponse)); } - executeInternalCallbacks() { - // Note: internal callbacks power `internalAfterNextRender`. Since internal callbacks - // are fairly trivial, they are kept separate so that `AfterRenderCallbackHandlerImpl` - // can still be tree-shaken unless used by the application. - const callbacks = [...this.internalCallbacks]; - this.internalCallbacks.length = 0; - for (const callback of callbacks) { - callback(); - } + return promise; + } + componentResourceResolutionQueue.forEach((component, type) => { + const promises = []; + if (component.templateUrl) { + promises.push(cachedResourceResolve(component.templateUrl).then(template => { + component.template = template; + })); } - ngOnDestroy() { - this.handler?.destroy(); - this.handler = null; - this.internalCallbacks.length = 0; + const styles = typeof component.styles === 'string' ? [component.styles] : component.styles || []; + component.styles = styles; + if (component.styleUrl && component.styleUrls?.length) { + throw new Error('@Component cannot define both `styleUrl` and `styleUrls`. ' + 'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple'); + } else if (component.styleUrls?.length) { + const styleOffset = component.styles.length; + const styleUrls = component.styleUrls; + component.styleUrls.forEach((styleUrl, index) => { + styles.push(''); // pre-allocate array. + promises.push(cachedResourceResolve(styleUrl).then(style => { + styles[styleOffset + index] = style; + styleUrls.splice(styleUrls.indexOf(styleUrl), 1); + if (styleUrls.length == 0) { + component.styleUrls = undefined; + } + })); + }); + } else if (component.styleUrl) { + promises.push(cachedResourceResolve(component.styleUrl).then(style => { + styles.push(style); + component.styleUrl = undefined; + })); } - /** @nocollapse */ - static #_ = this.ɵprov = ɵɵdefineInjectable({ - token: AfterRenderEventManager, - providedIn: 'root', - factory: () => new AfterRenderEventManager() - }); + const fullyResolved = Promise.all(promises).then(() => componentDefResolved(type)); + componentResolved.push(fullyResolved); + }); + clearResolutionOfComponentResourcesQueue(); + return Promise.all(componentResolved).then(() => undefined); +} +let componentResourceResolutionQueue = /*#__PURE__*/new Map(); +// Track when existing ɵcmp for a Type is waiting on resources. +const componentDefPendingResolution = /*#__PURE__*/new Set(); +function maybeQueueResolutionOfComponentResources(type, metadata) { + if (componentNeedsResolution(metadata)) { + componentResourceResolutionQueue.set(type, metadata); + componentDefPendingResolution.add(type); } - return AfterRenderEventManager; -})(); -function isModuleWithProviders(value) { - return value.ngModule !== undefined; } -function isNgModule(value) { - return !!getNgModuleDef(value); +function isComponentDefPendingResolution(type) { + return componentDefPendingResolution.has(type); } -function isPipe(value) { - return !!getPipeDef$1(value); +function componentNeedsResolution(component) { + return !!(component.templateUrl && !component.hasOwnProperty('template') || component.styleUrls && component.styleUrls.length || component.styleUrl); } -function isDirective(value) { - return !!getDirectiveDef(value); +function clearResolutionOfComponentResourcesQueue() { + const old = componentResourceResolutionQueue; + componentResourceResolutionQueue = new Map(); + return old; } -function isComponent(value) { - return !!getComponentDef(value); +function restoreComponentResolutionQueue(queue) { + componentDefPendingResolution.clear(); + queue.forEach((_, type) => componentDefPendingResolution.add(type)); + componentResourceResolutionQueue = queue; } -function getDependencyTypeForError(type) { - if (getComponentDef(type)) return 'component'; - if (getDirectiveDef(type)) return 'directive'; - if (getPipeDef$1(type)) return 'pipe'; - return 'type'; +function isComponentResourceResolutionQueueEmpty() { + return componentResourceResolutionQueue.size === 0; } -function verifyStandaloneImport(depType, importingType) { - if (isForwardRef(depType)) { - depType = resolveForwardRef(depType); - if (!depType) { - throw new Error(`Expected forwardRef function, imported from "${stringifyForError(importingType)}", to return a standalone entity or NgModule but got "${stringifyForError(depType) || depType}".`); - } - } - if (getNgModuleDef(depType) == null) { - const def = getComponentDef(depType) || getDirectiveDef(depType) || getPipeDef$1(depType); - if (def != null) { - // if a component, directive or pipe is imported make sure that it is standalone - if (!def.standalone) { - throw new Error(`The "${stringifyForError(depType)}" ${getDependencyTypeForError(depType)}, imported from "${stringifyForError(importingType)}", is not standalone. Did you forget to add the standalone: true flag?`); - } - } else { - // it can be either a module with provider or an unknown (not annotated) type - if (isModuleWithProviders(depType)) { - throw new Error(`A module with providers was imported from "${stringifyForError(importingType)}". Modules with providers are not supported in standalone components imports.`); - } else { - throw new Error(`The "${stringifyForError(depType)}" type, imported from "${stringifyForError(importingType)}", must be a standalone component / directive / pipe or an NgModule. Did you forget to add the required @Component / @Directive / @Pipe or @NgModule annotation?`); - } - } - } +function unwrapResponse(response) { + return typeof response == 'string' ? response : response.text(); +} +function componentDefResolved(type) { + componentDefPendingResolution.delete(type); } /** - * Indicates whether to use the runtime dependency tracker for scope calculation in JIT compilation. - * The value "false" means the old code path based on patching scope info into the types will be - * used. - * - * @deprecated For migration purposes only, to be removed soon. + * Map of module-id to the corresponding NgModule. */ -const USE_RUNTIME_DEPS_TRACKER_FOR_JIT = true; +const modules = /*#__PURE__*/new Map(); /** - * An implementation of DepsTrackerApi which will be used for JIT and local compilation. + * Whether to check for duplicate NgModule registrations. + * + * This can be disabled for testing. */ -class DepsTracker { - constructor() { - this.ownerNgModule = new Map(); - this.ngModulesWithSomeUnresolvedDecls = new Set(); - this.ngModulesScopeCache = new Map(); - this.standaloneComponentsScopeCache = new Map(); - } - /** - * Attempts to resolve ng module's forward ref declarations as much as possible and add them to - * the `ownerNgModule` map. This method normally should be called after the initial parsing when - * all the forward refs are resolved (e.g., when trying to render a component) - */ - resolveNgModulesDecls() { - if (this.ngModulesWithSomeUnresolvedDecls.size === 0) { - return; - } - for (const moduleType of this.ngModulesWithSomeUnresolvedDecls) { - const def = getNgModuleDef(moduleType); - if (def?.declarations) { - for (const decl of maybeUnwrapFn(def.declarations)) { - if (isComponent(decl)) { - this.ownerNgModule.set(decl, moduleType); - } - } - } - } - this.ngModulesWithSomeUnresolvedDecls.clear(); - } - /** @override */ - getComponentDependencies(type, rawImports) { - this.resolveNgModulesDecls(); - const def = getComponentDef(type); - if (def === null) { - throw new Error(`Attempting to get component dependencies for a type that is not a component: ${type}`); - } - if (def.standalone) { - const scope = this.getStandaloneComponentScope(type, rawImports); - if (scope.compilation.isPoisoned) { - return { - dependencies: [] - }; - } - return { - dependencies: [...scope.compilation.directives, ...scope.compilation.pipes, ...scope.compilation.ngModules] - }; - } else { - if (!this.ownerNgModule.has(type)) { - // This component is orphan! No need to handle the error since the component rendering - // pipeline (e.g., view_container_ref) will check for this error based on configs. - return { - dependencies: [] - }; - } - const scope = this.getNgModuleScope(this.ownerNgModule.get(type)); - if (scope.compilation.isPoisoned) { - return { - dependencies: [] - }; - } - return { - dependencies: [...scope.compilation.directives, ...scope.compilation.pipes] - }; - } - } - /** - * @override - * This implementation does not make use of param scopeInfo since it assumes the scope info is - * already added to the type itself through methods like {@link ɵɵsetNgModuleScope} - */ - registerNgModule(type, scopeInfo) { - if (!isNgModule(type)) { - throw new Error(`Attempting to register a Type which is not NgModule as NgModule: ${type}`); - } - // Lazily process the NgModules later when needed. - this.ngModulesWithSomeUnresolvedDecls.add(type); - } - /** @override */ - clearScopeCacheFor(type) { - this.ngModulesScopeCache.delete(type); - this.standaloneComponentsScopeCache.delete(type); - } - /** @override */ - getNgModuleScope(type) { - if (this.ngModulesScopeCache.has(type)) { - return this.ngModulesScopeCache.get(type); - } - const scope = this.computeNgModuleScope(type); - this.ngModulesScopeCache.set(type, scope); - return scope; - } - /** Compute NgModule scope afresh. */ - computeNgModuleScope(type) { - const def = getNgModuleDef(type, true); - const scope = { - exported: { - directives: new Set(), - pipes: new Set() - }, - compilation: { - directives: new Set(), - pipes: new Set() - } - }; - // Analyzing imports - for (const imported of maybeUnwrapFn(def.imports)) { - if (isNgModule(imported)) { - const importedScope = this.getNgModuleScope(imported); - // When this module imports another, the imported module's exported directives and pipes - // are added to the compilation scope of this module. - addSet(importedScope.exported.directives, scope.compilation.directives); - addSet(importedScope.exported.pipes, scope.compilation.pipes); - } else if (isStandalone(imported)) { - if (isDirective(imported) || isComponent(imported)) { - scope.compilation.directives.add(imported); - } else if (isPipe(imported)) { - scope.compilation.pipes.add(imported); - } else { - // The standalone thing is neither a component nor a directive nor a pipe ... (what?) - throw new RuntimeError(1000 /* RuntimeErrorCode.RUNTIME_DEPS_INVALID_IMPORTED_TYPE */, 'The standalone imported type is neither a component nor a directive nor a pipe'); - } - } else { - // The import is neither a module nor a module-with-providers nor a standalone thing. This - // is going to be an error. So we short circuit. - scope.compilation.isPoisoned = true; - break; - } - } - // Analyzing declarations - if (!scope.compilation.isPoisoned) { - for (const decl of maybeUnwrapFn(def.declarations)) { - // Cannot declare another NgModule or a standalone thing - if (isNgModule(decl) || isStandalone(decl)) { - scope.compilation.isPoisoned = true; - break; - } - if (isPipe(decl)) { - scope.compilation.pipes.add(decl); - } else { - // decl is either a directive or a component. The component may not yet have the ɵcmp due - // to async compilation. - scope.compilation.directives.add(decl); - } - } - } - // Analyzing exports - for (const exported of maybeUnwrapFn(def.exports)) { - if (isNgModule(exported)) { - // When this module exports another, the exported module's exported directives and pipes - // are added to both the compilation and exported scopes of this module. - const exportedScope = this.getNgModuleScope(exported); - // Based on the current logic there is no way to have poisoned exported scope. So no need to - // check for it. - addSet(exportedScope.exported.directives, scope.exported.directives); - addSet(exportedScope.exported.pipes, scope.exported.pipes); - // Some test toolings which run in JIT mode depend on this behavior that the exported scope - // should also be present in the compilation scope, even though AoT does not support this - // and it is also in odds with NgModule metadata definitions. Without this some tests in - // Google will fail. - addSet(exportedScope.exported.directives, scope.compilation.directives); - addSet(exportedScope.exported.pipes, scope.compilation.pipes); - } else if (isPipe(exported)) { - scope.exported.pipes.add(exported); - } else { - scope.exported.directives.add(exported); - } - } - return scope; - } - /** @override */ - getStandaloneComponentScope(type, rawImports) { - if (this.standaloneComponentsScopeCache.has(type)) { - return this.standaloneComponentsScopeCache.get(type); - } - const ans = this.computeStandaloneComponentScope(type, rawImports); - this.standaloneComponentsScopeCache.set(type, ans); - return ans; - } - computeStandaloneComponentScope(type, rawImports) { - const ans = { - compilation: { - // Standalone components are always able to self-reference. - directives: new Set([type]), - pipes: new Set(), - ngModules: new Set() - } - }; - for (const rawImport of flatten(rawImports ?? [])) { - const imported = resolveForwardRef(rawImport); - try { - verifyStandaloneImport(imported, type); - } catch (e) { - // Short-circuit if an import is not valid - ans.compilation.isPoisoned = true; - return ans; - } - if (isNgModule(imported)) { - ans.compilation.ngModules.add(imported); - const importedScope = this.getNgModuleScope(imported); - // Short-circuit if an imported NgModule has corrupted exported scope. - if (importedScope.exported.isPoisoned) { - ans.compilation.isPoisoned = true; - return ans; - } - addSet(importedScope.exported.directives, ans.compilation.directives); - addSet(importedScope.exported.pipes, ans.compilation.pipes); - } else if (isPipe(imported)) { - ans.compilation.pipes.add(imported); - } else if (isDirective(imported) || isComponent(imported)) { - ans.compilation.directives.add(imported); - } else { - // The imported thing is not module/pipe/directive/component, so we error and short-circuit - // here - ans.compilation.isPoisoned = true; - return ans; - } - } - return ans; - } - /** @override */ - isOrphanComponent(cmp) { - const def = getComponentDef(cmp); - if (!def || def.standalone) { - return false; - } - this.resolveNgModulesDecls(); - return !this.ownerNgModule.has(cmp); +let checkForDuplicateNgModules = true; +function assertSameOrNotExisting(id, type, incoming) { + if (type && type !== incoming && checkForDuplicateNgModules) { + throw new Error(`Duplicate module registered for ${id} - ${stringify(type)} vs ${stringify(type.name)}`); } } -function addSet(sourceSet, targetSet) { - for (const m of sourceSet) { - targetSet.add(m); - } +/** + * Adds the given NgModule type to Angular's NgModule registry. + * + * This is generated as a side-effect of NgModule compilation. Note that the `id` is passed in + * explicitly and not read from the NgModule definition. This is for two reasons: it avoids a + * megamorphic read, and in JIT there's a chicken-and-egg problem where the NgModule may not be + * fully resolved when it's registered. + * + * @codeGenApi + */ +function registerNgModuleType(ngModuleType, id) { + const existing = modules.get(id) || null; + assertSameOrNotExisting(id, existing, ngModuleType); + modules.set(id, ngModuleType); +} +function clearModulesForTest() { + modules.clear(); +} +function getRegisteredNgModuleType(id) { + return modules.get(id); +} +/** + * Control whether the NgModule registration system enforces that each NgModule type registered has + * a unique id. + * + * This is useful for testing as the NgModule registry cannot be properly reset between tests with + * Angular's current API. + */ +function setAllowDuplicateNgModuleIdsForTest(allowDuplicates) { + checkForDuplicateNgModules = !allowDuplicates; } -/** The deps tracker to be used in the current Angular app in dev mode. */ -const depsTracker = /*#__PURE__*/new DepsTracker(); -const TEST_ONLY = { - DepsTracker -}; /** - * Compute the static styling (class/style) from `TAttributes`. + * Validation function invoked at runtime for each binding that might potentially + * represent a security-sensitive attribute of an