/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { hasInSkipHydrationBlockFlag } from '../hydration/skip_hydration'; import { ViewEncapsulation } from '../metadata/view'; import { RendererStyleFlags2 } from '../render/api_flags'; import { consumerDestroy } from '../signals'; import { addToArray, removeFromArray } from '../util/array_utils'; import { assertDefined, assertEqual, assertFunction, assertNumber, assertString } from '../util/assert'; import { escapeCommentText } from '../util/dom'; import { assertLContainer, assertLView, assertParentView, assertProjectionSlots, assertTNodeForLView } from './assert'; import { attachPatchData } from './context_discovery'; import { icuContainerIterate } from './i18n/i18n_tree_shaking'; import { CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, MOVED_VIEWS, NATIVE } from './interfaces/container'; import { NodeInjectorFactory } from './interfaces/injector'; import { unregisterLView } from './interfaces/lview_tracking'; import { isLContainer, isLView } from './interfaces/type_checks'; import { CHILD_HEAD, CLEANUP, DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, FLAGS, HOST, NEXT, ON_DESTROY_HOOKS, PARENT, QUERIES, REACTIVE_HOST_BINDING_CONSUMER, REACTIVE_TEMPLATE_CONSUMER, RENDERER, T_HOST, TVIEW } from './interfaces/view'; import { assertTNodeType } from './node_assert'; import { profiler } from './profiler'; import { setUpAttributes } from './util/attrs_utils'; import { getLViewParent } from './util/view_traversal_utils'; import { clearViewRefreshFlag, getNativeByTNode, unwrapRNode } from './util/view_utils'; /** * NOTE: for performance reasons, the possible actions are inlined within the function instead of * being passed as an argument. */ function applyToElementOrContainer(action, renderer, parent, lNodeToHandle, beforeNode) { // If this slot was allocated for a text node dynamically created by i18n, the text node itself // won't be created until i18nApply() in the update block, so this node should be skipped. // For more info, see "ICU expressions should work inside an ngTemplateOutlet inside an ngFor" // in `i18n_spec.ts`. if (lNodeToHandle != null) { let lContainer; let isComponent = false; // We are expecting an RNode, but in the case of a component or LContainer the `RNode` is // wrapped in an array which needs to be unwrapped. We need to know if it is a component and if // it has LContainer so that we can process all of those cases appropriately. if (isLContainer(lNodeToHandle)) { lContainer = lNodeToHandle; } else if (isLView(lNodeToHandle)) { isComponent = true; ngDevMode && assertDefined(lNodeToHandle[HOST], 'HOST must be defined for a component LView'); lNodeToHandle = lNodeToHandle[HOST]; } const rNode = unwrapRNode(lNodeToHandle); if (action === 0 /* WalkTNodeTreeAction.Create */ && parent !== null) { if (beforeNode == null) { nativeAppendChild(renderer, parent, rNode); } else { nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true); } } else if (action === 1 /* WalkTNodeTreeAction.Insert */ && parent !== null) { nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true); } else if (action === 2 /* WalkTNodeTreeAction.Detach */) { nativeRemoveNode(renderer, rNode, isComponent); } else if (action === 3 /* WalkTNodeTreeAction.Destroy */) { ngDevMode && ngDevMode.rendererDestroyNode++; renderer.destroyNode(rNode); } if (lContainer != null) { applyContainer(renderer, action, lContainer, parent, beforeNode); } } } export function createTextNode(renderer, value) { ngDevMode && ngDevMode.rendererCreateTextNode++; ngDevMode && ngDevMode.rendererSetText++; return renderer.createText(value); } export function updateTextNode(renderer, rNode, value) { ngDevMode && ngDevMode.rendererSetText++; renderer.setValue(rNode, value); } export function createCommentNode(renderer, value) { ngDevMode && ngDevMode.rendererCreateComment++; return renderer.createComment(escapeCommentText(value)); } /** * Creates a native element from a tag name, using a renderer. * @param renderer A renderer to use * @param name the tag name * @param namespace Optional namespace for element. * @returns the element created */ export function createElementNode(renderer, name, namespace) { ngDevMode && ngDevMode.rendererCreateElement++; return renderer.createElement(name, namespace); } /** * Removes all DOM elements associated with a view. * * Because some root nodes of the view may be containers, we sometimes need * to propagate deeply into the nested containers to remove all elements in the * views beneath it. * * @param tView The `TView' of the `LView` from which elements should be added or removed * @param lView The view from which elements should be added or removed */ export function removeViewFromDOM(tView, lView) { const renderer = lView[RENDERER]; applyView(tView, lView, renderer, 2 /* WalkTNodeTreeAction.Detach */, null, null); lView[HOST] = null; lView[T_HOST] = null; } /** * Adds all DOM elements associated with a view. * * Because some root nodes of the view may be containers, we sometimes need * to propagate deeply into the nested containers to add all elements in the * views beneath it. * * @param tView The `TView' of the `LView` from which elements should be added or removed * @param parentTNode The `TNode` where the `LView` should be attached to. * @param renderer Current renderer to use for DOM manipulations. * @param lView The view from which elements should be added or removed * @param parentNativeNode The parent `RElement` where it should be inserted into. * @param beforeNode The node before which elements should be added, if insert mode */ export function addViewToDOM(tView, parentTNode, renderer, lView, parentNativeNode, beforeNode) { lView[HOST] = parentNativeNode; lView[T_HOST] = parentTNode; applyView(tView, lView, renderer, 1 /* WalkTNodeTreeAction.Insert */, parentNativeNode, beforeNode); } /** * Detach a `LView` from the DOM by detaching its nodes. * * @param tView The `TView' of the `LView` to be detached * @param lView the `LView` to be detached. */ export function detachViewFromDOM(tView, lView) { applyView(tView, lView, lView[RENDERER], 2 /* WalkTNodeTreeAction.Detach */, null, null); } /** * Traverses down and up the tree of views and containers to remove listeners and * call onDestroy callbacks. * * Notes: * - Because it's used for onDestroy calls, it needs to be bottom-up. * - Must process containers instead of their views to avoid splicing * when views are destroyed and re-added. * - Using a while loop because it's faster than recursion * - Destroy only called on movement to sibling or movement to parent (laterally or up) * * @param rootView The view to destroy */ export function destroyViewTree(rootView) { // If the view has no children, we can clean it up and return early. let lViewOrLContainer = rootView[CHILD_HEAD]; if (!lViewOrLContainer) { return cleanUpView(rootView[TVIEW], rootView); } while (lViewOrLContainer) { let next = null; if (isLView(lViewOrLContainer)) { // If LView, traverse down to child. next = lViewOrLContainer[CHILD_HEAD]; } else { ngDevMode && assertLContainer(lViewOrLContainer); // If container, traverse down to its first LView. const firstView = lViewOrLContainer[CONTAINER_HEADER_OFFSET]; if (firstView) next = firstView; } if (!next) { // Only clean up view when moving to the side or up, as destroy hooks // should be called in order from the bottom up. while (lViewOrLContainer && !lViewOrLContainer[NEXT] && lViewOrLContainer !== rootView) { if (isLView(lViewOrLContainer)) { cleanUpView(lViewOrLContainer[TVIEW], lViewOrLContainer); } lViewOrLContainer = lViewOrLContainer[PARENT]; } if (lViewOrLContainer === null) lViewOrLContainer = rootView; if (isLView(lViewOrLContainer)) { cleanUpView(lViewOrLContainer[TVIEW], lViewOrLContainer); } next = lViewOrLContainer && lViewOrLContainer[NEXT]; } lViewOrLContainer = next; } } /** * Inserts a view into a container. * * This adds the view to the container's array of active views in the correct * position. It also adds the view's elements to the DOM if the container isn't a * root node of another view (in that case, the view's elements will be added when * the container's parent view is added later). * * @param tView The `TView' of the `LView` to insert * @param lView The view to insert * @param lContainer The container into which the view should be inserted * @param index Which index in the container to insert the child view into */ export function insertView(tView, lView, lContainer, index) { ngDevMode && assertLView(lView); ngDevMode && assertLContainer(lContainer); const indexInContainer = CONTAINER_HEADER_OFFSET + index; const containerLength = lContainer.length; if (index > 0) { // This is a new view, we need to add it to the children. lContainer[indexInContainer - 1][NEXT] = lView; } if (index < containerLength - CONTAINER_HEADER_OFFSET) { lView[NEXT] = lContainer[indexInContainer]; addToArray(lContainer, CONTAINER_HEADER_OFFSET + index, lView); } else { lContainer.push(lView); lView[NEXT] = null; } lView[PARENT] = lContainer; // track views where declaration and insertion points are different const declarationLContainer = lView[DECLARATION_LCONTAINER]; if (declarationLContainer !== null && lContainer !== declarationLContainer) { trackMovedView(declarationLContainer, lView); } // notify query that a new view has been added const lQueries = lView[QUERIES]; if (lQueries !== null) { lQueries.insertView(tView); } // Sets the attached flag lView[FLAGS] |= 128 /* LViewFlags.Attached */; } /** * Track views created from the declaration container (TemplateRef) and inserted into a * different LContainer. */ function trackMovedView(declarationContainer, lView) { ngDevMode && assertDefined(lView, 'LView required'); ngDevMode && assertLContainer(declarationContainer); const movedViews = declarationContainer[MOVED_VIEWS]; const insertedLContainer = lView[PARENT]; ngDevMode && assertLContainer(insertedLContainer); const insertedComponentLView = insertedLContainer[PARENT][DECLARATION_COMPONENT_VIEW]; ngDevMode && assertDefined(insertedComponentLView, 'Missing insertedComponentLView'); const declaredComponentLView = lView[DECLARATION_COMPONENT_VIEW]; ngDevMode && assertDefined(declaredComponentLView, 'Missing declaredComponentLView'); if (declaredComponentLView !== insertedComponentLView) { // At this point the declaration-component is not same as insertion-component; this means that // this is a transplanted view. Mark the declared lView as having transplanted views so that // those views can participate in CD. declarationContainer[HAS_TRANSPLANTED_VIEWS] = true; } if (movedViews === null) { declarationContainer[MOVED_VIEWS] = [lView]; } else { movedViews.push(lView); } } function detachMovedView(declarationContainer, lView) { ngDevMode && assertLContainer(declarationContainer); ngDevMode && assertDefined(declarationContainer[MOVED_VIEWS], 'A projected view should belong to a non-empty projected views collection'); const movedViews = declarationContainer[MOVED_VIEWS]; const declarationViewIndex = movedViews.indexOf(lView); const insertionLContainer = lView[PARENT]; ngDevMode && assertLContainer(insertionLContainer); // If the view was marked for refresh but then detached before it was checked (where the flag // would be cleared and the counter decremented), we need to update the status here. clearViewRefreshFlag(lView); movedViews.splice(declarationViewIndex, 1); } /** * Detaches a view from a container. * * This method removes the view from the container's array of active views. It also * removes the view's elements from the DOM. * * @param lContainer The container from which to detach a view * @param removeIndex The index of the view to detach * @returns Detached LView instance. */ export function detachView(lContainer, removeIndex) { if (lContainer.length <= CONTAINER_HEADER_OFFSET) return; const indexInContainer = CONTAINER_HEADER_OFFSET + removeIndex; const viewToDetach = lContainer[indexInContainer]; if (viewToDetach) { const declarationLContainer = viewToDetach[DECLARATION_LCONTAINER]; if (declarationLContainer !== null && declarationLContainer !== lContainer) { detachMovedView(declarationLContainer, viewToDetach); } if (removeIndex > 0) { lContainer[indexInContainer - 1][NEXT] = viewToDetach[NEXT]; } const removedLView = removeFromArray(lContainer, CONTAINER_HEADER_OFFSET + removeIndex); removeViewFromDOM(viewToDetach[TVIEW], viewToDetach); // notify query that a view has been removed const lQueries = removedLView[QUERIES]; if (lQueries !== null) { lQueries.detachView(removedLView[TVIEW]); } viewToDetach[PARENT] = null; viewToDetach[NEXT] = null; // Unsets the attached flag viewToDetach[FLAGS] &= ~128 /* LViewFlags.Attached */; } return viewToDetach; } /** * A standalone function which destroys an LView, * conducting clean up (e.g. removing listeners, calling onDestroys). * * @param tView The `TView' of the `LView` to be destroyed * @param lView The view to be destroyed. */ export function destroyLView(tView, lView) { if (!(lView[FLAGS] & 256 /* LViewFlags.Destroyed */)) { const renderer = lView[RENDERER]; lView[REACTIVE_TEMPLATE_CONSUMER] && consumerDestroy(lView[REACTIVE_TEMPLATE_CONSUMER]); lView[REACTIVE_HOST_BINDING_CONSUMER] && consumerDestroy(lView[REACTIVE_HOST_BINDING_CONSUMER]); if (renderer.destroyNode) { applyView(tView, lView, renderer, 3 /* WalkTNodeTreeAction.Destroy */, null, null); } destroyViewTree(lView); } } /** * Calls onDestroys hooks for all directives and pipes in a given view and then removes all * listeners. Listeners are removed as the last step so events delivered in the onDestroys hooks * can be propagated to @Output listeners. * * @param tView `TView` for the `LView` to clean up. * @param lView The LView to clean up */ function cleanUpView(tView, lView) { if (!(lView[FLAGS] & 256 /* LViewFlags.Destroyed */)) { // 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 */; // Mark the LView as destroyed *before* executing the onDestroy hooks. An onDestroy hook // runs arbitrary user code, which could include its own `viewRef.destroy()` (or similar). If // We don't flag the view as destroyed before the hooks, this could lead to an infinite loop. // This also aligns with the ViewEngine behavior. It also means that the onDestroy hook is // really more of an "afterDestroy" hook if you think about it. lView[FLAGS] |= 256 /* LViewFlags.Destroyed */; executeOnDestroys(tView, lView); processCleanups(tView, lView); // For component views only, the local renderer is destroyed at clean up time. if (lView[TVIEW].type === 1 /* TViewType.Component */) { ngDevMode && ngDevMode.rendererDestroy++; lView[RENDERER].destroy(); } const declarationContainer = lView[DECLARATION_LCONTAINER]; // we are dealing with an embedded view that is still inserted into a container if (declarationContainer !== null && isLContainer(lView[PARENT])) { // and this is a projected view if (declarationContainer !== lView[PARENT]) { detachMovedView(declarationContainer, lView); } // For embedded views still attached to a container: remove query result from this view. const lQueries = lView[QUERIES]; if (lQueries !== null) { lQueries.detachView(tView); } } // Unregister the view once everything else has been cleaned up. unregisterLView(lView); } } /** Removes listeners and unsubscribes from output subscriptions */ function processCleanups(tView, lView) { const tCleanup = tView.cleanup; const lCleanup = lView[CLEANUP]; if (tCleanup !== null) { for (let i = 0; i < tCleanup.length - 1; i += 2) { if (typeof tCleanup[i] === 'string') { // This is a native DOM listener. It will occupy 4 entries in the TCleanup array (hence i += // 2 at the end of this block). const targetIdx = tCleanup[i + 3]; ngDevMode && assertNumber(targetIdx, 'cleanup target must be a number'); if (targetIdx >= 0) { // unregister lCleanup[targetIdx](); } else { // Subscription lCleanup[-targetIdx].unsubscribe(); } i += 2; } else { // This is a cleanup function that is grouped with the index of its context const context = lCleanup[tCleanup[i + 1]]; tCleanup[i].call(context); } } } if (lCleanup !== null) { lView[CLEANUP] = null; } const destroyHooks = lView[ON_DESTROY_HOOKS]; if (destroyHooks !== null) { // Reset the ON_DESTROY_HOOKS array before iterating over it to prevent hooks that unregister // themselves from mutating the array during iteration. lView[ON_DESTROY_HOOKS] = null; for (let i = 0; i < destroyHooks.length; i++) { const destroyHooksFn = destroyHooks[i]; ngDevMode && assertFunction(destroyHooksFn, 'Expecting destroy hook to be a function.'); destroyHooksFn(); } } } /** Calls onDestroy hooks for this view */ function executeOnDestroys(tView, lView) { let destroyHooks; if (tView != null && (destroyHooks = tView.destroyHooks) != null) { for (let i = 0; i < destroyHooks.length; i += 2) { const context = lView[destroyHooks[i]]; // Only call the destroy hook if the context has been requested. if (!(context instanceof NodeInjectorFactory)) { const toCall = destroyHooks[i + 1]; if (Array.isArray(toCall)) { for (let j = 0; j < toCall.length; j += 2) { const callContext = context[toCall[j]]; const hook = toCall[j + 1]; profiler(4 /* ProfilerEvent.LifecycleHookStart */, callContext, hook); try { hook.call(callContext); } finally { profiler(5 /* ProfilerEvent.LifecycleHookEnd */, callContext, hook); } } } else { profiler(4 /* ProfilerEvent.LifecycleHookStart */, context, toCall); try { toCall.call(context); } finally { profiler(5 /* ProfilerEvent.LifecycleHookEnd */, context, toCall); } } } } } } /** * Returns a native element if a node can be inserted into the given parent. * * There are two reasons why we may not be able to insert a element immediately. * - Projection: When creating a child content element of a component, we have to skip the * insertion because the content of a component will be projected. * `delayed due to projection` * - Parent container is disconnected: This can happen when we are inserting a view into * parent container, which itself is disconnected. For example the parent container is part * of a View which has not be inserted or is made for projection but has not been inserted * into destination. * * @param tView: Current `TView`. * @param tNode: `TNode` for which we wish to retrieve render parent. * @param lView: Current `LView`. */ export function getParentRElement(tView, tNode, lView) { return getClosestRElement(tView, tNode.parent, lView); } /** * Get closest `RElement` or `null` if it can't be found. * * If `TNode` is `TNodeType.Element` => return `RElement` at `LView[tNode.index]` location. * If `TNode` is `TNodeType.ElementContainer|IcuContain` => return the parent (recursively). * If `TNode` is `null` then return host `RElement`: * - return `null` if projection * - return `null` if parent container is disconnected (we have no parent.) * * @param tView: Current `TView`. * @param tNode: `TNode` for which we wish to retrieve `RElement` (or `null` if host element is * needed). * @param lView: Current `LView`. * @returns `null` if the `RElement` can't be determined at this time (no parent / projection) */ export function getClosestRElement(tView, tNode, lView) { let parentTNode = tNode; // Skip over element and ICU containers as those are represented by a comment node and // can't be used as a render parent. while (parentTNode !== null && (parentTNode.type & (8 /* TNodeType.ElementContainer */ | 32 /* TNodeType.Icu */))) { tNode = parentTNode; parentTNode = tNode.parent; } // If the parent tNode is null, then we are inserting across views: either into an embedded view // or a component view. if (parentTNode === null) { // We are inserting a root element of the component view into the component host element and // it should always be eager. return lView[HOST]; } else { ngDevMode && assertTNodeType(parentTNode, 3 /* TNodeType.AnyRNode */ | 4 /* TNodeType.Container */); const { componentOffset } = parentTNode; if (componentOffset > -1) { ngDevMode && assertTNodeForLView(parentTNode, lView); const { encapsulation } = tView.data[parentTNode.directiveStart + componentOffset]; // We've got a parent which is an element in the current view. We just need to verify if the // parent element is not a component. Component's content nodes are not inserted immediately // because they will be projected, and so doing insert at this point would be wasteful. // Since the projection would then move it to its final destination. Note that we can't // make this assumption when using the Shadow DOM, because the native projection placeholders // ( or ) have to be in place as elements are being inserted. if (encapsulation === ViewEncapsulation.None || encapsulation === ViewEncapsulation.Emulated) { return null; } } return getNativeByTNode(parentTNode, lView); } } /** * Inserts a native node before another native node for a given parent. * This is a utility function that can be used when native nodes were determined. */ export function nativeInsertBefore(renderer, parent, child, beforeNode, isMove) { ngDevMode && ngDevMode.rendererInsertBefore++; renderer.insertBefore(parent, child, beforeNode, isMove); } function nativeAppendChild(renderer, parent, child) { ngDevMode && ngDevMode.rendererAppendChild++; ngDevMode && assertDefined(parent, 'parent node must be defined'); renderer.appendChild(parent, child); } function nativeAppendOrInsertBefore(renderer, parent, child, beforeNode, isMove) { if (beforeNode !== null) { nativeInsertBefore(renderer, parent, child, beforeNode, isMove); } else { nativeAppendChild(renderer, parent, child); } } /** Removes a node from the DOM given its native parent. */ function nativeRemoveChild(renderer, parent, child, isHostElement) { renderer.removeChild(parent, child, isHostElement); } /** Checks if an element is a `