UNPKG

@angular/compiler

Version:

Angular - the compiler library

931 lines 302 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 { flatten } from '../../compile_metadata'; import { BindingForm, BuiltinFunctionCall, convertActionBinding, convertPropertyBinding, convertUpdateArguments } from '../../compiler_util/expression_converter'; import * as core from '../../core'; import { AstMemoryEfficientTransformer, Call, ImplicitReceiver, Interpolation, LiteralArray, LiteralPrimitive, PropertyRead } from '../../expression_parser/ast'; import { Lexer } from '../../expression_parser/lexer'; import { IvyParser } from '../../expression_parser/parser'; import * as html from '../../ml_parser/ast'; import { HtmlParser } from '../../ml_parser/html_parser'; import { WhitespaceVisitor } from '../../ml_parser/html_whitespaces'; import { DEFAULT_INTERPOLATION_CONFIG } from '../../ml_parser/interpolation_config'; import { isNgContainer as checkIsNgContainer, splitNsName } from '../../ml_parser/tags'; import { mapLiteral } from '../../output/map_util'; import * as o from '../../output/output_ast'; import { sanitizeIdentifier } from '../../parse_util'; import { DomElementSchemaRegistry } from '../../schema/dom_element_schema_registry'; import { isTrustedTypesSink } from '../../schema/trusted_types_sinks'; import { CssSelector } from '../../selector'; import { BindingParser } from '../../template_parser/binding_parser'; import { error, partitionArray } from '../../util'; import * as t from '../r3_ast'; import { Identifiers as R3 } from '../r3_identifiers'; import { htmlAstToRender3Ast } from '../r3_template_transform'; import { prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName } from '../util'; import { I18nContext } from './i18n/context'; import { createGoogleGetMsgStatements } from './i18n/get_msg_utils'; import { createLocalizeStatements } from './i18n/localize_utils'; import { I18nMetaVisitor } from './i18n/meta'; import { assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, hasI18nMeta, I18N_ICU_MAPPING_PREFIX, i18nFormatPlaceholderNames, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, TRANSLATION_VAR_PREFIX, wrapI18nPlaceholder } from './i18n/util'; import { StylingBuilder } from './styling_builder'; import { asLiteral, chainedInstruction, CONTEXT_NAME, getAttrsForDirectiveMatching, getInterpolationArgsLength, IMPLICIT_REFERENCE, invalid, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, RESTORED_VIEW_CONTEXT_NAME, trimTrailingNulls, unsupported } from './util'; // Selector attribute name of `<ng-content>` const NG_CONTENT_SELECT_ATTR = 'select'; // Attribute name of `ngProjectAs`. const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs'; // Global symbols available only inside event bindings. const EVENT_BINDING_SCOPE_GLOBALS = new Set(['$event']); // List of supported global targets for event listeners const GLOBAL_TARGET_RESOLVERS = new Map([['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]); export const LEADING_TRIVIA_CHARS = [' ', '\n', '\r', '\t']; // if (rf & flags) { .. } export function renderFlagCheckIfStmt(flags, statements) { return o.ifStmt(o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(flags), null, false), statements); } export function prepareEventListenerParameters(eventAst, handlerName = null, scope = null) { const { type, name, target, phase, handler } = eventAst; if (target && !GLOBAL_TARGET_RESOLVERS.has(target)) { throw new Error(`Unexpected global target '${target}' defined for '${name}' event. Supported list of global targets: ${Array.from(GLOBAL_TARGET_RESOLVERS.keys())}.`); } const eventArgumentName = '$event'; const implicitReceiverAccesses = new Set(); const implicitReceiverExpr = (scope === null || scope.bindingLevel === 0) ? o.variable(CONTEXT_NAME) : scope.getOrCreateSharedContextVar(0); const bindingExpr = convertActionBinding(scope, implicitReceiverExpr, handler, 'b', () => error('Unexpected interpolation'), eventAst.handlerSpan, implicitReceiverAccesses, EVENT_BINDING_SCOPE_GLOBALS); const statements = []; if (scope) { // `variableDeclarations` needs to run first, because // `restoreViewStatement` depends on the result. statements.push(...scope.variableDeclarations()); statements.unshift(...scope.restoreViewStatement()); } statements.push(...bindingExpr.render3Stmts); const eventName = type === 1 /* Animation */ ? prepareSyntheticListenerName(name, phase) : name; const fnName = handlerName && sanitizeIdentifier(handlerName); const fnArgs = []; if (implicitReceiverAccesses.has(eventArgumentName)) { fnArgs.push(new o.FnParam(eventArgumentName, o.DYNAMIC_TYPE)); } const handlerFn = o.fn(fnArgs, statements, o.INFERRED_TYPE, null, fnName); const params = [o.literal(eventName), handlerFn]; if (target) { params.push(o.literal(false), // `useCapture` flag, defaults to `false` o.importExpr(GLOBAL_TARGET_RESOLVERS.get(target))); } return params; } function createComponentDefConsts() { return { prepareStatements: [], constExpressions: [], i18nVarRefsCache: new Map(), }; } export class TemplateDefinitionBuilder { constructor(constantPool, parentBindingScope, level = 0, contextName, i18nContext, templateIndex, templateName, directiveMatcher, directives, pipeTypeByName, pipes, _namespace, relativeContextFilePath, i18nUseExternalIds, _constants = createComponentDefConsts()) { this.constantPool = constantPool; this.level = level; this.contextName = contextName; this.i18nContext = i18nContext; this.templateIndex = templateIndex; this.templateName = templateName; this.directiveMatcher = directiveMatcher; this.directives = directives; this.pipeTypeByName = pipeTypeByName; this.pipes = pipes; this._namespace = _namespace; this.i18nUseExternalIds = i18nUseExternalIds; this._constants = _constants; this._dataIndex = 0; this._bindingContext = 0; this._prefixCode = []; /** * List of callbacks to generate creation mode instructions. We store them here as we process * the template so bindings in listeners are resolved only once all nodes have been visited. * This ensures all local refs and context variables are available for matching. */ this._creationCodeFns = []; /** * List of callbacks to generate update mode instructions. We store them here as we process * the template so bindings are resolved only once all nodes have been visited. This ensures * all local refs and context variables are available for matching. */ this._updateCodeFns = []; /** Index of the currently-selected node. */ this._currentIndex = 0; /** Temporary variable declarations generated from visiting pipes, literals, etc. */ this._tempVariables = []; /** * List of callbacks to build nested templates. Nested templates must not be visited until * after the parent template has finished visiting all of its nodes. This ensures that all * local ref bindings in nested templates are able to find local ref values if the refs * are defined after the template declaration. */ this._nestedTemplateFns = []; this._unsupported = unsupported; // i18n context local to this template this.i18n = null; // Number of slots to reserve for pureFunctions this._pureFunctionSlots = 0; // Number of binding slots this._bindingSlots = 0; // Projection slots found in the template. Projection slots can distribute projected // nodes based on a selector, or can just use the wildcard selector to match // all nodes which aren't matching any selector. this._ngContentReservedSlots = []; // Number of non-default selectors found in all parent templates of this template. We need to // track it to properly adjust projection slot index in the `projection` instruction. this._ngContentSelectorsOffset = 0; // Expression that should be used as implicit receiver when converting template // expressions to output AST. this._implicitReceiverExpr = null; // These should be handled in the template or element directly. this.visitReference = invalid; this.visitVariable = invalid; this.visitTextAttribute = invalid; this.visitBoundAttribute = invalid; this.visitBoundEvent = invalid; this._bindingScope = parentBindingScope.nestedScope(level); // Turn the relative context file path into an identifier by replacing non-alphanumeric // characters with underscores. this.fileBasedI18nSuffix = relativeContextFilePath.replace(/[^A-Za-z0-9]/g, '_') + '_'; this._valueConverter = new ValueConverter(constantPool, () => this.allocateDataSlot(), (numSlots) => this.allocatePureFunctionSlots(numSlots), (name, localName, slot, value) => { const pipeType = pipeTypeByName.get(name); if (pipeType) { this.pipes.add(pipeType); } this._bindingScope.set(this.level, localName, value); this.creationInstruction(null, R3.pipe, [o.literal(slot), o.literal(name)]); }); } buildTemplateFunction(nodes, variables, ngContentSelectorsOffset = 0, i18n) { this._ngContentSelectorsOffset = ngContentSelectorsOffset; if (this._namespace !== R3.namespaceHTML) { this.creationInstruction(null, this._namespace); } // Create variable bindings variables.forEach(v => this.registerContextVariables(v)); // Initiate i18n context in case: // - this template has parent i18n context // - or the template has i18n meta associated with it, // but it's not initiated by the Element (e.g. <ng-template i18n>) const initI18nContext = this.i18nContext || (isI18nRootNode(i18n) && !isSingleI18nIcu(i18n) && !(isSingleElementTemplate(nodes) && nodes[0].i18n === i18n)); const selfClosingI18nInstruction = hasTextChildrenOnly(nodes); if (initI18nContext) { this.i18nStart(null, i18n, selfClosingI18nInstruction); } // This is the initial pass through the nodes of this template. In this pass, we // queue all creation mode and update mode instructions for generation in the second // pass. It's necessary to separate the passes to ensure local refs are defined before // resolving bindings. We also count bindings in this pass as we walk bound expressions. t.visitAll(this, nodes); // Add total binding count to pure function count so pure function instructions are // generated with the correct slot offset when update instructions are processed. this._pureFunctionSlots += this._bindingSlots; // Pipes are walked in the first pass (to enqueue `pipe()` creation instructions and // `pipeBind` update instructions), so we have to update the slot offsets manually // to account for bindings. this._valueConverter.updatePipeSlotOffsets(this._bindingSlots); // Nested templates must be processed before creation instructions so template() // instructions can be generated with the correct internal const count. this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn()); // Output the `projectionDef` instruction when some `<ng-content>` tags are present. // The `projectionDef` instruction is only emitted for the component template and // is skipped for nested templates (<ng-template> tags). if (this.level === 0 && this._ngContentReservedSlots.length) { const parameters = []; // By default the `projectionDef` instructions creates one slot for the wildcard // selector if no parameters are passed. Therefore we only want to allocate a new // array for the projection slots if the default projection slot is not sufficient. if (this._ngContentReservedSlots.length > 1 || this._ngContentReservedSlots[0] !== '*') { const r3ReservedSlots = this._ngContentReservedSlots.map(s => s !== '*' ? core.parseSelectorToR3Selector(s) : s); parameters.push(this.constantPool.getConstLiteral(asLiteral(r3ReservedSlots), true)); } // Since we accumulate ngContent selectors while processing template elements, // we *prepend* `projectionDef` to creation instructions block, to put it before // any `projection` instructions this.creationInstruction(null, R3.projectionDef, parameters, /* prepend */ true); } if (initI18nContext) { this.i18nEnd(null, selfClosingI18nInstruction); } // Generate all the creation mode instructions (e.g. resolve bindings in listeners) const creationStatements = this._creationCodeFns.map((fn) => fn()); // Generate all the update mode instructions (e.g. resolve property or text bindings) const updateStatements = this._updateCodeFns.map((fn) => fn()); // Variable declaration must occur after binding resolution so we can generate context // instructions that build on each other. // e.g. const b = nextContext().$implicit(); const b = nextContext(); const creationVariables = this._bindingScope.viewSnapshotStatements(); const updateVariables = this._bindingScope.variableDeclarations().concat(this._tempVariables); const creationBlock = creationStatements.length > 0 ? [renderFlagCheckIfStmt(1 /* Create */, creationVariables.concat(creationStatements))] : []; const updateBlock = updateStatements.length > 0 ? [renderFlagCheckIfStmt(2 /* Update */, updateVariables.concat(updateStatements))] : []; return o.fn( // i.e. (rf: RenderFlags, ctx: any) [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)], [ // Temporary variable declarations for query refresh (i.e. let _t: any;) ...this._prefixCode, // Creating mode (i.e. if (rf & RenderFlags.Create) { ... }) ...creationBlock, // Binding and refresh mode (i.e. if (rf & RenderFlags.Update) {...}) ...updateBlock, ], o.INFERRED_TYPE, null, this.templateName); } // LocalResolver getLocal(name) { return this._bindingScope.get(name); } // LocalResolver notifyImplicitReceiverUse() { this._bindingScope.notifyImplicitReceiverUse(); } // LocalResolver maybeRestoreView() { this._bindingScope.maybeRestoreView(); } i18nTranslate(message, params = {}, ref, transformFn) { const _ref = ref || this.i18nGenerateMainBlockVar(); // Closure Compiler requires const names to start with `MSG_` but disallows any other const to // start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call const closureVar = this.i18nGenerateClosureVar(message.id); const statements = getTranslationDeclStmts(message, _ref, closureVar, params, transformFn); this._constants.prepareStatements.push(...statements); return _ref; } registerContextVariables(variable) { const scopedName = this._bindingScope.freshReferenceName(); const retrievalLevel = this.level; const lhs = o.variable(variable.name + scopedName); this._bindingScope.set(retrievalLevel, variable.name, lhs, 1 /* CONTEXT */, (scope, relativeLevel) => { let rhs; if (scope.bindingLevel === retrievalLevel) { if (scope.isListenerScope() && scope.hasRestoreViewVariable()) { // e.g. restoredCtx. // We have to get the context from a view reference, if one is available, because // the context that was passed in during creation may not be correct anymore. // For more information see: https://github.com/angular/angular/pull/40360. rhs = o.variable(RESTORED_VIEW_CONTEXT_NAME); scope.notifyRestoredViewContextUse(); } else { // e.g. ctx rhs = o.variable(CONTEXT_NAME); } } else { const sharedCtxVar = scope.getSharedContextName(retrievalLevel); // e.g. ctx_r0 OR x(2); rhs = sharedCtxVar ? sharedCtxVar : generateNextContextExpr(relativeLevel); } // e.g. const $item$ = x(2).$implicit; return [lhs.set(rhs.prop(variable.value || IMPLICIT_REFERENCE)).toConstDecl()]; }); } i18nAppendBindings(expressions) { if (expressions.length > 0) { expressions.forEach(expression => this.i18n.appendBinding(expression)); } } i18nBindProps(props) { const bound = {}; Object.keys(props).forEach(key => { const prop = props[key]; if (prop instanceof t.Text) { bound[key] = o.literal(prop.value); } else { const value = prop.value.visit(this._valueConverter); this.allocateBindingSlots(value); if (value instanceof Interpolation) { const { strings, expressions } = value; const { id, bindings } = this.i18n; const label = assembleI18nBoundString(strings, bindings.size, id); this.i18nAppendBindings(expressions); bound[key] = o.literal(label); } } }); return bound; } // Generates top level vars for i18n blocks (i.e. `i18n_N`). i18nGenerateMainBlockVar() { return o.variable(this.constantPool.uniqueName(TRANSLATION_VAR_PREFIX)); } // Generates vars with Closure-specific names for i18n blocks (i.e. `MSG_XXX`). i18nGenerateClosureVar(messageId) { let name; const suffix = this.fileBasedI18nSuffix.toUpperCase(); if (this.i18nUseExternalIds) { const prefix = getTranslationConstPrefix(`EXTERNAL_`); const uniqueSuffix = this.constantPool.uniqueName(suffix); name = `${prefix}${sanitizeIdentifier(messageId)}$$${uniqueSuffix}`; } else { const prefix = getTranslationConstPrefix(suffix); name = this.constantPool.uniqueName(prefix); } return o.variable(name); } i18nUpdateRef(context) { const { icus, meta, isRoot, isResolved, isEmitted } = context; if (isRoot && isResolved && !isEmitted && !isSingleI18nIcu(meta)) { context.isEmitted = true; const placeholders = context.getSerializedPlaceholders(); let icuMapping = {}; let params = placeholders.size ? placeholdersToParams(placeholders) : {}; if (icus.size) { icus.forEach((refs, key) => { if (refs.length === 1) { // if we have one ICU defined for a given // placeholder - just output its reference params[key] = refs[0]; } else { // ... otherwise we need to activate post-processing // to replace ICU placeholders with proper values const placeholder = wrapI18nPlaceholder(`${I18N_ICU_MAPPING_PREFIX}${key}`); params[key] = o.literal(placeholder); icuMapping[key] = o.literalArr(refs); } }); } // translation requires post processing in 2 cases: // - if we have placeholders with multiple values (ex. `START_DIV`: [�#1�, �#2�, ...]) // - if we have multiple ICUs that refer to the same placeholder name const needsPostprocessing = Array.from(placeholders.values()).some((value) => value.length > 1) || Object.keys(icuMapping).length; let transformFn; if (needsPostprocessing) { transformFn = (raw) => { const args = [raw]; if (Object.keys(icuMapping).length) { args.push(mapLiteral(icuMapping, true)); } return instruction(null, R3.i18nPostprocess, args); }; } this.i18nTranslate(meta, params, context.ref, transformFn); } } i18nStart(span = null, meta, selfClosing) { const index = this.allocateDataSlot(); this.i18n = this.i18nContext ? this.i18nContext.forkChildContext(index, this.templateIndex, meta) : new I18nContext(index, this.i18nGenerateMainBlockVar(), 0, this.templateIndex, meta); // generate i18nStart instruction const { id, ref } = this.i18n; const params = [o.literal(index), this.addToConsts(ref)]; if (id > 0) { // do not push 3rd argument (sub-block id) // into i18nStart call for top level i18n context params.push(o.literal(id)); } this.creationInstruction(span, selfClosing ? R3.i18n : R3.i18nStart, params); } i18nEnd(span = null, selfClosing) { if (!this.i18n) { throw new Error('i18nEnd is executed with no i18n context present'); } if (this.i18nContext) { this.i18nContext.reconcileChildContext(this.i18n); this.i18nUpdateRef(this.i18nContext); } else { this.i18nUpdateRef(this.i18n); } // setup accumulated bindings const { index, bindings } = this.i18n; if (bindings.size) { const chainBindings = []; bindings.forEach(binding => { chainBindings.push({ sourceSpan: span, value: () => this.convertPropertyBinding(binding) }); }); // for i18n block, advance to the most recent element index (by taking the current number of // elements and subtracting one) before invoking `i18nExp` instructions, to make sure the // necessary lifecycle hooks of components/directives are properly flushed. this.updateInstructionChainWithAdvance(this.getConstCount() - 1, R3.i18nExp, chainBindings); this.updateInstruction(span, R3.i18nApply, [o.literal(index)]); } if (!selfClosing) { this.creationInstruction(span, R3.i18nEnd); } this.i18n = null; // reset local i18n context } i18nAttributesInstruction(nodeIndex, attrs, sourceSpan) { let hasBindings = false; const i18nAttrArgs = []; const bindings = []; attrs.forEach(attr => { const message = attr.i18n; const converted = attr.value.visit(this._valueConverter); this.allocateBindingSlots(converted); if (converted instanceof Interpolation) { const placeholders = assembleBoundTextPlaceholders(message); const params = placeholdersToParams(placeholders); i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params)); converted.expressions.forEach(expression => { hasBindings = true; bindings.push({ sourceSpan, value: () => this.convertPropertyBinding(expression), }); }); } }); if (bindings.length > 0) { this.updateInstructionChainWithAdvance(nodeIndex, R3.i18nExp, bindings); } if (i18nAttrArgs.length > 0) { const index = o.literal(this.allocateDataSlot()); const constIndex = this.addToConsts(o.literalArr(i18nAttrArgs)); this.creationInstruction(sourceSpan, R3.i18nAttributes, [index, constIndex]); if (hasBindings) { this.updateInstruction(sourceSpan, R3.i18nApply, [index]); } } } getNamespaceInstruction(namespaceKey) { switch (namespaceKey) { case 'math': return R3.namespaceMathML; case 'svg': return R3.namespaceSVG; default: return R3.namespaceHTML; } } addNamespaceInstruction(nsInstruction, element) { this._namespace = nsInstruction; this.creationInstruction(element.startSourceSpan, nsInstruction); } /** * Adds an update instruction for an interpolated property or attribute, such as * `prop="{{value}}"` or `attr.title="{{value}}"` */ interpolatedUpdateInstruction(instruction, elementIndex, attrName, input, value, params) { this.updateInstructionWithAdvance(elementIndex, input.sourceSpan, instruction, () => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]); } visitContent(ngContent) { const slot = this.allocateDataSlot(); const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length; const parameters = [o.literal(slot)]; this._ngContentReservedSlots.push(ngContent.selector); const nonContentSelectAttributes = ngContent.attributes.filter(attr => attr.name.toLowerCase() !== NG_CONTENT_SELECT_ATTR); const attributes = this.getAttributeExpressions(ngContent.name, nonContentSelectAttributes, [], []); if (attributes.length > 0) { parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes)); } else if (projectionSlotIdx !== 0) { parameters.push(o.literal(projectionSlotIdx)); } this.creationInstruction(ngContent.sourceSpan, R3.projection, parameters); if (this.i18n) { this.i18n.appendProjection(ngContent.i18n, slot); } } visitElement(element) { const elementIndex = this.allocateDataSlot(); const stylingBuilder = new StylingBuilder(null); let isNonBindableMode = false; const isI18nRootElement = isI18nRootNode(element.i18n) && !isSingleI18nIcu(element.i18n); const outputAttrs = []; const [namespaceKey, elementName] = splitNsName(element.name); const isNgContainer = checkIsNgContainer(element.name); // Handle styling, i18n, ngNonBindable attributes for (const attr of element.attributes) { const { name, value } = attr; if (name === NON_BINDABLE_ATTR) { isNonBindableMode = true; } else if (name === 'style') { stylingBuilder.registerStyleAttr(value); } else if (name === 'class') { stylingBuilder.registerClassAttr(value); } else { outputAttrs.push(attr); } } // Match directives on non i18n attributes this.matchDirectives(element.name, element); // Regular element or ng-container creation mode const parameters = [o.literal(elementIndex)]; if (!isNgContainer) { parameters.push(o.literal(elementName)); } // Add the attributes const allOtherInputs = []; const boundI18nAttrs = []; element.inputs.forEach(input => { const stylingInputWasSet = stylingBuilder.registerBoundInput(input); if (!stylingInputWasSet) { if (input.type === 0 /* Property */ && input.i18n) { boundI18nAttrs.push(input); } else { allOtherInputs.push(input); } } }); // add attributes for directive and projection matching purposes const attributes = this.getAttributeExpressions(element.name, outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], boundI18nAttrs); parameters.push(this.addAttrsToConsts(attributes)); // local refs (ex.: <div #foo #bar="baz">) const refs = this.prepareRefsArray(element.references); parameters.push(this.addToConsts(refs)); const wasInNamespace = this._namespace; const currentNamespace = this.getNamespaceInstruction(namespaceKey); // If the namespace is changing now, include an instruction to change it // during element creation. if (currentNamespace !== wasInNamespace) { this.addNamespaceInstruction(currentNamespace, element); } if (this.i18n) { this.i18n.appendElement(element.i18n, elementIndex); } // Note that we do not append text node instructions and ICUs inside i18n section, // so we exclude them while calculating whether current element has children const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) : element.children.length > 0; const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes && element.outputs.length === 0 && boundI18nAttrs.length === 0 && !hasChildren; const createSelfClosingI18nInstruction = !createSelfClosingInstruction && hasTextChildrenOnly(element.children); if (createSelfClosingInstruction) { this.creationInstruction(element.sourceSpan, isNgContainer ? R3.elementContainer : R3.element, trimTrailingNulls(parameters)); } else { this.creationInstruction(element.startSourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart, trimTrailingNulls(parameters)); if (isNonBindableMode) { this.creationInstruction(element.startSourceSpan, R3.disableBindings); } if (boundI18nAttrs.length > 0) { this.i18nAttributesInstruction(elementIndex, boundI18nAttrs, element.startSourceSpan ?? element.sourceSpan); } // Generate Listeners (outputs) if (element.outputs.length > 0) { const listeners = element.outputs.map((outputAst) => ({ sourceSpan: outputAst.sourceSpan, params: this.prepareListenerParameter(element.name, outputAst, elementIndex) })); this.creationInstructionChain(R3.listener, listeners); } // Note: it's important to keep i18n/i18nStart instructions after i18nAttributes and // listeners, to make sure i18nAttributes instruction targets current element at runtime. if (isI18nRootElement) { this.i18nStart(element.startSourceSpan, element.i18n, createSelfClosingI18nInstruction); } } // the code here will collect all update-level styling instructions and add them to the // update block of the template function AOT code. Instructions like `styleProp`, // `styleMap`, `classMap`, `classProp` // are all generated and assigned in the code below. const stylingInstructions = stylingBuilder.buildUpdateLevelInstructions(this._valueConverter); const limit = stylingInstructions.length - 1; for (let i = 0; i <= limit; i++) { const instruction = stylingInstructions[i]; this._bindingSlots += this.processStylingUpdateInstruction(elementIndex, instruction); } // the reason why `undefined` is used is because the renderer understands this as a // special value to symbolize that there is no RHS to this binding // TODO (matsko): revisit this once FW-959 is approached const emptyValueBindInstruction = o.literal(undefined); const propertyBindings = []; const attributeBindings = []; // Generate element input bindings allOtherInputs.forEach(input => { const inputType = input.type; if (inputType === 4 /* Animation */) { const value = input.value.visit(this._valueConverter); // animation bindings can be presented in the following formats: // 1. [@binding]="fooExp" // 2. [@binding]="{value:fooExp, params:{...}}" // 3. [@binding] // 4. @binding // All formats will be valid for when a synthetic binding is created. // The reasoning for this is because the renderer should get each // synthetic binding value in the order of the array that they are // defined in... const hasValue = value instanceof LiteralPrimitive ? !!value.value : true; this.allocateBindingSlots(value); propertyBindings.push({ name: prepareSyntheticPropertyName(input.name), sourceSpan: input.sourceSpan, value: () => hasValue ? this.convertPropertyBinding(value) : emptyValueBindInstruction }); } else { // we must skip attributes with associated i18n context, since these attributes are handled // separately and corresponding `i18nExp` and `i18nApply` instructions will be generated if (input.i18n) return; const value = input.value.visit(this._valueConverter); if (value !== undefined) { const params = []; const [attrNamespace, attrName] = splitNsName(input.name); const isAttributeBinding = inputType === 1 /* Attribute */; const sanitizationRef = resolveSanitizationFn(input.securityContext, isAttributeBinding); if (sanitizationRef) params.push(sanitizationRef); if (attrNamespace) { const namespaceLiteral = o.literal(attrNamespace); if (sanitizationRef) { params.push(namespaceLiteral); } else { // If there wasn't a sanitization ref, we need to add // an extra param so that we can pass in the namespace. params.push(o.literal(null), namespaceLiteral); } } this.allocateBindingSlots(value); if (inputType === 0 /* Property */) { if (value instanceof Interpolation) { // prop="{{value}}" and friends this.interpolatedUpdateInstruction(getPropertyInterpolationExpression(value), elementIndex, attrName, input, value, params); } else { // [prop]="value" // Collect all the properties so that we can chain into a single function at the end. propertyBindings.push({ name: attrName, sourceSpan: input.sourceSpan, value: () => this.convertPropertyBinding(value), params }); } } else if (inputType === 1 /* Attribute */) { if (value instanceof Interpolation && getInterpolationArgsLength(value) > 1) { // attr.name="text{{value}}" and friends this.interpolatedUpdateInstruction(getAttributeInterpolationExpression(value), elementIndex, attrName, input, value, params); } else { const boundValue = value instanceof Interpolation ? value.expressions[0] : value; // [attr.name]="value" or attr.name="{{value}}" // Collect the attribute bindings so that they can be chained at the end. attributeBindings.push({ name: attrName, sourceSpan: input.sourceSpan, value: () => this.convertPropertyBinding(boundValue), params }); } } else { // class prop this.updateInstructionWithAdvance(elementIndex, input.sourceSpan, R3.classProp, () => { return [ o.literal(elementIndex), o.literal(attrName), this.convertPropertyBinding(value), ...params ]; }); } } } }); if (propertyBindings.length > 0) { this.updateInstructionChainWithAdvance(elementIndex, R3.property, propertyBindings); } if (attributeBindings.length > 0) { this.updateInstructionChainWithAdvance(elementIndex, R3.attribute, attributeBindings); } // Traverse element child nodes t.visitAll(this, element.children); if (!isI18nRootElement && this.i18n) { this.i18n.appendElement(element.i18n, elementIndex, true); } if (!createSelfClosingInstruction) { // Finish element construction mode. const span = element.endSourceSpan ?? element.sourceSpan; if (isI18nRootElement) { this.i18nEnd(span, createSelfClosingI18nInstruction); } if (isNonBindableMode) { this.creationInstruction(span, R3.enableBindings); } this.creationInstruction(span, isNgContainer ? R3.elementContainerEnd : R3.elementEnd); } } visitTemplate(template) { const NG_TEMPLATE_TAG_NAME = 'ng-template'; const templateIndex = this.allocateDataSlot(); if (this.i18n) { this.i18n.appendTemplate(template.i18n, templateIndex); } const tagNameWithoutNamespace = template.tagName ? splitNsName(template.tagName)[1] : template.tagName; const contextName = `${this.contextName}${template.tagName ? '_' + sanitizeIdentifier(template.tagName) : ''}_${templateIndex}`; const templateName = `${contextName}_Template`; const parameters = [ o.literal(templateIndex), o.variable(templateName), // We don't care about the tag's namespace here, because we infer // it based on the parent nodes inside the template instruction. o.literal(tagNameWithoutNamespace), ]; // find directives matching on a given <ng-template> node this.matchDirectives(NG_TEMPLATE_TAG_NAME, template); // prepare attributes parameter (including attributes used for directive matching) const attrsExprs = this.getAttributeExpressions(NG_TEMPLATE_TAG_NAME, template.attributes, template.inputs, template.outputs, undefined /* styles */, template.templateAttrs); parameters.push(this.addAttrsToConsts(attrsExprs)); // local refs (ex.: <ng-template #foo>) if (template.references && template.references.length) { const refs = this.prepareRefsArray(template.references); parameters.push(this.addToConsts(refs)); parameters.push(o.importExpr(R3.templateRefExtractor)); } // Create the template function const templateVisitor = new TemplateDefinitionBuilder(this.constantPool, this._bindingScope, this.level + 1, contextName, this.i18n, templateIndex, templateName, this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace, this.fileBasedI18nSuffix, this.i18nUseExternalIds, this._constants); // Nested templates must not be visited until after their parent templates have completed // processing, so they are queued here until after the initial pass. Otherwise, we wouldn't // be able to support bindings in nested templates to local refs that occur after the // template definition. e.g. <div *ngIf="showing">{{ foo }}</div> <div #foo></div> this._nestedTemplateFns.push(() => { const templateFunctionExpr = templateVisitor.buildTemplateFunction(template.children, template.variables, this._ngContentReservedSlots.length + this._ngContentSelectorsOffset, template.i18n); this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName)); if (templateVisitor._ngContentReservedSlots.length) { this._ngContentReservedSlots.push(...templateVisitor._ngContentReservedSlots); } }); // e.g. template(1, MyComp_Template_1) this.creationInstruction(template.sourceSpan, R3.templateCreate, () => { parameters.splice(2, 0, o.literal(templateVisitor.getConstCount()), o.literal(templateVisitor.getVarCount())); return trimTrailingNulls(parameters); }); // handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al; this.templatePropertyBindings(templateIndex, template.templateAttrs); // Only add normal input/output binding instructions on explicit <ng-template> elements. if (tagNameWithoutNamespace === NG_TEMPLATE_TAG_NAME) { const [i18nInputs, inputs] = partitionArray(template.inputs, hasI18nMeta); // Add i18n attributes that may act as inputs to directives. If such attributes are present, // generate `i18nAttributes` instruction. Note: we generate it only for explicit <ng-template> // elements, in case of inline templates, corresponding instructions will be generated in the // nested template function. if (i18nInputs.length > 0) { this.i18nAttributesInstruction(templateIndex, i18nInputs, template.startSourceSpan ?? template.sourceSpan); } // Add the input bindings if (inputs.length > 0) { this.templatePropertyBindings(templateIndex, inputs); } // Generate listeners for directive output if (template.outputs.length > 0) { const listeners = template.outputs.map((outputAst) => ({ sourceSpan: outputAst.sourceSpan, params: this.prepareListenerParameter('ng_template', outputAst, templateIndex) })); this.creationInstructionChain(R3.listener, listeners); } } } visitBoundText(text) { if (this.i18n) { const value = text.value.visit(this._valueConverter); this.allocateBindingSlots(value); if (value instanceof Interpolation) { this.i18n.appendBoundText(text.i18n); this.i18nAppendBindings(value.expressions); } return; } const nodeIndex = this.allocateDataSlot(); this.creationInstruction(text.sourceSpan, R3.text, [o.literal(nodeIndex)]); const value = text.value.visit(this._valueConverter); this.allocateBindingSlots(value); if (value instanceof Interpolation) { this.updateInstructionWithAdvance(nodeIndex, text.sourceSpan, getTextInterpolationExpression(value), () => this.getUpdateInstructionArguments(value)); } else { error('Text nodes should be interpolated and never bound directly.'); } } visitText(text) { // when a text element is located within a translatable // block, we exclude this text element from instructions set, // since it will be captured in i18n content and processed at runtime if (!this.i18n) { this.creationInstruction(text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), o.literal(text.value)]); } } visitIcu(icu) { let initWasInvoked = false; // if an ICU was created outside of i18n block, we still treat // it as a translatable entity and invoke i18nStart and i18nEnd // to generate i18n context and the necessary instructions if (!this.i18n) { initWasInvoked = true; this.i18nStart(null, icu.i18n, true); } const i18n = this.i18n; const vars = this.i18nBindProps(icu.vars); const placeholders = this.i18nBindProps(icu.placeholders); // output ICU directly and keep ICU reference in context const message = icu.i18n; // we always need post-processing function for ICUs, to make sure that: // - all placeholders in a form of {PLACEHOLDER} are replaced with actual values (note: // `goog.getMsg` does not process ICUs and uses the `{PLACEHOLDER}` format for placeholders // inside ICUs) // - all ICU vars (such as `VAR_SELECT` or `VAR_PLURAL`) are replaced with correct values const transformFn = (raw) => { const params = { ...vars, ...placeholders }; const formatted = i18nFormatPlaceholderNames(params, /* useCamelCase */ false); return instruction(null, R3.i18nPostprocess, [raw, mapLiteral(formatted, true)]); }; // in case the whole i18n message is a single ICU - we do not need to // create a separate top-level translation, we can use the root ref instead // and make this ICU a top-level translation // note: ICU placeholders are replaced with actual values in `i18nPostprocess` function // separately, so we do not pass placeholders into `i18nTranslate` function. if (isSingleI18nIcu(i18n.meta)) { this.i18nTranslate(message, /* placeholders */ {}, i18n.ref, transformFn); } else { // output ICU directly and keep ICU reference in context const ref = this.i18nTranslate(message, /* placeholders */ {}, /* ref */ undefined, transformFn); i18n.appendIcu(icuFromI18nMessage(message).name, ref); } if (initWasInvoked) { this.i18nEnd(null, true); } return null; } allocateDataSlot() { return this._dataIndex++; } getConstCount() { return this._dataIndex; } getVarCount() { return this._pureFunctionSlots; } getConsts() { return this._constants; } getNgContentSelectors() { return this._ngContentReservedSlots.length ? this.constantPool.getConstLiteral(asLiteral(this._ngContentReservedSlots), true) : null; } bindingContext() { return `${this._bindingContext++}`; } templatePropertyBindings(templateIndex, attrs) { const propertyBindings = []; attrs.forEach(input => { if (input instanceof t.BoundAttribute) { const value = input.value.visit(this._valueConverter); if (value !== undefined) { this.allocateBindingSlots(value); if (value instanceof Interpolation) { // Params typically contain attribute namespace and value sanitizer, which is applicable // for regular HTML elements, but not applicable for <ng-template> (since props act as // inputs to directives), so keep params array empty. const params = []; // prop="{{value}}" case this.interpolatedUpdateInstruction(getPropertyInterpolationExpression(value), templateIndex, input.name, input, value, params); } else { // [prop]="value" case propertyBindings.push({ name: input.name, sourceSpan: input.sourceSpan, value: () => this.convertPropertyBinding(value) }); } } } }); if (propertyBindings.length > 0) { this.updateInstructionChainWithAdvance(templateIndex, R3.property, propertyBindings); } } // Bindings must only be resolved after all local refs have been visited, so all // instructions are queued in callbacks that execute once the initial pass has completed. // Otherwise, we wouldn't be able to support local refs that are defined after their // bindings. e.g. {{ foo }} <div #foo></div> instructionFn(fns, span, reference, paramsOrFn, prepend = false) { fns[prepend ? 'unshift' : 'push'](() => { const params = Array.isArray(paramsOrFn) ? paramsOrFn : paramsOrFn(); return instruction(span, reference, params).toStmt(); }); } processStylingUpdateInstruction(elementIndex, instruction) { let allocateBindingSlots = 0; if (instruction) { const calls = []; instruction.calls.forEach(call => { allocateBindingSlots += call.allocateBind