@angular/core
Version:
Angular - the core framework
1,047 lines • 206 kB
JavaScript
/**
* @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