UNPKG

@angular/core

Version:

Angular - the core framework

1,047 lines 206 kB
/** * @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 { setActiveConsumer } from '@angular/core/primitives/signals'; import { ErrorHandler } from '../../error_handler'; import { RuntimeError } from '../../errors'; import { hasSkipHydrationAttrOnRElement } from '../../hydration/skip_hydration'; import { PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT } from '../../hydration/tokens'; import { processTextNodeMarkersBeforeHydration } from '../../hydration/utils'; import { ViewEncapsulation } from '../../metadata/view'; import { validateAgainstEventAttributes, validateAgainstEventProperties } from '../../sanitization/sanitization'; import { assertDefined, assertEqual, assertGreaterThan, assertGreaterThanOrEqual, assertIndexInRange, assertNotEqual, assertNotSame, assertSame, assertString } from '../../util/assert'; import { escapeCommentText } from '../../util/dom'; import { normalizeDebugBindingName, normalizeDebugBindingValue } from '../../util/ng_reflect'; import { stringify } from '../../util/stringify'; import { assertFirstCreatePass, assertFirstUpdatePass, assertLView, assertNoDuplicateDirectives, assertTNodeForLView, assertTNodeForTView } from '../assert'; import { attachPatchData } from '../context_discovery'; import { getFactoryDef } from '../definition_factory'; import { diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode } from '../di'; import { throwMultipleComponentError } from '../errors'; import { CONTAINER_HEADER_OFFSET } from '../interfaces/container'; import { NodeInjectorFactory } from '../interfaces/injector'; import { getUniqueLViewId } from '../interfaces/lview_tracking'; import { isComponentDef, isComponentHost, isContentQueryHost } from '../interfaces/type_checks'; import { CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, ENVIRONMENT, FLAGS, HEADER_OFFSET, HOST, HYDRATION, ID, INJECTOR, NEXT, PARENT, RENDERER, T_HOST, TVIEW } from '../interfaces/view'; import { assertPureTNodeType, assertTNodeType } from '../node_assert'; import { clearElementContents, updateTextNode } from '../node_manipulation'; import { isInlineTemplate, isNodeMatchingSelectorList } from '../node_selector_matcher'; import { profiler } from '../profiler'; import { getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex } from '../state'; import { NO_CHANGE } from '../tokens'; import { mergeHostAttrs } from '../util/attrs_utils'; import { INTERPOLATION_DELIMITER } from '../util/misc_utils'; import { renderStringify } from '../util/stringify_utils'; import { getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, resetPreOrderHookFlags, unwrapLView } from '../util/view_utils'; import { selectIndexInternal } from './advance'; import { ɵɵdirectiveInject } from './di'; import { handleUnknownPropertyError, isPropertyValid, matchingSchemas } from './element_validation'; /** * Invoke `HostBindingsFunction`s for view. * * This methods executes `TView.hostBindingOpCodes`. It is used to execute the * `HostBindingsFunction`s associated with the current `LView`. * * @param tView Current `TView`. * @param lView Current `LView`. */ export function processHostBindingOpCodes(tView, lView) { const hostBindingOpCodes = tView.hostBindingOpCodes; if (hostBindingOpCodes === null) return; try { for (let i = 0; i < hostBindingOpCodes.length; i++) { const opCode = hostBindingOpCodes[i]; if (opCode < 0) { // Negative numbers are element indexes. setSelectedIndex(~opCode); } else { // Positive numbers are NumberTuple which store bindingRootIndex and directiveIndex. const directiveIdx = opCode; const bindingRootIndx = hostBindingOpCodes[++i]; const hostBindingFn = hostBindingOpCodes[++i]; setBindingRootForHostBindings(bindingRootIndx, directiveIdx); const context = lView[directiveIdx]; hostBindingFn(2 /* RenderFlags.Update */, context); } } } finally { setSelectedIndex(-1); } } export function createLView(parentLView, tView, context, flags, host, tHostNode, environment, renderer, injector, embeddedViewInjector, hydrationInfo) { const lView = tView.blueprint.slice(); lView[HOST] = host; lView[FLAGS] = flags | 4 /* LViewFlags.CreationMode */ | 128 /* LViewFlags.Attached */ | 8 /* LViewFlags.FirstLViewPass */; if (embeddedViewInjector !== null || (parentLView && (parentLView[FLAGS] & 2048 /* LViewFlags.HasEmbeddedViewInjector */))) { lView[FLAGS] |= 2048 /* LViewFlags.HasEmbeddedViewInjector */; } resetPreOrderHookFlags(lView); ngDevMode && tView.declTNode && parentLView && assertTNodeForLView(tView.declTNode, parentLView); lView[PARENT] = lView[DECLARATION_VIEW] = parentLView; lView[CONTEXT] = context; lView[ENVIRONMENT] = (environment || parentLView && parentLView[ENVIRONMENT]); ngDevMode && assertDefined(lView[ENVIRONMENT], 'LViewEnvironment is required'); lView[RENDERER] = (renderer || parentLView && parentLView[RENDERER]); ngDevMode && assertDefined(lView[RENDERER], 'Renderer is required'); lView[INJECTOR] = injector || parentLView && parentLView[INJECTOR] || null; lView[T_HOST] = tHostNode; lView[ID] = getUniqueLViewId(); lView[HYDRATION] = hydrationInfo; lView[EMBEDDED_VIEW_INJECTOR] = embeddedViewInjector; ngDevMode && assertEqual(tView.type == 2 /* TViewType.Embedded */ ? parentLView !== null : true, true, 'Embedded views must have parentLView'); lView[DECLARATION_COMPONENT_VIEW] = tView.type == 2 /* TViewType.Embedded */ ? parentLView[DECLARATION_COMPONENT_VIEW] : lView; return lView; } export function getOrCreateTNode(tView, index, type, name, attrs) { ngDevMode && index !== 0 && // 0 are bogus nodes and they are OK. See `createContainerRef` in // `view_engine_compatibility` for additional context. assertGreaterThanOrEqual(index, HEADER_OFFSET, 'TNodes can\'t be in the LView header.'); // Keep this function short, so that the VM will inline it. ngDevMode && assertPureTNodeType(type); let tNode = tView.data[index]; if (tNode === null) { tNode = createTNodeAtIndex(tView, index, type, name, attrs); if (isInI18nBlock()) { // If we are in i18n block then all elements should be pre declared through `Placeholder` // See `TNodeType.Placeholder` and `LFrame.inI18n` for more context. // If the `TNode` was not pre-declared than it means it was not mentioned which means it was // removed, so we mark it as detached. tNode.flags |= 32 /* TNodeFlags.isDetached */; } } else if (tNode.type & 64 /* TNodeType.Placeholder */) { tNode.type = type; tNode.value = name; tNode.attrs = attrs; const parent = getCurrentParentTNode(); tNode.injectorIndex = parent === null ? -1 : parent.injectorIndex; ngDevMode && assertTNodeForTView(tNode, tView); ngDevMode && assertEqual(index, tNode.index, 'Expecting same index'); } setCurrentTNode(tNode, true); return tNode; } export function createTNodeAtIndex(tView, index, type, name, attrs) { const currentTNode = getCurrentTNodePlaceholderOk(); const isParent = isCurrentTNodeParent(); const parent = isParent ? currentTNode : currentTNode && currentTNode.parent; // Parents cannot cross component boundaries because components will be used in multiple places. const tNode = tView.data[index] = createTNode(tView, parent, type, index, name, attrs); // Assign a pointer to the first child node of a given view. The first node is not always the one // at index 0, in case of i18n, index 0 can be the instruction `i18nStart` and the first node has // the index 1 or more, so we can't just check node index. if (tView.firstChild === null) { tView.firstChild = tNode; } if (currentTNode !== null) { if (isParent) { // FIXME(misko): This logic looks unnecessarily complicated. Could we simplify? if (currentTNode.child == null && tNode.parent !== null) { // We are in the same view, which means we are adding content node to the parent view. currentTNode.child = tNode; } } else { if (currentTNode.next === null) { // In the case of i18n the `currentTNode` may already be linked, in which case we don't want // to break the links which i18n created. currentTNode.next = tNode; tNode.prev = currentTNode; } } } return tNode; } /** * When elements are created dynamically after a view blueprint is created (e.g. through * i18nApply()), we need to adjust the blueprint for future * template passes. * * @param tView `TView` associated with `LView` * @param lView The `LView` containing the blueprint to adjust * @param numSlotsToAlloc The number of slots to alloc in the LView, should be >0 * @param initialValue Initial value to store in blueprint */ export function allocExpando(tView, lView, numSlotsToAlloc, initialValue) { if (numSlotsToAlloc === 0) return -1; if (ngDevMode) { assertFirstCreatePass(tView); assertSame(tView, lView[TVIEW], '`LView` must be associated with `TView`!'); assertEqual(tView.data.length, lView.length, 'Expecting LView to be same size as TView'); assertEqual(tView.data.length, tView.blueprint.length, 'Expecting Blueprint to be same size as TView'); assertFirstUpdatePass(tView); } const allocIdx = lView.length; for (let i = 0; i < numSlotsToAlloc; i++) { lView.push(initialValue); tView.blueprint.push(initialValue); tView.data.push(null); } return allocIdx; } export function executeTemplate(tView, lView, templateFn, rf, context) { const prevSelectedIndex = getSelectedIndex(); const isUpdatePhase = rf & 2 /* RenderFlags.Update */; try { setSelectedIndex(-1); if (isUpdatePhase && lView.length > HEADER_OFFSET) { // When we're updating, inherently select 0 so we don't // have to generate that instruction for most update blocks. selectIndexInternal(tView, lView, HEADER_OFFSET, !!ngDevMode && isInCheckNoChangesMode()); } const preHookType = isUpdatePhase ? 2 /* ProfilerEvent.TemplateUpdateStart */ : 0 /* ProfilerEvent.TemplateCreateStart */; profiler(preHookType, context); templateFn(rf, context); } finally { setSelectedIndex(prevSelectedIndex); const postHookType = isUpdatePhase ? 3 /* ProfilerEvent.TemplateUpdateEnd */ : 1 /* ProfilerEvent.TemplateCreateEnd */; profiler(postHookType, context); } } ////////////////////////// //// Element ////////////////////////// export function executeContentQueries(tView, tNode, lView) { if (isContentQueryHost(tNode)) { const prevConsumer = setActiveConsumer(null); try { const start = tNode.directiveStart; const end = tNode.directiveEnd; for (let directiveIndex = start; directiveIndex < end; directiveIndex++) { const def = tView.data[directiveIndex]; if (def.contentQueries) { def.contentQueries(1 /* RenderFlags.Create */, lView[directiveIndex], directiveIndex); } } } finally { setActiveConsumer(prevConsumer); } } } /** * Creates directive instances. */ export function createDirectivesInstances(tView, lView, tNode) { if (!getBindingsEnabled()) return; instantiateAllDirectives(tView, lView, tNode, getNativeByTNode(tNode, lView)); if ((tNode.flags & 64 /* TNodeFlags.hasHostBindings */) === 64 /* TNodeFlags.hasHostBindings */) { invokeDirectivesHostBindings(tView, lView, tNode); } } /** * Takes a list of local names and indices and pushes the resolved local variable values * to LView in the same order as they are loaded in the template with load(). */ export function saveResolvedLocalsInData(viewData, tNode, localRefExtractor = getNativeByTNode) { const localNames = tNode.localNames; if (localNames !== null) { let localIndex = tNode.index + 1; for (let i = 0; i < localNames.length; i += 2) { const index = localNames[i + 1]; const value = index === -1 ? localRefExtractor(tNode, viewData) : viewData[index]; viewData[localIndex++] = value; } } } /** * Gets TView from a template function or creates a new TView * if it doesn't already exist. * * @param def ComponentDef * @returns TView */ export function getOrCreateComponentTView(def) { const tView = def.tView; // Create a TView if there isn't one, or recreate it if the first create pass didn't // complete successfully since we can't know for sure whether it's in a usable shape. if (tView === null || tView.incompleteFirstPass) { // Declaration node here is null since this function is called when we dynamically create a // component and hence there is no declaration. const declTNode = null; return def.tView = createTView(1 /* TViewType.Component */, declTNode, def.template, def.decls, def.vars, def.directiveDefs, def.pipeDefs, def.viewQuery, def.schemas, def.consts, def.id); } return tView; } /** * Creates a TView instance * * @param type Type of `TView`. * @param declTNode Declaration location of this `TView`. * @param templateFn Template function * @param decls The number of nodes, local refs, and pipes in this template * @param directives Registry of directives for this view * @param pipes Registry of pipes for this view * @param viewQuery View queries for this view * @param schemas Schemas for this view * @param consts Constants for this view */ export function createTView(type, declTNode, templateFn, decls, vars, directives, pipes, viewQuery, schemas, constsOrFactory, ssrId) { ngDevMode && ngDevMode.tView++; const bindingStartIndex = HEADER_OFFSET + decls; // This length does not yet contain host bindings from child directives because at this point, // we don't know which directives are active on this template. As soon as a directive is matched // that has a host binding, we will update the blueprint with that def's hostVars count. const initialViewLength = bindingStartIndex + vars; const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength); const consts = typeof constsOrFactory === 'function' ? constsOrFactory() : constsOrFactory; const tView = blueprint[TVIEW] = { type: type, blueprint: blueprint, template: templateFn, queries: null, viewQuery: viewQuery, declTNode: declTNode, data: blueprint.slice().fill(null, bindingStartIndex), bindingStartIndex: bindingStartIndex, expandoStartIndex: initialViewLength, hostBindingOpCodes: null, firstCreatePass: true, firstUpdatePass: true, staticViewQueries: false, staticContentQueries: false, preOrderHooks: null, preOrderCheckHooks: null, contentHooks: null, contentCheckHooks: null, viewHooks: null, viewCheckHooks: null, destroyHooks: null, cleanup: null, contentQueries: null, components: null, directiveRegistry: typeof directives === 'function' ? directives() : directives, pipeRegistry: typeof pipes === 'function' ? pipes() : pipes, firstChild: null, schemas: schemas, consts: consts, incompleteFirstPass: false, ssrId, }; if (ngDevMode) { // For performance reasons it is important that the tView retains the same shape during runtime. // (To make sure that all of the code is monomorphic.) For this reason we seal the object to // prevent class transitions. Object.seal(tView); } return tView; } function createViewBlueprint(bindingStartIndex, initialViewLength) { const blueprint = []; for (let i = 0; i < initialViewLength; i++) { blueprint.push(i < bindingStartIndex ? null : NO_CHANGE); } return blueprint; } /** * Locates the host native element, used for bootstrapping existing nodes into rendering pipeline. * * @param renderer the renderer used to locate the element. * @param elementOrSelector Render element or CSS selector to locate the element. * @param encapsulation View Encapsulation defined for component that requests host element. * @param injector Root view injector instance. */ export function locateHostElement(renderer, elementOrSelector, encapsulation, injector) { // Note: we use default value for the `PRESERVE_HOST_CONTENT` here even though it's a // tree-shakable one (providedIn:'root'). This code path can be triggered during dynamic // component creation (after calling ViewContainerRef.createComponent) when an injector // instance can be provided. The injector instance might be disconnected from the main DI // tree, thus the `PRESERVE_HOST_CONTENT` would not be able to instantiate. In this case, the // default value will be used. const preserveHostContent = injector.get(PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT); // When using native Shadow DOM, do not clear host element to allow native slot // projection. const preserveContent = preserveHostContent || encapsulation === ViewEncapsulation.ShadowDom; const rootElement = renderer.selectRootElement(elementOrSelector, preserveContent); applyRootElementTransform(rootElement); return rootElement; } /** * Applies any root element transformations that are needed. If hydration is enabled, * this will process corrupted text nodes. * * @param rootElement the app root HTML Element */ export function applyRootElementTransform(rootElement) { _applyRootElementTransformImpl(rootElement); } /** * Reference to a function that applies transformations to the root HTML element * of an app. When hydration is enabled, this processes any corrupt text nodes * so they are properly hydratable on the client. * * @param rootElement the app root HTML Element */ let _applyRootElementTransformImpl = (rootElement) => null; /** * Processes text node markers before hydration begins. This replaces any special comment * nodes that were added prior to serialization are swapped out to restore proper text * nodes before hydration. * * @param rootElement the app root HTML Element */ export function applyRootElementTransformImpl(rootElement) { if (hasSkipHydrationAttrOnRElement(rootElement)) { // Handle a situation when the `ngSkipHydration` attribute is applied // to the root node of an application. In this case, we should clear // the contents and render everything from scratch. clearElementContents(rootElement); } else { processTextNodeMarkersBeforeHydration(rootElement); } } /** * Sets the implementation for the `applyRootElementTransform` function. */ export function enableApplyRootElementTransformImpl() { _applyRootElementTransformImpl = applyRootElementTransformImpl; } /** * Saves context for this cleanup function in LView.cleanupInstances. * * On the first template pass, saves in TView: * - Cleanup function * - Index of context we just saved in LView.cleanupInstances */ export function storeCleanupWithContext(tView, lView, context, cleanupFn) { const lCleanup = getOrCreateLViewCleanup(lView); // Historically the `storeCleanupWithContext` was used to register both framework-level and // user-defined cleanup callbacks, but over time those two types of cleanups were separated. // This dev mode checks assures that user-level cleanup callbacks are _not_ stored in data // structures reserved for framework-specific hooks. ngDevMode && assertDefined(context, 'Cleanup context is mandatory when registering framework-level destroy hooks'); lCleanup.push(context); if (tView.firstCreatePass) { getOrCreateTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1); } else { // Make sure that no new framework-level cleanup functions are registered after the first // template pass is done (and TView data structures are meant to fully constructed). if (ngDevMode) { Object.freeze(getOrCreateTViewCleanup(tView)); } } } export function createTNode(tView, tParent, type, index, value, attrs) { ngDevMode && index !== 0 && // 0 are bogus nodes and they are OK. See `createContainerRef` in // `view_engine_compatibility` for additional context. assertGreaterThanOrEqual(index, HEADER_OFFSET, 'TNodes can\'t be in the LView header.'); ngDevMode && assertNotSame(attrs, undefined, '\'undefined\' is not valid value for \'attrs\''); ngDevMode && ngDevMode.tNode++; ngDevMode && tParent && assertTNodeForTView(tParent, tView); let injectorIndex = tParent ? tParent.injectorIndex : -1; let flags = 0; if (isInSkipHydrationBlock()) { flags |= 128 /* TNodeFlags.inSkipHydrationBlock */; } const tNode = { type, index, insertBeforeIndex: null, injectorIndex, directiveStart: -1, directiveEnd: -1, directiveStylingLast: -1, componentOffset: -1, propertyBindings: null, flags, providerIndexes: 0, value: value, attrs: attrs, mergedAttrs: null, localNames: null, initialInputs: undefined, inputs: null, outputs: null, tView: null, next: null, prev: null, projectionNext: null, child: null, parent: tParent, projection: null, styles: null, stylesWithoutHost: null, residualStyles: undefined, classes: null, classesWithoutHost: null, residualClasses: undefined, classBindings: 0, styleBindings: 0, }; if (ngDevMode) { // For performance reasons it is important that the tNode retains the same shape during runtime. // (To make sure that all of the code is monomorphic.) For this reason we seal the object to // prevent class transitions. Object.seal(tNode); } return tNode; } /** * Generates the `PropertyAliases` data structure from the provided input/output mapping. * @param aliasMap Input/output mapping from the directive definition. * @param directiveIndex Index of the directive. * @param propertyAliases Object in which to store the results. * @param hostDirectiveAliasMap Object used to alias or filter out properties for host directives. * If the mapping is provided, it'll act as an allowlist, as well as a mapping of what public * name inputs/outputs should be exposed under. */ function generatePropertyAliases(aliasMap, directiveIndex, propertyAliases, hostDirectiveAliasMap) { for (let publicName in aliasMap) { if (aliasMap.hasOwnProperty(publicName)) { propertyAliases = propertyAliases === null ? {} : propertyAliases; const internalName = aliasMap[publicName]; // If there are no host directive mappings, we want to remap using the alias map from the // definition itself. If there is an alias map, it has two functions: // 1. It serves as an allowlist of bindings that are exposed by the host directives. Only the // ones inside the host directive map will be exposed on the host. // 2. The public name of the property is aliased using the host directive alias map, rather // than the alias map from the definition. if (hostDirectiveAliasMap === null) { addPropertyAlias(propertyAliases, directiveIndex, publicName, internalName); } else if (hostDirectiveAliasMap.hasOwnProperty(publicName)) { addPropertyAlias(propertyAliases, directiveIndex, hostDirectiveAliasMap[publicName], internalName); } } } return propertyAliases; } function addPropertyAlias(propertyAliases, directiveIndex, publicName, internalName) { if (propertyAliases.hasOwnProperty(publicName)) { propertyAliases[publicName].push(directiveIndex, internalName); } else { propertyAliases[publicName] = [directiveIndex, internalName]; } } /** * Initializes data structures required to work with directive inputs and outputs. * Initialization is done for all directives matched on a given TNode. */ function initializeInputAndOutputAliases(tView, tNode, hostDirectiveDefinitionMap) { ngDevMode && assertFirstCreatePass(tView); const start = tNode.directiveStart; const end = tNode.directiveEnd; const tViewData = tView.data; const tNodeAttrs = tNode.attrs; const inputsFromAttrs = []; let inputsStore = null; let outputsStore = null; for (let directiveIndex = start; directiveIndex < end; directiveIndex++) { const directiveDef = tViewData[directiveIndex]; const aliasData = hostDirectiveDefinitionMap ? hostDirectiveDefinitionMap.get(directiveDef) : null; const aliasedInputs = aliasData ? aliasData.inputs : null; const aliasedOutputs = aliasData ? aliasData.outputs : null; inputsStore = generatePropertyAliases(directiveDef.inputs, directiveIndex, inputsStore, aliasedInputs); outputsStore = generatePropertyAliases(directiveDef.outputs, directiveIndex, outputsStore, aliasedOutputs); // Do not use unbound attributes as inputs to structural directives, since structural // directive inputs can only be set using microsyntax (e.g. `<div *dir="exp">`). // TODO(FW-1930): microsyntax expressions may also contain unbound/static attributes, which // should be set for inline templates. const initialInputs = (inputsStore !== null && tNodeAttrs !== null && !isInlineTemplate(tNode)) ? generateInitialInputs(inputsStore, directiveIndex, tNodeAttrs) : null; inputsFromAttrs.push(initialInputs); } if (inputsStore !== null) { if (inputsStore.hasOwnProperty('class')) { tNode.flags |= 8 /* TNodeFlags.hasClassInput */; } if (inputsStore.hasOwnProperty('style')) { tNode.flags |= 16 /* TNodeFlags.hasStyleInput */; } } tNode.initialInputs = inputsFromAttrs; tNode.inputs = inputsStore; tNode.outputs = outputsStore; } /** * Mapping between attributes names that don't correspond to their element property names. * * Performance note: this function is written as a series of if checks (instead of, say, a property * object lookup) for performance reasons - the series of `if` checks seems to be the fastest way of * mapping property names. Do NOT change without benchmarking. * * Note: this mapping has to be kept in sync with the equally named mapping in the template * type-checking machinery of ngtsc. */ function mapPropName(name) { if (name === 'class') return 'className'; if (name === 'for') return 'htmlFor'; if (name === 'formaction') return 'formAction'; if (name === 'innerHtml') return 'innerHTML'; if (name === 'readonly') return 'readOnly'; if (name === 'tabindex') return 'tabIndex'; return name; } export function elementPropertyInternal(tView, tNode, lView, propName, value, renderer, sanitizer, nativeOnly) { ngDevMode && assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.'); const element = getNativeByTNode(tNode, lView); let inputData = tNode.inputs; let dataValue; if (!nativeOnly && inputData != null && (dataValue = inputData[propName])) { setInputsForProperty(tView, lView, dataValue, propName, value); if (isComponentHost(tNode)) markDirtyIfOnPush(lView, tNode.index); if (ngDevMode) { setNgReflectProperties(lView, element, tNode.type, dataValue, value); } } else if (tNode.type & 3 /* TNodeType.AnyRNode */) { propName = mapPropName(propName); if (ngDevMode) { validateAgainstEventProperties(propName); if (!isPropertyValid(element, propName, tNode.value, tView.schemas)) { handleUnknownPropertyError(propName, tNode.value, tNode.type, lView); } ngDevMode.rendererSetProperty++; } // It is assumed that the sanitizer is only added when the compiler determines that the // property is risky, so sanitization can be done without further checks. value = sanitizer != null ? sanitizer(value, tNode.value || '', propName) : value; renderer.setProperty(element, propName, value); } else if (tNode.type & 12 /* TNodeType.AnyContainer */) { // If the node is a container and the property didn't // match any of the inputs or schemas we should throw. if (ngDevMode && !matchingSchemas(tView.schemas, tNode.value)) { handleUnknownPropertyError(propName, tNode.value, tNode.type, lView); } } } /** If node is an OnPush component, marks its LView dirty. */ export function markDirtyIfOnPush(lView, viewIndex) { ngDevMode && assertLView(lView); const childComponentLView = getComponentLViewByIndex(viewIndex, lView); if (!(childComponentLView[FLAGS] & 16 /* LViewFlags.CheckAlways */)) { childComponentLView[FLAGS] |= 64 /* LViewFlags.Dirty */; } } function setNgReflectProperty(lView, element, type, attrName, value) { const renderer = lView[RENDERER]; attrName = normalizeDebugBindingName(attrName); const debugValue = normalizeDebugBindingValue(value); if (type & 3 /* TNodeType.AnyRNode */) { if (value == null) { renderer.removeAttribute(element, attrName); } else { renderer.setAttribute(element, attrName, debugValue); } } else { const textContent = escapeCommentText(`bindings=${JSON.stringify({ [attrName]: debugValue }, null, 2)}`); renderer.setValue(element, textContent); } } export function setNgReflectProperties(lView, element, type, dataValue, value) { if (type & (3 /* TNodeType.AnyRNode */ | 4 /* TNodeType.Container */)) { /** * dataValue is an array containing runtime input or output names for the directives: * i+0: directive instance index * i+1: privateName * * e.g. [0, 'change', 'change-minified'] * we want to set the reflected property with the privateName: dataValue[i+1] */ for (let i = 0; i < dataValue.length; i += 2) { setNgReflectProperty(lView, element, type, dataValue[i + 1], value); } } } /** * Resolve the matched directives on a node. */ export function resolveDirectives(tView, lView, tNode, localRefs) { // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in // tsickle. ngDevMode && assertFirstCreatePass(tView); if (getBindingsEnabled()) { const exportsMap = localRefs === null ? null : { '': -1 }; const matchResult = findDirectiveDefMatches(tView, tNode); let directiveDefs; let hostDirectiveDefs; if (matchResult === null) { directiveDefs = hostDirectiveDefs = null; } else { [directiveDefs, hostDirectiveDefs] = matchResult; } if (directiveDefs !== null) { initializeDirectives(tView, lView, tNode, directiveDefs, exportsMap, hostDirectiveDefs); } if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap); } // Merge the template attrs last so that they have the highest priority. tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, tNode.attrs); } /** Initializes the data structures necessary for a list of directives to be instantiated. */ export function initializeDirectives(tView, lView, tNode, directives, exportsMap, hostDirectiveDefs) { ngDevMode && assertFirstCreatePass(tView); // Publishes the directive types to DI so they can be injected. Needs to // happen in a separate pass before the TNode flags have been initialized. for (let i = 0; i < directives.length; i++) { diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, lView), tView, directives[i].type); } initTNodeFlags(tNode, tView.data.length, directives.length); // When the same token is provided by several directives on the same node, some rules apply in // the viewEngine: // - viewProviders have priority over providers // - the last directive in NgModule.declarations has priority over the previous one // So to match these rules, the order in which providers are added in the arrays is very // important. for (let i = 0; i < directives.length; i++) { const def = directives[i]; if (def.providersResolver) def.providersResolver(def); } let preOrderHooksFound = false; let preOrderCheckHooksFound = false; let directiveIdx = allocExpando(tView, lView, directives.length, null); ngDevMode && assertSame(directiveIdx, tNode.directiveStart, 'TNode.directiveStart should point to just allocated space'); for (let i = 0; i < directives.length; i++) { const def = directives[i]; // Merge the attrs in the order of matches. This assumes that the first directive is the // component itself, so that the component has the least priority. tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs); configureViewWithDirective(tView, tNode, lView, directiveIdx, def); saveNameToExportMap(directiveIdx, def, exportsMap); if (def.contentQueries !== null) tNode.flags |= 4 /* TNodeFlags.hasContentQuery */; if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0) tNode.flags |= 64 /* TNodeFlags.hasHostBindings */; const lifeCycleHooks = def.type.prototype; // Only push a node index into the preOrderHooks array if this is the first // pre-order hook found on this node. if (!preOrderHooksFound && (lifeCycleHooks.ngOnChanges || lifeCycleHooks.ngOnInit || lifeCycleHooks.ngDoCheck)) { // We will push the actual hook function into this array later during dir instantiation. // We cannot do it now because we must ensure hooks are registered in the same // order that directives are created (i.e. injection order). (tView.preOrderHooks ??= []).push(tNode.index); preOrderHooksFound = true; } if (!preOrderCheckHooksFound && (lifeCycleHooks.ngOnChanges || lifeCycleHooks.ngDoCheck)) { (tView.preOrderCheckHooks ??= []).push(tNode.index); preOrderCheckHooksFound = true; } directiveIdx++; } initializeInputAndOutputAliases(tView, tNode, hostDirectiveDefs); } /** * Add `hostBindings` to the `TView.hostBindingOpCodes`. * * @param tView `TView` to which the `hostBindings` should be added. * @param tNode `TNode` the element which contains the directive * @param directiveIdx Directive index in view. * @param directiveVarsIdx Where will the directive's vars be stored * @param def `ComponentDef`/`DirectiveDef`, which contains the `hostVars`/`hostBindings` to add. */ export function registerHostBindingOpCodes(tView, tNode, directiveIdx, directiveVarsIdx, def) { ngDevMode && assertFirstCreatePass(tView); const hostBindings = def.hostBindings; if (hostBindings) { let hostBindingOpCodes = tView.hostBindingOpCodes; if (hostBindingOpCodes === null) { hostBindingOpCodes = tView.hostBindingOpCodes = []; } const elementIndx = ~tNode.index; if (lastSelectedElementIdx(hostBindingOpCodes) != elementIndx) { // Conditionally add select element so that we are more efficient in execution. // NOTE: this is strictly not necessary and it trades code size for runtime perf. // (We could just always add it.) hostBindingOpCodes.push(elementIndx); } hostBindingOpCodes.push(directiveIdx, directiveVarsIdx, hostBindings); } } /** * Returns the last selected element index in the `HostBindingOpCodes` * * For perf reasons we don't need to update the selected element index in `HostBindingOpCodes` only * if it changes. This method returns the last index (or '0' if not found.) * * Selected element index are only the ones which are negative. */ function lastSelectedElementIdx(hostBindingOpCodes) { let i = hostBindingOpCodes.length; while (i > 0) { const value = hostBindingOpCodes[--i]; if (typeof value === 'number' && value < 0) { return value; } } return 0; } /** * Instantiate all the directives that were previously resolved on the current node. */ function instantiateAllDirectives(tView, lView, tNode, native) { const start = tNode.directiveStart; const end = tNode.directiveEnd; // The component view needs to be created before creating the node injector // since it is used to inject some special symbols like `ChangeDetectorRef`. if (isComponentHost(tNode)) { ngDevMode && assertTNodeType(tNode, 3 /* TNodeType.AnyRNode */); addComponentLogic(lView, tNode, tView.data[start + tNode.componentOffset]); } if (!tView.firstCreatePass) { getOrCreateNodeInjectorForNode(tNode, lView); } attachPatchData(native, lView); const initialInputs = tNode.initialInputs; for (let i = start; i < end; i++) { const def = tView.data[i]; const directive = getNodeInjectable(lView, tView, i, tNode); attachPatchData(directive, lView); if (initialInputs !== null) { setInputsFromAttrs(lView, i - start, directive, def, tNode, initialInputs); } if (isComponentDef(def)) { const componentView = getComponentLViewByIndex(tNode.index, lView); componentView[CONTEXT] = getNodeInjectable(lView, tView, i, tNode); } } } export function invokeDirectivesHostBindings(tView, lView, tNode) { const start = tNode.directiveStart; const end = tNode.directiveEnd; const elementIndex = tNode.index; const currentDirectiveIndex = getCurrentDirectiveIndex(); try { setSelectedIndex(elementIndex); for (let dirIndex = start; dirIndex < end; dirIndex++) { const def = tView.data[dirIndex]; const directive = lView[dirIndex]; setCurrentDirectiveIndex(dirIndex); if (def.hostBindings !== null || def.hostVars !== 0 || def.hostAttrs !== null) { invokeHostBindingsInCreationMode(def, directive); } } } finally { setSelectedIndex(-1); setCurrentDirectiveIndex(currentDirectiveIndex); } } /** * Invoke the host bindings in creation mode. * * @param def `DirectiveDef` which may contain the `hostBindings` function. * @param directive Instance of directive. */ export function invokeHostBindingsInCreationMode(def, directive) { if (def.hostBindings !== null) { def.hostBindings(1 /* RenderFlags.Create */, directive); } } /** * Matches the current node against all available selectors. * If a component is matched (at most one), it is returned in first position in the array. */ function findDirectiveDefMatches(tView, tNode) { ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertTNodeType(tNode, 3 /* TNodeType.AnyRNode */ | 12 /* TNodeType.AnyContainer */); const registry = tView.directiveRegistry; let matches = null; let hostDirectiveDefs = null; if (registry) { for (let i = 0; i < registry.length; i++) { const def = registry[i]; if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) { matches || (matches = []); if (isComponentDef(def)) { if (ngDevMode) { assertTNodeType(tNode, 2 /* TNodeType.Element */, `"${tNode.value}" tags cannot be used as component hosts. ` + `Please use a different tag to activate the ${stringify(def.type)} component.`); if (isComponentHost(tNode)) { throwMultipleComponentError(tNode, matches.find(isComponentDef).type, def.type); } } // Components are inserted at the front of the matches array so that their lifecycle // hooks run before any directive lifecycle hooks. This appears to be for ViewEngine // compatibility. This logic doesn't make sense with host directives, because it // would allow the host directives to undo any overrides the host may have made. // To handle this case, the host directives of components are inserted at the beginning // of the array, followed by the component. As such, the insertion order is as follows: // 1. Host directives belonging to the selector-matched component. // 2. Selector-matched component. // 3. Host directives belonging to selector-matched directives. // 4. Selector-matched directives. if (def.findHostDirectiveDefs !== null) { const hostDirectiveMatches = []; hostDirectiveDefs = hostDirectiveDefs || new Map(); def.findHostDirectiveDefs(def, hostDirectiveMatches, hostDirectiveDefs); // Add all host directives declared on this component, followed by the component itself. // Host directives should execute first so the host has a chance to override changes // to the DOM made by them. matches.unshift(...hostDirectiveMatches, def); // Component is offset starting from the beginning of the host directives array. const componentOffset = hostDirectiveMatches.length; markAsComponentHost(tView, tNode, componentOffset); } else { // No host directives on this component, just add the // component def to the beginning of the matches. matches.unshift(def); markAsComponentHost(tView, tNode, 0); } } else { // Append any host directives to the matches first. hostDirectiveDefs = hostDirectiveDefs || new Map(); def.findHostDirectiveDefs?.(def, matches, hostDirectiveDefs); matches.push(def); } } } } ngDevMode && matches !== null && assertNoDuplicateDirectives(matches); return matches === null ? null : [matches, hostDirectiveDefs]; } /** * Marks a given TNode as a component's host. This consists of: * - setting the component offset on the TNode. * - storing index of component's host element so it will be queued for view refresh during CD. */ export function markAsComponentHost(tView, hostTNode, componentOffset) { ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertGreaterThan(componentOffset, -1, 'componentOffset must be great than -1'); hostTNode.componentOffset = componentOffset; (tView.components ??= []).push(hostTNode.index); } /** Caches local names and their matching directive indices for query and template lookups. */ function cacheMatchingLocalNames(tNode, localRefs, exportsMap) { if (localRefs) { const localNames = tNode.localNames = []; // Local names must be stored in tNode in the same order that localRefs are defined // in the template to ensure the data is loaded in the same slots as their refs // in the template (for template queries). for (let i = 0; i < localRefs.length; i += 2) { const index = exportsMap[localRefs[i + 1]]; if (index == null) throw new RuntimeError(-301 /* RuntimeErrorCode.EXPORT_NOT_FOUND */, ngDevMode && `Export of name '${localRefs[i + 1]}' not found!`); localNames.push(localRefs[i], index); } } } /** * Builds up an export map as directives are created, so local refs can be quickly mapped * to their directive instances. */ function saveNameToExportMap(directiveIdx, def, exportsMap) { if (exportsMap) { if (def.exportAs) { for (let i = 0; i < def.exportAs.length; i++) { exportsMap[def.exportAs[i]] = directiveIdx; } } if (isComponentDef(def)) exportsMap[''] = directiveIdx; } } /** * Initializes the flags on the current node, setting all indices to the initial index, * the directive count to 0, and adding the isComponent flag. * @param index the initial index */ export function initTNodeFlags(tNode, index, numberOfDirectives) { ngDevMode && assertNotEqual(numberOfDirectives, tNode.directiveEnd - tNode.directiveStart, 'Reached the max number of directives'); tNode.flags |= 1 /* TNodeFlags.isDirectiveHost */; // When the first directive is created on a node, save the index tNode.directiveStart = index; tNode.directiveEnd = index + numberOfDirectives; tNode.providerIndexes = index; } /** * Setup directive for instantiation. * * We need to create a `NodeInjectorFactory` which is then inserted in both the `Blueprint` as well * as `LView`. `TView` gets the `DirectiveDef`. * * @param tView `TView` * @param tNode `TNode` * @param lView `LView` * @param directiveIndex Index where the directive will be stored in the Expando. * @param def `DirectiveDef` */ export function configureViewWithDirective(tView, tNode, lView, directiveIndex, def) { ngDevMode && assertGreaterThanOrEqual(directiveIndex, HEADER_OFFSET, 'Must be in Expando section'); tView.data[directiveIndex] = def; const directiveFactory = def.factory || (def.factory = getFactoryDef(def.type, true)); // Even though `directiveFactory` will already be using `ɵɵdirectiveInject` in its generated code, // we also want to support `inject()` directly from the directive constructor context so we set // `ɵɵdirectiveInject` as the inject implementation here too. const nodeInjectorFactory = new NodeInjectorFactory(directiveFactory, isComponentDef(def), ɵɵdirectiveInject); tView.blueprint[directiveIndex] = nodeInjectorFactory; lView[directiveIndex] = nodeInjectorFactory; registerHostBindingOpCodes(tView, tNode, directiveIndex, allocExpando(tView, lView, def.hostVars, NO_CHANGE), def); } function addComponentLogic(lView, hostTNode, def) { const native = getNativeByTNode(hostTNode, lView); const tView = getOrCreateComponentTView(def); // Only component views should be added to the view tree directly. Embedded views are // accessed through their containers because they may be removed / re-added later. const rendererFactory = lView[ENVIRONMENT].rendererFactory; let lViewFlags = 16 /* LViewFlags.CheckAlways */; if (def.signals) { lViewFlags = 4096 /* LViewFlags.SignalView */; } else if (def.onPush) { lViewFlags = 64 /* LViewFlags.Dirty */; } const componentView = addToViewTree(lView, createLView(lView, tView, null, lViewFlags, native, hostTNode, null, rendererFactory.createRenderer(native, def), null, null, null)); // Component view will always be created before any injected LContainers, // so this is a regular element, wrap it with the component view lView[hostTNode.index] = componentView; } export function elementAttributeInternal(tNode, lView, name, value, sanitizer, namespace) { if (ngDevMode) { assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.'); validateAgainstEventAttributes(name); assertTNodeType(tNode, 2 /* TNodeType.Element */, `Attempted to set attribute \`${name}\` on a container node. ` + `Host bindings are not valid on ng-container or ng-template.`); } const element = getNativeByTNode(tNode, lView); setElementAttribute(lView[RENDERER], element, namespace, tNode.value, name, value, sanitizer); } export function setElementAttribute(renderer, element, namespace, tagName, name, value, sanitizer) { if (value == null) { ngDevMode && ngDevMode.rendererRemoveAttribute++; renderer.removeAttribute(element, name, namespace); } else { ngDevMode && ngDevMode.rendererSetAttribute++; const strValue = sanitizer == null ? ren