UNPKG

@angular/core

Version:

Angular - the core framework

1,049 lines 271 kB
import { ErrorHandler } from '../../error_handler'; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '../../metadata/schema'; import { ViewEncapsulation } from '../../metadata/view'; import { validateAgainstEventAttributes, validateAgainstEventProperties } from '../../sanitization/sanitization'; import { assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertNotEqual, assertNotSame, assertSame } from '../../util/assert'; import { createNamedArrayType } from '../../util/named_array_type'; import { initNgDevMode } from '../../util/ng_dev_mode'; import { normalizeDebugBindingName, normalizeDebugBindingValue } from '../../util/ng_reflect'; import { stringify } from '../../util/stringify'; import { assertFirstCreatePass, assertLContainer, assertLView } from '../assert'; import { attachPatchData } from '../context_discovery'; import { getFactoryDef } from '../definition'; import { diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode } from '../di'; import { throwMultipleComponentError } from '../errors'; import { executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags } from '../hooks'; import { CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, MOVED_VIEWS } from '../interfaces/container'; import { INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory } from '../interfaces/injector'; import { isProceduralRenderer } from '../interfaces/renderer'; import { isComponentDef, isComponentHost, isContentQueryHost, isRootView } from '../interfaces/type_checks'; import { CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, NEXT, PARENT, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW } from '../interfaces/view'; import { assertNodeNotOfTypes, assertNodeOfPossibleTypes } from '../node_assert'; import { isInlineTemplate, isNodeMatchingSelectorList } from '../node_selector_matcher'; import { enterView, getBindingsEnabled, getCheckNoChangesMode, getCurrentDirectiveIndex, getIsParent, getPreviousOrParentTNode, getSelectedIndex, leaveView, setBindingIndex, setBindingRootForHostBindings, setCheckNoChangesMode, setCurrentDirectiveIndex, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex } from '../state'; import { NO_CHANGE } from '../tokens'; import { isAnimationProp, mergeHostAttrs } from '../util/attrs_utils'; import { INTERPOLATION_DELIMITER, renderStringify, stringifyForError } from '../util/misc_utils'; import { getFirstLContainer, getLViewParent, getNextLContainer } from '../util/view_traversal_utils'; import { getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, unwrapLView, updateTransplantedViewCount, viewAttachedToChangeDetector } from '../util/view_utils'; import { selectIndexInternal } from './advance'; import { attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData, LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor } from './lview_debug'; const ɵ0 = () => Promise.resolve(null); /** * A permanent marker promise which signifies that the current CD tree is * clean. */ const _CLEAN_PROMISE = (ɵ0)(); /** * Process the `TView.expandoInstructions`. (Execute the `hostBindings`.) * * @param tView `TView` containing the `expandoInstructions` * @param lView `LView` associated with the `TView` */ export function setHostBindingsByExecutingExpandoInstructions(tView, lView) { ngDevMode && assertSame(tView, lView[TVIEW], '`LView` is not associated with the `TView`!'); try { const expandoInstructions = tView.expandoInstructions; if (expandoInstructions !== null) { let bindingRootIndex = tView.expandoStartIndex; let currentDirectiveIndex = -1; let currentElementIndex = -1; // TODO(misko): PERF It is possible to get here with `TView.expandoInstructions` containing no // functions to execute. This is wasteful as there is no work to be done, but we still need // to iterate over the instructions. // In example of this is in this test: `host_binding_spec.ts` // `fit('should not cause problems if detectChanges is called when a property updates', ...` // In the above test we get here with expando [0, 0, 1] which requires a lot of processing but // there is no function to execute. for (let i = 0; i < expandoInstructions.length; i++) { const instruction = expandoInstructions[i]; if (typeof instruction === 'number') { if (instruction <= 0) { // Negative numbers mean that we are starting new EXPANDO block and need to update // the current element and directive index. // Important: In JS `-x` and `0-x` is not the same! If `x===0` then `-x` will produce // `-0` which requires non standard math arithmetic and it can prevent VM optimizations. // `0-0` will always produce `0` and will not cause a potential deoptimization in VM. // TODO(misko): PERF This should be refactored to use `~instruction` as that does not // suffer from `-0` and it is faster/more compact. currentElementIndex = 0 - instruction; setSelectedIndex(currentElementIndex); // Injector block and providers are taken into account. const providerCount = expandoInstructions[++i]; bindingRootIndex += INJECTOR_BLOOM_PARENT_SIZE + providerCount; currentDirectiveIndex = bindingRootIndex; } else { // This is either the injector size (so the binding root can skip over directives // and get to the first set of host bindings on this node) or the host var count // (to get to the next set of host bindings on this node). bindingRootIndex += instruction; } } else { // If it's not a number, it's a host binding function that needs to be executed. if (instruction !== null) { setBindingRootForHostBindings(bindingRootIndex, currentDirectiveIndex); const hostCtx = lView[currentDirectiveIndex]; instruction(2 /* Update */, hostCtx); } // TODO(misko): PERF Relying on incrementing the `currentDirectiveIndex` here is // sub-optimal. The implications are that if we have a lot of directives but none of them // have host bindings we nevertheless need to iterate over the expando instructions to // update the counter. It would be much better if we could encode the // `currentDirectiveIndex` into the `expandoInstruction` array so that we only need to // iterate over those directives which actually have `hostBindings`. currentDirectiveIndex++; } } } } finally { setSelectedIndex(-1); } } /** Refreshes all content queries declared by directives in a given view */ function refreshContentQueries(tView, lView) { const contentQueries = tView.contentQueries; if (contentQueries !== null) { for (let i = 0; i < contentQueries.length; i += 2) { const queryStartIdx = contentQueries[i]; const directiveDefIdx = contentQueries[i + 1]; if (directiveDefIdx !== -1) { const directiveDef = tView.data[directiveDefIdx]; ngDevMode && assertDefined(directiveDef.contentQueries, 'contentQueries function should be defined'); setCurrentQueryIndex(queryStartIdx); directiveDef.contentQueries(2 /* Update */, lView[directiveDefIdx], directiveDefIdx); } } } } /** Refreshes child components in the current view (update mode). */ function refreshChildComponents(hostLView, components) { for (let i = 0; i < components.length; i++) { refreshComponent(hostLView, components[i]); } } /** Renders child components in the current view (creation mode). */ function renderChildComponents(hostLView, components) { for (let i = 0; i < components.length; i++) { renderComponent(hostLView, components[i]); } } /** * Creates a native element from a tag name, using a renderer. * @param name the tag name * @param renderer A renderer to use * @returns the element created */ export function elementCreate(name, renderer, namespace) { if (isProceduralRenderer(renderer)) { return renderer.createElement(name, namespace); } else { return namespace === null ? renderer.createElement(name) : renderer.createElementNS(namespace, name); } } export function createLView(parentLView, tView, context, flags, host, tHostNode, rendererFactory, renderer, sanitizer, injector) { const lView = ngDevMode ? cloneToLViewFromTViewBlueprint(tView) : tView.blueprint.slice(); lView[HOST] = host; lView[FLAGS] = flags | 4 /* CreationMode */ | 128 /* Attached */ | 8 /* FirstLViewPass */; resetPreOrderHookFlags(lView); lView[PARENT] = lView[DECLARATION_VIEW] = parentLView; lView[CONTEXT] = context; lView[RENDERER_FACTORY] = (rendererFactory || parentLView && parentLView[RENDERER_FACTORY]); ngDevMode && assertDefined(lView[RENDERER_FACTORY], 'RendererFactory is required'); lView[RENDERER] = (renderer || parentLView && parentLView[RENDERER]); ngDevMode && assertDefined(lView[RENDERER], 'Renderer is required'); lView[SANITIZER] = sanitizer || parentLView && parentLView[SANITIZER] || null; lView[INJECTOR] = injector || parentLView && parentLView[INJECTOR] || null; lView[T_HOST] = tHostNode; ngDevMode && assertEqual(tView.type == 2 /* Embedded */ ? parentLView !== null : true, true, 'Embedded views must have parentLView'); lView[DECLARATION_COMPONENT_VIEW] = tView.type == 2 /* Embedded */ ? parentLView[DECLARATION_COMPONENT_VIEW] : lView; ngDevMode && attachLViewDebug(lView); return lView; } export function getOrCreateTNode(tView, tHostNode, index, type, name, attrs) { // Keep this function short, so that the VM will inline it. const adjustedIndex = index + HEADER_OFFSET; const tNode = tView.data[adjustedIndex] || createTNodeAtIndex(tView, tHostNode, adjustedIndex, type, name, attrs); setPreviousOrParentTNode(tNode, true); return tNode; } function createTNodeAtIndex(tView, tHostNode, adjustedIndex, type, name, attrs) { const previousOrParentTNode = getPreviousOrParentTNode(); const isParent = getIsParent(); const parent = isParent ? previousOrParentTNode : previousOrParentTNode && previousOrParentTNode.parent; // Parents cannot cross component boundaries because components will be used in multiple places, // so it's only set if the view is the same. const parentInSameView = parent && parent !== tHostNode; const tParentNode = parentInSameView ? parent : null; const tNode = tView.data[adjustedIndex] = createTNode(tView, tParentNode, type, adjustedIndex, 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 (previousOrParentTNode) { if (isParent && previousOrParentTNode.child == null && (tNode.parent !== null || previousOrParentTNode.type === 2 /* View */)) { // We are in the same view, which means we are adding content node to the parent view. previousOrParentTNode.child = tNode; } else if (!isParent) { previousOrParentTNode.next = tNode; } } return tNode; } export function assignTViewNodeToLView(tView, tParentNode, index, lView) { // View nodes are not stored in data because they can be added / removed at runtime (which // would cause indices to change). Their TNodes are instead stored in tView.node. let tNode = tView.node; if (tNode == null) { ngDevMode && tParentNode && assertNodeOfPossibleTypes(tParentNode, [3 /* Element */, 0 /* Container */]); tView.node = tNode = createTNode(tView, tParentNode, // 2 /* View */, index, null, null); } lView[T_HOST] = tNode; } /** * When elements are created dynamically after a view blueprint is created (e.g. through * i18nApply() or ComponentFactory.create), we need to adjust the blueprint for future * template passes. * * @param tView `TView` associated with `LView` * @param view The `LView` containing the blueprint to adjust * @param numSlotsToAlloc The number of slots to alloc in the LView, should be >0 */ export function allocExpando(tView, lView, numSlotsToAlloc) { ngDevMode && assertGreaterThan(numSlotsToAlloc, 0, 'The number of slots to alloc should be greater than 0'); if (numSlotsToAlloc > 0) { if (tView.firstCreatePass) { for (let i = 0; i < numSlotsToAlloc; i++) { tView.blueprint.push(null); tView.data.push(null); lView.push(null); } // We should only increment the expando start index if there aren't already directives // and injectors saved in the "expando" section if (!tView.expandoInstructions) { tView.expandoStartIndex += numSlotsToAlloc; } else { // Since we're adding the dynamic nodes into the expando section, we need to let the host // bindings know that they should skip x slots tView.expandoInstructions.push(numSlotsToAlloc); } } } } ////////////////////////// //// Render ////////////////////////// /** * Processes a view in the creation mode. This includes a number of steps in a specific order: * - creating view query functions (if any); * - executing a template function in the creation mode; * - updating static queries (if any); * - creating child components defined in a given view. */ export function renderView(tView, lView, context) { ngDevMode && assertEqual(isCreationMode(lView), true, 'Should be run in creation mode'); enterView(lView, lView[T_HOST]); try { const viewQuery = tView.viewQuery; if (viewQuery !== null) { executeViewQueryFn(1 /* Create */, viewQuery, context); } // Execute a template associated with this view, if it exists. A template function might not be // defined for the root component views. const templateFn = tView.template; if (templateFn !== null) { executeTemplate(tView, lView, templateFn, 1 /* Create */, context); } // This needs to be set before children are processed to support recursive components. // This must be set to false immediately after the first creation run because in an // ngFor loop, all the views will be created together before update mode runs and turns // off firstCreatePass. If we don't set it here, instances will perform directive // matching, etc again and again. if (tView.firstCreatePass) { tView.firstCreatePass = false; } // We resolve content queries specifically marked as `static` in creation mode. Dynamic // content queries are resolved during change detection (i.e. update mode), after embedded // views are refreshed (see block above). if (tView.staticContentQueries) { refreshContentQueries(tView, lView); } // We must materialize query results before child components are processed // in case a child component has projected a container. The LContainer needs // to exist so the embedded views are properly attached by the container. if (tView.staticViewQueries) { executeViewQueryFn(2 /* Update */, tView.viewQuery, context); } // Render child component views. const components = tView.components; if (components !== null) { renderChildComponents(lView, components); } } catch (error) { // If we didn't manage to get past the first template pass due to // an error, mark the view as corrupted so we can try to recover. if (tView.firstCreatePass) { tView.incompleteFirstPass = true; } throw error; } finally { lView[FLAGS] &= ~4 /* CreationMode */; leaveView(); } } /** * Processes a view in update mode. This includes a number of steps in a specific order: * - executing a template function in update mode; * - executing hooks; * - refreshing queries; * - setting host bindings; * - refreshing child (embedded and component) views. */ export function refreshView(tView, lView, templateFn, context) { ngDevMode && assertEqual(isCreationMode(lView), false, 'Should be run in update mode'); const flags = lView[FLAGS]; if ((flags & 256 /* Destroyed */) === 256 /* Destroyed */) return; enterView(lView, lView[T_HOST]); const checkNoChangesMode = getCheckNoChangesMode(); try { resetPreOrderHookFlags(lView); setBindingIndex(tView.bindingStartIndex); if (templateFn !== null) { executeTemplate(tView, lView, templateFn, 2 /* Update */, context); } const hooksInitPhaseCompleted = (flags & 3 /* InitPhaseStateMask */) === 3 /* InitPhaseCompleted */; // execute pre-order hooks (OnInit, OnChanges, DoCheck) // PERF WARNING: do NOT extract this to a separate function without running benchmarks if (!checkNoChangesMode) { if (hooksInitPhaseCompleted) { const preOrderCheckHooks = tView.preOrderCheckHooks; if (preOrderCheckHooks !== null) { executeCheckHooks(lView, preOrderCheckHooks, null); } } else { const preOrderHooks = tView.preOrderHooks; if (preOrderHooks !== null) { executeInitAndCheckHooks(lView, preOrderHooks, 0 /* OnInitHooksToBeRun */, null); } incrementInitPhaseFlags(lView, 0 /* OnInitHooksToBeRun */); } } // First mark transplanted views that are declared in this lView as needing a refresh at their // insertion points. This is needed to avoid the situation where the template is defined in this // `LView` but its declaration appears after the insertion component. markTransplantedViewsForRefresh(lView); refreshEmbeddedViews(lView); // Content query results must be refreshed before content hooks are called. if (tView.contentQueries !== null) { refreshContentQueries(tView, lView); } // execute content hooks (AfterContentInit, AfterContentChecked) // PERF WARNING: do NOT extract this to a separate function without running benchmarks if (!checkNoChangesMode) { if (hooksInitPhaseCompleted) { const contentCheckHooks = tView.contentCheckHooks; if (contentCheckHooks !== null) { executeCheckHooks(lView, contentCheckHooks); } } else { const contentHooks = tView.contentHooks; if (contentHooks !== null) { executeInitAndCheckHooks(lView, contentHooks, 1 /* AfterContentInitHooksToBeRun */); } incrementInitPhaseFlags(lView, 1 /* AfterContentInitHooksToBeRun */); } } setHostBindingsByExecutingExpandoInstructions(tView, lView); // Refresh child component views. const components = tView.components; if (components !== null) { refreshChildComponents(lView, components); } // View queries must execute after refreshing child components because a template in this view // could be inserted in a child component. If the view query executes before child component // refresh, the template might not yet be inserted. const viewQuery = tView.viewQuery; if (viewQuery !== null) { executeViewQueryFn(2 /* Update */, viewQuery, context); } // execute view hooks (AfterViewInit, AfterViewChecked) // PERF WARNING: do NOT extract this to a separate function without running benchmarks if (!checkNoChangesMode) { if (hooksInitPhaseCompleted) { const viewCheckHooks = tView.viewCheckHooks; if (viewCheckHooks !== null) { executeCheckHooks(lView, viewCheckHooks); } } else { const viewHooks = tView.viewHooks; if (viewHooks !== null) { executeInitAndCheckHooks(lView, viewHooks, 2 /* AfterViewInitHooksToBeRun */); } incrementInitPhaseFlags(lView, 2 /* AfterViewInitHooksToBeRun */); } } if (tView.firstUpdatePass === true) { // We need to make sure that we only flip the flag on successful `refreshView` only // Don't do this in `finally` block. // If we did this in `finally` block then an exception could block the execution of styling // instructions which in turn would be unable to insert themselves into the styling linked // list. The result of this would be that if the exception would not be throw on subsequent CD // the styling would be unable to process it data and reflect to the DOM. tView.firstUpdatePass = false; } // Do not reset the dirty state when running in check no changes mode. We don't want components // to behave differently depending on whether check no changes is enabled or not. For example: // Marking an OnPush component as dirty from within the `ngAfterViewInit` hook in order to // refresh a `NgClass` binding should work. If we would reset the dirty state in the check // no changes cycle, the component would be not be dirty for the next update pass. This would // be different in production mode where the component dirty state is not reset. if (!checkNoChangesMode) { lView[FLAGS] &= ~(64 /* Dirty */ | 8 /* FirstLViewPass */); } if (lView[FLAGS] & 1024 /* RefreshTransplantedView */) { lView[FLAGS] &= ~1024 /* RefreshTransplantedView */; updateTransplantedViewCount(lView[PARENT], -1); } } finally { leaveView(); } } export function renderComponentOrTemplate(tView, lView, templateFn, context) { const rendererFactory = lView[RENDERER_FACTORY]; const normalExecutionPath = !getCheckNoChangesMode(); const creationModeIsActive = isCreationMode(lView); try { if (normalExecutionPath && !creationModeIsActive && rendererFactory.begin) { rendererFactory.begin(); } if (creationModeIsActive) { renderView(tView, lView, context); } refreshView(tView, lView, templateFn, context); } finally { if (normalExecutionPath && !creationModeIsActive && rendererFactory.end) { rendererFactory.end(); } } } function executeTemplate(tView, lView, templateFn, rf, context) { const prevSelectedIndex = getSelectedIndex(); try { setSelectedIndex(-1); if (rf & 2 /* Update */ && 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, 0, getCheckNoChangesMode()); } templateFn(rf, context); } finally { setSelectedIndex(prevSelectedIndex); } } ////////////////////////// //// Element ////////////////////////// export function executeContentQueries(tView, tNode, lView) { if (isContentQueryHost(tNode)) { 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 /* Create */, lView[directiveIndex], directiveIndex); } } } } /** * Creates directive instances. */ export function createDirectivesInstances(tView, lView, tNode) { if (!getBindingsEnabled()) return; instantiateAllDirectives(tView, lView, tNode, getNativeByTNode(tNode, lView)); if ((tNode.flags & 128 /* hasHostBindings */) === 128 /* 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 getOrCreateTComponentView(def) { const tView = def.tView; // Create a TView if there isn't one, or recreate it if the first create pass didn't // complete successfuly since we can't know for sure whether it's in a usable shape. if (tView === null || tView.incompleteFirstPass) { return def.tView = createTView(1 /* Component */, -1, def.template, def.decls, def.vars, def.directiveDefs, def.pipeDefs, def.viewQuery, def.schemas, def.consts); } return tView; } /** * Creates a TView instance * * @param viewIndex The viewBlockId for inline views, or -1 if it's a component/dynamic * @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, viewIndex, templateFn, decls, vars, directives, pipes, viewQuery, schemas, consts) { 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); return blueprint[TVIEW] = ngDevMode ? new TViewConstructor(type, viewIndex, // id: number, blueprint, // blueprint: LView, templateFn, // template: ComponentTemplate<{}>|null, null, // queries: TQueries|null viewQuery, // viewQuery: ViewQueriesFunction<{}>|null, null, // node: TViewNode|TElementNode|null, cloneToTViewData(blueprint).fill(null, bindingStartIndex), // data: TData, bindingStartIndex, // bindingStartIndex: number, initialViewLength, // expandoStartIndex: number, null, // expandoInstructions: ExpandoInstructions|null, true, // firstCreatePass: boolean, true, // firstUpdatePass: boolean, false, // staticViewQueries: boolean, false, // staticContentQueries: boolean, null, // preOrderHooks: HookData|null, null, // preOrderCheckHooks: HookData|null, null, // contentHooks: HookData|null, null, // contentCheckHooks: HookData|null, null, // viewHooks: HookData|null, null, // viewCheckHooks: HookData|null, null, // destroyHooks: DestroyHookData|null, null, // cleanup: any[]|null, null, // contentQueries: number[]|null, null, // components: number[]|null, typeof directives === 'function' ? directives() : directives, // directiveRegistry: DirectiveDefList|null, typeof pipes === 'function' ? pipes() : pipes, // pipeRegistry: PipeDefList|null, null, // firstChild: TNode|null, schemas, // schemas: SchemaMetadata[]|null, consts, // consts: TConstants|null false // incompleteFirstPass: boolean ) : { type: type, id: viewIndex, blueprint: blueprint, template: templateFn, queries: null, viewQuery: viewQuery, node: null, data: blueprint.slice().fill(null, bindingStartIndex), bindingStartIndex: bindingStartIndex, expandoStartIndex: initialViewLength, expandoInstructions: 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 }; } function createViewBlueprint(bindingStartIndex, initialViewLength) { const blueprint = ngDevMode ? new LViewBlueprint() : []; for (let i = 0; i < initialViewLength; i++) { blueprint.push(i < bindingStartIndex ? null : NO_CHANGE); } return blueprint; } function createError(text, token) { return new Error(`Renderer: ${text} [${stringifyForError(token)}]`); } function assertHostNodeExists(rElement, elementOrSelector) { if (!rElement) { if (typeof elementOrSelector === 'string') { throw createError('Host node with selector not found:', elementOrSelector); } else { throw createError('Host node is required:', elementOrSelector); } } } /** * Locates the host native element, used for bootstrapping existing nodes into rendering pipeline. * * @param rendererFactory Factory function to create renderer instance. * @param elementOrSelector Render element or CSS selector to locate the element. * @param encapsulation View Encapsulation defined for component that requests host element. */ export function locateHostElement(renderer, elementOrSelector, encapsulation) { if (isProceduralRenderer(renderer)) { // When using native Shadow DOM, do not clear host element to allow native slot projection const preserveContent = encapsulation === ViewEncapsulation.ShadowDom; return renderer.selectRootElement(elementOrSelector, preserveContent); } let rElement = typeof elementOrSelector === 'string' ? renderer.querySelector(elementOrSelector) : elementOrSelector; ngDevMode && assertHostNodeExists(rElement, elementOrSelector); // Always clear host element's content when Renderer3 is in use. For procedural renderer case we // make it depend on whether ShadowDom encapsulation is used (in which case the content should be // preserved to allow native slot projection). ShadowDom encapsulation requires procedural // renderer, and procedural renderer case is handled above. rElement.textContent = ''; return rElement; } /** * 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 = getLCleanup(lView); lCleanup.push(context); if (tView.firstCreatePass) { getTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1); } } /** * Constructs a TNode object from the arguments. * * @param tView `TView` to which this `TNode` belongs (used only in `ngDevMode`) * @param type The type of the node * @param adjustedIndex The index of the TNode in TView.data, adjusted for HEADER_OFFSET * @param tagName The tag name of the node * @param attrs The attributes defined on this node * @param tViews Any TViews attached to this node * @returns the TNode object */ export function createTNode(tView, tParent, type, adjustedIndex, tagName, attrs) { ngDevMode && ngDevMode.tNode++; let injectorIndex = tParent ? tParent.injectorIndex : -1; return ngDevMode ? new TNodeDebug(tView, // tView_: TView type, // type: TNodeType adjustedIndex, // index: number injectorIndex, // injectorIndex: number -1, // directiveStart: number -1, // directiveEnd: number -1, // directiveStylingLast: number null, // propertyBindings: number[]|null 0, // flags: TNodeFlags 0, // providerIndexes: TNodeProviderIndexes tagName, // tagName: string|null attrs, // attrs: (string|AttributeMarker|(string|SelectorFlags)[])[]|null null, // mergedAttrs null, // localNames: (string|number)[]|null undefined, // initialInputs: (string[]|null)[]|null|undefined null, // inputs: PropertyAliases|null null, // outputs: PropertyAliases|null null, // tViews: ITView|ITView[]|null null, // next: ITNode|null null, // projectionNext: ITNode|null null, // child: ITNode|null tParent, // parent: TElementNode|TContainerNode|null null, // projection: number|(ITNode|RNode[])[]|null null, // styles: string|null null, // stylesWithoutHost: string|null undefined, // residualStyles: string|null null, // classes: string|null null, // classesWithoutHost: string|null undefined, // residualClasses: string|null 0, // classBindings: TStylingRange; 0) : { type: type, index: adjustedIndex, injectorIndex: injectorIndex, directiveStart: -1, directiveEnd: -1, directiveStylingLast: -1, propertyBindings: null, flags: 0, providerIndexes: 0, tagName: tagName, attrs: attrs, mergedAttrs: null, localNames: null, initialInputs: undefined, inputs: null, outputs: null, tViews: null, next: 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, }; } function generatePropertyAliases(inputAliasMap, directiveDefIdx, propStore) { for (let publicName in inputAliasMap) { if (inputAliasMap.hasOwnProperty(publicName)) { propStore = propStore === null ? {} : propStore; const internalName = inputAliasMap[publicName]; if (propStore.hasOwnProperty(publicName)) { propStore[publicName].push(directiveDefIdx, internalName); } else { (propStore[publicName] = [directiveDefIdx, internalName]); } } } return propStore; } /** * Initializes data structures required to work with directive outputs and outputs. * Initialization is done for all directives matched on a given TNode. */ function initializeInputAndOutputAliases(tView, tNode) { ngDevMode && assertFirstCreatePass(tView); const start = tNode.directiveStart; const end = tNode.directiveEnd; const defs = tView.data; const tNodeAttrs = tNode.attrs; const inputsFromAttrs = ngDevMode ? new TNodeInitialInputs() : []; let inputsStore = null; let outputsStore = null; for (let i = start; i < end; i++) { const directiveDef = defs[i]; const directiveInputs = directiveDef.inputs; // 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 = (tNodeAttrs !== null && !isInlineTemplate(tNode)) ? generateInitialInputs(directiveInputs, tNodeAttrs) : null; inputsFromAttrs.push(initialInputs); inputsStore = generatePropertyAliases(directiveInputs, i, inputsStore); outputsStore = generatePropertyAliases(directiveDef.outputs, i, outputsStore); } if (inputsStore !== null) { if (inputsStore.hasOwnProperty('class')) { tNode.flags |= 16 /* hasClassInput */; } if (inputsStore.hasOwnProperty('style')) { tNode.flags |= 32 /* 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 /* Element */) { propName = mapPropName(propName); if (ngDevMode) { validateAgainstEventProperties(propName); if (!validateProperty(tView, lView, element, propName, tNode)) { // Return here since we only log warnings for unknown properties. logUnknownPropertyError(propName, tNode); return; } 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.tagName || '', propName) : value; if (isProceduralRenderer(renderer)) { renderer.setProperty(element, propName, value); } else if (!isAnimationProp(propName)) { element.setProperty ? element.setProperty(propName, value) : element[propName] = value; } } else if (tNode.type === 0 /* Container */) { // 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, lView, tNode.tagName)) { logUnknownPropertyError(propName, tNode); } } } /** If node is an OnPush component, marks its LView dirty. */ function markDirtyIfOnPush(lView, viewIndex) { ngDevMode && assertLView(lView); const childComponentLView = getComponentLViewByIndex(viewIndex, lView); if (!(childComponentLView[FLAGS] & 16 /* CheckAlways */)) { childComponentLView[FLAGS] |= 64 /* Dirty */; } } function setNgReflectProperty(lView, element, type, attrName, value) { const renderer = lView[RENDERER]; attrName = normalizeDebugBindingName(attrName); const debugValue = normalizeDebugBindingValue(value); if (type === 3 /* Element */) { if (value == null) { isProceduralRenderer(renderer) ? renderer.removeAttribute(element, attrName) : element.removeAttribute(attrName); } else { isProceduralRenderer(renderer) ? renderer.setAttribute(element, attrName, debugValue) : element.setAttribute(attrName, debugValue); } } else { const textContent = `bindings=${JSON.stringify({ [attrName]: debugValue }, null, 2)}`; if (isProceduralRenderer(renderer)) { renderer.setValue(element, textContent); } else { element.textContent = textContent; } } } export function setNgReflectProperties(lView, element, type, dataValue, value) { if (type === 3 /* Element */ || type === 0 /* 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); } } } function validateProperty(tView, lView, element, propName, tNode) { // If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT // mode where this check happens at compile time. In JIT mode, `schemas` is always present and // defined as an array (as an empty array in case `schemas` field is not defined) and we should // execute the check below. if (tView.schemas === null) return true; // The property is considered valid if the element matches the schema, it exists on the element // or it is synthetic, and we are in a browser context (web worker nodes should be skipped). if (matchingSchemas(tView, lView, tNode.tagName) || propName in element || isAnimationProp(propName)) { return true; } // Note: `typeof Node` returns 'function' in most browsers, but on IE it is 'object' so we // need to account for both here, while being careful for `typeof null` also returning 'object'. return typeof Node === 'undefined' || Node === null || !(element instanceof Node); } export function matchingSchemas(tView, lView, tagName) { const schemas = tView.schemas; if (schemas !== null) { for (let i = 0; i < schemas.length; i++) { const schema = schemas[i]; if (schema === NO_ERRORS_SCHEMA || schema === CUSTOM_ELEMENTS_SCHEMA && tagName && tagName.indexOf('-') > -1) { return true; } } } return false; } /** * Logs an error that a property is not supported on an element. * @param propName Name of the invalid property. * @param tNode Node on which we encountered the property. */ function logUnknownPropertyError(propName, tNode) { console.error(`Can't bind to '${propName}' since it isn't a known property of '${tNode.tagName}'.`); } /** * Instantiate a root component. */ export function instantiateRootComponent(tView, lView, def) { const rootTNode = getPreviousOrParentTNode(); if (tView.firstCreatePass) { if (def.providersResolver) def.providersResolver(def); generateExpandoInstructionBlock(tView, rootTNode, 1); baseResolveDirective(tView, lView, def); } const directive = getNodeInjectable(lView, tView, lView.length - 1, rootTNode); attachPatchData(directive, lView); const native = getNativeByTNode(rootTNode, lView); if (native) { attachPatchData(native, lView); } return directive; } /** * 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); let hasDirectives = false; if (getBindingsEnabled()) { const directiveDefs = findDirectiveDefMatches(tView, lView, tNode); const exportsMap = localRefs === null ? null : { '': -1 }; if (directiveDefs !== null) { let totalDirectiveHostVars = 0; hasDirectives = true; initTNodeFlags(tNode, tView.data.length, directiveDefs.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 < directiveDefs.length; i++) { const def = directiveDefs[i]; if (def.providersResolver) def.providersResolver(def); } generateExpandoInstructionBlock(tView, tNode, directiveDefs.length); let preOrderHooksFound = false; let preOrderCheckHooksFound = false; for (let i = 0; i < directiveDefs.length; i++) { const def = directiveDefs[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); baseResolveDirective(tView, lView, def); saveNameToExportMap(tView.data.length - 1, def, exportsMap); if (def.contentQueries !== null) tNode.flags |= 8 /* hasContentQuery */; if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0) tNode.flags |= 128 /* hasHostBindings */; // Only push a node index into the preOrderHooks array if this is the first // pre-order hook found on this node. if (!preOrderHooksFound && (def.onChanges || def.onInit || def.doCheck)) { // 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 || (tView.preOrderHooks = [])).push(tNode.index - HEADER_OFFSET); preOrderHooksFound = true; } if (!preOrderCheckHooksFound && (def.onChanges || def.doCheck)) { (tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])) .push(tNode.index - HEADER_OFFSET); preOrderCheckHooksFound = true; } addHostBindingsToExpandoInstructions(tView, def); totalDirectiveHostVars += def.hostVars; } initializeInputAndOutputAliases(tView, tNode); growHostVarsSpace(tView, lView, totalDirectiveHostVars); } 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); return hasDirectives; } /** * Add `hostBindings` to the `TView.expandoInstructions`. * * @param tView `TView` to which the `hos