@angular/compiler
Version:
Angular - the compiler library
931 lines • 302 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 { 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