refactor(compiler): compile{Component,Directive} take only local information (#23545)
Previously, the compileComponent() and compileDirective() APIs still required the output of global analysis, even though they only read local information from that output. With this refactor, compileComponent() and compileDirective() now define their inputs explicitly, with the new interfaces R3ComponentMetadata and R3DirectiveMetadata. compileComponentGlobal() and compileDirectiveGlobal() are introduced and convert from global analysis output into the new metadata format. This refactor also splits out the view compiler into separate files as r3_view_compiler_local.ts was getting unwieldy. Finally, this refactor also splits out generation of DI factory functions into a separate r3_factory utility as the logic is utilized between different compilers. PR Close #23545
This commit is contained in:

committed by
Igor Minar

parent
d01ec03f54
commit
b0eca85e51
178
packages/compiler/src/render3/view/api.ts
Normal file
178
packages/compiler/src/render3/view/api.ts
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 * as o from '../../output/output_ast';
|
||||
import {ParseSourceSpan} from '../../parse_util';
|
||||
import * as t from '../r3_ast';
|
||||
import {R3DependencyMetadata} from '../r3_factory';
|
||||
|
||||
/**
|
||||
* Information needed to compile a directive for the render3 runtime.
|
||||
*/
|
||||
export interface R3DirectiveMetadata {
|
||||
/**
|
||||
* Name of the directive type.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* An expression representing a reference to the directive itself.
|
||||
*/
|
||||
type: o.Expression;
|
||||
|
||||
/**
|
||||
* A source span for the directive type.
|
||||
*/
|
||||
typeSourceSpan: ParseSourceSpan;
|
||||
|
||||
/**
|
||||
* Dependencies of the directive's constructor.
|
||||
*/
|
||||
deps: R3DependencyMetadata[];
|
||||
|
||||
/**
|
||||
* Unparsed selector of the directive, or `null` if there was no selector.
|
||||
*/
|
||||
selector: string|null;
|
||||
|
||||
/**
|
||||
* Information about the content queries made by the directive.
|
||||
*/
|
||||
queries: R3QueryMetadata[];
|
||||
|
||||
/**
|
||||
* Mappings indicating how the directive interacts with its host element (host bindings,
|
||||
* listeners, etc).
|
||||
*/
|
||||
host: {
|
||||
/**
|
||||
* A mapping of attribute binding keys to unparsed expressions.
|
||||
*/
|
||||
attributes: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* A mapping of event binding keys to unparsed expressions.
|
||||
*/
|
||||
listeners: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* A mapping of property binding keys to unparsed expressions.
|
||||
*/
|
||||
properties: {[key: string]: string};
|
||||
};
|
||||
|
||||
/**
|
||||
* A mapping of input field names to the property names.
|
||||
*/
|
||||
inputs: {[field: string]: string};
|
||||
|
||||
/**
|
||||
* A mapping of output field names to the property names.
|
||||
*/
|
||||
outputs: {[field: string]: string};
|
||||
}
|
||||
|
||||
/**
|
||||
* Information needed to compile a component for the render3 runtime.
|
||||
*/
|
||||
export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||
/**
|
||||
* Information about the component's template.
|
||||
*/
|
||||
template: {
|
||||
/**
|
||||
* Parsed nodes of the template.
|
||||
*/
|
||||
nodes: t.Node[];
|
||||
|
||||
/**
|
||||
* Whether the template includes <ng-content> tags.
|
||||
*/
|
||||
hasNgContent: boolean;
|
||||
|
||||
/**
|
||||
* Selectors found in the <ng-content> tags in the template.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Information about usage of specific lifecycle events which require special treatment in the
|
||||
* code generator.
|
||||
*/
|
||||
lifecycle: {
|
||||
/**
|
||||
* Whether the component uses NgOnChanges.
|
||||
*/
|
||||
usesOnChanges: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Information about the view queries made by the component.
|
||||
*/
|
||||
viewQueries: R3QueryMetadata[];
|
||||
|
||||
/**
|
||||
* A map of pipe names to an expression referencing the pipe type which are in the scope of the
|
||||
* compilation.
|
||||
*/
|
||||
pipes: Map<string, o.Expression>;
|
||||
|
||||
/**
|
||||
* A map of directive selectors to an expression referencing the directive type which are in the
|
||||
* scope of the compilation.
|
||||
*/
|
||||
directives: Map<string, o.Expression>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information needed to compile a query (view or content).
|
||||
*/
|
||||
export interface R3QueryMetadata {
|
||||
/**
|
||||
* Name of the property on the class to update with query results.
|
||||
*/
|
||||
propertyName: string;
|
||||
|
||||
/**
|
||||
* Whether to read only the first matching result, or an array of results.
|
||||
*/
|
||||
first: boolean;
|
||||
|
||||
/**
|
||||
* Either an expression representing a type for the query predicate, or a set of string selectors.
|
||||
*/
|
||||
predicate: o.Expression|string[];
|
||||
|
||||
/**
|
||||
* Whether to include only direct children or all descendants.
|
||||
*/
|
||||
descendants: boolean;
|
||||
|
||||
/**
|
||||
* An expression representing a type to read from each matched node, or null if the node itself
|
||||
* is to be returned.
|
||||
*/
|
||||
read: o.Expression|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output of render3 directive compilation.
|
||||
*/
|
||||
export interface R3DirectiveDef {
|
||||
expression: o.Expression;
|
||||
type: o.Type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output of render3 component compilation.
|
||||
*/
|
||||
export interface R3ComponentDef {
|
||||
expression: o.Expression;
|
||||
type: o.Type;
|
||||
}
|
444
packages/compiler/src/render3/view/compiler.ts
Normal file
444
packages/compiler/src/render3/view/compiler.ts
Normal file
@ -0,0 +1,444 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {StaticSymbol} from '../../aot/static_symbol';
|
||||
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, sanitizeIdentifier, tokenReference} from '../../compile_metadata';
|
||||
import {CompileReflector} from '../../compile_reflector';
|
||||
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
||||
import {ConstantPool, DefinitionKind} from '../../constant_pool';
|
||||
import * as core from '../../core';
|
||||
import {AST, AstMemoryEfficientTransformer, BindingPipe, BoundElementBindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast';
|
||||
import {Identifiers} from '../../identifiers';
|
||||
import {LifecycleHooks} from '../../lifecycle_reflector';
|
||||
import * as o from '../../output/output_ast';
|
||||
import {ParseSourceSpan, typeSourceSpan} from '../../parse_util';
|
||||
import {CssSelector, SelectorMatcher} from '../../selector';
|
||||
import {BindingParser} from '../../template_parser/binding_parser';
|
||||
import {OutputContext, error} from '../../util';
|
||||
|
||||
import * as t from './../r3_ast';
|
||||
import {R3DependencyMetadata, R3ResolvedDependencyType, compileFactoryFunction, dependenciesFromGlobalMetadata} from './../r3_factory';
|
||||
import {Identifiers as R3} from './../r3_identifiers';
|
||||
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
|
||||
import {BindingScope, TemplateDefinitionBuilder} from './template';
|
||||
import {CONTEXT_NAME, DefinitionMap, ID_SEPARATOR, MEANING_SEPARATOR, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator, unsupported} from './util';
|
||||
|
||||
function baseDirectiveFields(
|
||||
meta: R3DirectiveMetadata, constantPool: ConstantPool,
|
||||
bindingParser: BindingParser): DefinitionMap {
|
||||
const definitionMap = new DefinitionMap();
|
||||
|
||||
// e.g. `type: MyDirective`
|
||||
definitionMap.set('type', meta.type);
|
||||
|
||||
// e.g. `selectors: [['', 'someDir', '']]`
|
||||
definitionMap.set('selectors', createDirectiveSelector(meta.selector !));
|
||||
|
||||
const queryDefinitions = createQueryDefinitions(meta.queries, constantPool);
|
||||
|
||||
// e.g. `factory: () => new MyApp(injectElementRef())`
|
||||
definitionMap.set('factory', compileFactoryFunction({
|
||||
name: meta.name,
|
||||
fnOrClass: meta.type,
|
||||
deps: meta.deps,
|
||||
useNew: true,
|
||||
injectFn: R3.directiveInject,
|
||||
useOptionalParam: false,
|
||||
extraResults: queryDefinitions,
|
||||
}));
|
||||
|
||||
// e.g. `hostBindings: (dirIndex, elIndex) => { ... }
|
||||
definitionMap.set('hostBindings', createHostBindingsFunction(meta, bindingParser));
|
||||
|
||||
// e.g. `attributes: ['role', 'listbox']`
|
||||
definitionMap.set('attributes', createHostAttributesArray(meta));
|
||||
|
||||
// e.g 'inputs: {a: 'a'}`
|
||||
definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs));
|
||||
|
||||
// e.g 'outputs: {a: 'a'}`
|
||||
definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs));
|
||||
|
||||
return definitionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a directive for the render3 runtime as defined by the `R3DirectiveMetadata`.
|
||||
*/
|
||||
export function compileDirective(
|
||||
meta: R3DirectiveMetadata, constantPool: ConstantPool,
|
||||
bindingParser: BindingParser): R3DirectiveDef {
|
||||
const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
|
||||
const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]);
|
||||
const type =
|
||||
new o.ExpressionType(o.importExpr(R3.DirectiveDef, [new o.ExpressionType(meta.type)]));
|
||||
return {expression, type};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a component for the render3 runtime as defined by the `R3ComponentMetadata`.
|
||||
*/
|
||||
export function compileComponent(
|
||||
meta: R3ComponentMetadata, constantPool: ConstantPool,
|
||||
bindingParser: BindingParser): R3ComponentDef {
|
||||
const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
|
||||
|
||||
const selector = meta.selector && CssSelector.parse(meta.selector);
|
||||
const firstSelector = selector && selector[0];
|
||||
|
||||
// e.g. `attr: ["class", ".my.app"]`
|
||||
// This is optional an only included if the first selector of a component specifies attributes.
|
||||
if (firstSelector) {
|
||||
const selectorAttributes = firstSelector.getAttrs();
|
||||
if (selectorAttributes.length) {
|
||||
definitionMap.set(
|
||||
'attrs', constantPool.getConstLiteral(
|
||||
o.literalArr(selectorAttributes.map(
|
||||
value => value != null ? o.literal(value) : o.literal(undefined))),
|
||||
/* forceShared */ true));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the CSS matcher that recognize directive
|
||||
let directiveMatcher: SelectorMatcher|null = null;
|
||||
|
||||
if (meta.directives.size) {
|
||||
const matcher = new SelectorMatcher();
|
||||
meta.directives.forEach((expression, selector: string) => {
|
||||
matcher.addSelectables(CssSelector.parse(selector), expression);
|
||||
});
|
||||
directiveMatcher = matcher;
|
||||
}
|
||||
|
||||
// e.g. `template: function MyComponent_Template(_ctx, _cm) {...}`
|
||||
const templateTypeName = meta.name;
|
||||
const templateName = templateTypeName ? `${templateTypeName}_Template` : null;
|
||||
|
||||
const directivesUsed = new Set<o.Expression>();
|
||||
const pipesUsed = new Set<o.Expression>();
|
||||
|
||||
const template = meta.template;
|
||||
const templateFunctionExpression =
|
||||
new TemplateDefinitionBuilder(
|
||||
constantPool, CONTEXT_NAME, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName,
|
||||
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed)
|
||||
.buildTemplateFunction(
|
||||
template.nodes, [], template.hasNgContent, template.ngContentSelectors);
|
||||
|
||||
definitionMap.set('template', templateFunctionExpression);
|
||||
|
||||
// e.g. `directives: [MyDirective]`
|
||||
if (directivesUsed.size) {
|
||||
definitionMap.set('directives', o.literalArr(Array.from(directivesUsed)));
|
||||
}
|
||||
|
||||
// e.g. `pipes: [MyPipe]`
|
||||
if (pipesUsed.size) {
|
||||
definitionMap.set('pipes', o.literalArr(Array.from(pipesUsed)));
|
||||
}
|
||||
|
||||
// e.g. `features: [NgOnChangesFeature(MyComponent)]`
|
||||
const features: o.Expression[] = [];
|
||||
if (meta.lifecycle.usesOnChanges) {
|
||||
features.push(o.importExpr(R3.NgOnChangesFeature, null, null).callFn([meta.type]));
|
||||
}
|
||||
if (features.length) {
|
||||
definitionMap.set('features', o.literalArr(features));
|
||||
}
|
||||
|
||||
const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]);
|
||||
const type =
|
||||
new o.ExpressionType(o.importExpr(R3.ComponentDef, [new o.ExpressionType(meta.type)]));
|
||||
|
||||
return {expression, type};
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around `compileDirective` which depends on render2 global analysis data as its input
|
||||
* instead of the `R3DirectiveMetadata`.
|
||||
*
|
||||
* `R3DirectiveMetadata` is computed from `CompileDirectiveMetadata` and other statically reflected
|
||||
* information.
|
||||
*/
|
||||
export function compileDirectiveFromRender2(
|
||||
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
|
||||
bindingParser: BindingParser) {
|
||||
const name = identifierName(directive.type) !;
|
||||
name || error(`Cannot resolver the name of ${directive.type}`);
|
||||
|
||||
const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Directive);
|
||||
|
||||
const meta = directiveMetadataFromGlobalMetadata(directive, outputCtx, reflector);
|
||||
const res = compileDirective(meta, outputCtx.constantPool, bindingParser);
|
||||
|
||||
// Create the partial class to be merged with the actual class.
|
||||
outputCtx.statements.push(new o.ClassStmt(
|
||||
name, null,
|
||||
[new o.ClassField(definitionField, o.INFERRED_TYPE, [o.StmtModifier.Static], res.expression)],
|
||||
[], new o.ClassMethod(null, [], []), []));
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around `compileComponent` which depends on render2 global analysis data as its input
|
||||
* instead of the `R3DirectiveMetadata`.
|
||||
*
|
||||
* `R3ComponentMetadata` is computed from `CompileDirectiveMetadata` and other statically reflected
|
||||
* information.
|
||||
*/
|
||||
export function compileComponentFromRender2(
|
||||
outputCtx: OutputContext, component: CompileDirectiveMetadata, nodes: t.Node[],
|
||||
hasNgContent: boolean, ngContentSelectors: string[], reflector: CompileReflector,
|
||||
bindingParser: BindingParser, directiveTypeBySel: Map<string, any>,
|
||||
pipeTypeByName: Map<string, any>) {
|
||||
const name = identifierName(component.type) !;
|
||||
name || error(`Cannot resolver the name of ${component.type}`);
|
||||
|
||||
const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Component);
|
||||
|
||||
const summary = component.toSummary();
|
||||
|
||||
// Compute the R3ComponentMetadata from the CompileDirectiveMetadata
|
||||
const meta: R3ComponentMetadata = {
|
||||
...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector),
|
||||
selector: component.selector,
|
||||
template: {
|
||||
nodes, hasNgContent, ngContentSelectors,
|
||||
},
|
||||
lifecycle: {
|
||||
usesOnChanges:
|
||||
component.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges),
|
||||
},
|
||||
directives: typeMapToExpressionMap(directiveTypeBySel, outputCtx),
|
||||
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
|
||||
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
|
||||
};
|
||||
const res = compileComponent(meta, outputCtx.constantPool, bindingParser);
|
||||
|
||||
// Create the partial class to be merged with the actual class.
|
||||
outputCtx.statements.push(new o.ClassStmt(
|
||||
name, null,
|
||||
[new o.ClassField(definitionField, o.INFERRED_TYPE, [o.StmtModifier.Static], res.expression)],
|
||||
[], new o.ClassMethod(null, [], []), []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute `R3DirectiveMetadata` given `CompileDirectiveMetadata` and a `CompileReflector`.
|
||||
*/
|
||||
function directiveMetadataFromGlobalMetadata(
|
||||
directive: CompileDirectiveMetadata, outputCtx: OutputContext,
|
||||
reflector: CompileReflector): R3DirectiveMetadata {
|
||||
const summary = directive.toSummary();
|
||||
const name = identifierName(directive.type) !;
|
||||
name || error(`Cannot resolver the name of ${directive.type}`);
|
||||
|
||||
return {
|
||||
name,
|
||||
type: outputCtx.importExpr(directive.type.reference),
|
||||
typeSourceSpan:
|
||||
typeSourceSpan(directive.isComponent ? 'Component' : 'Directive', directive.type),
|
||||
selector: directive.selector,
|
||||
deps: dependenciesFromGlobalMetadata(directive.type, outputCtx, reflector),
|
||||
queries: queriesFromGlobalMetadata(directive.queries, outputCtx),
|
||||
host: {
|
||||
attributes: directive.hostAttributes,
|
||||
listeners: summary.hostListeners,
|
||||
properties: summary.hostProperties,
|
||||
},
|
||||
inputs: directive.inputs,
|
||||
outputs: directive.outputs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `CompileQueryMetadata` into `R3QueryMetadata`.
|
||||
*/
|
||||
function queriesFromGlobalMetadata(
|
||||
queries: CompileQueryMetadata[], outputCtx: OutputContext): R3QueryMetadata[] {
|
||||
return queries.map(query => {
|
||||
let read: o.Expression|null = null;
|
||||
if (query.read && query.read.identifier) {
|
||||
read = outputCtx.importExpr(query.read.identifier.reference);
|
||||
}
|
||||
return {
|
||||
propertyName: query.propertyName,
|
||||
first: query.first,
|
||||
predicate: selectorsFromGlobalMetadata(query.selectors, outputCtx),
|
||||
descendants: query.descendants, read,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `CompileTokenMetadata` for query selectors into either an expression for a predicate
|
||||
* type, or a list of string predicates.
|
||||
*/
|
||||
function selectorsFromGlobalMetadata(
|
||||
selectors: CompileTokenMetadata[], outputCtx: OutputContext): o.Expression|string[] {
|
||||
if (selectors.length > 1 || (selectors.length == 1 && selectors[0].value)) {
|
||||
const selectorStrings = selectors.map(value => value.value as string);
|
||||
selectorStrings.some(value => !value) &&
|
||||
error('Found a type among the string selectors expected');
|
||||
return outputCtx.constantPool.getConstLiteral(
|
||||
o.literalArr(selectorStrings.map(value => o.literal(value))));
|
||||
}
|
||||
|
||||
if (selectors.length == 1) {
|
||||
const first = selectors[0];
|
||||
if (first.identifier) {
|
||||
return outputCtx.importExpr(first.identifier.reference);
|
||||
}
|
||||
}
|
||||
|
||||
error('Unexpected query form');
|
||||
return o.NULL_EXPR;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param meta
|
||||
* @param constantPool
|
||||
*/
|
||||
function createQueryDefinitions(
|
||||
queries: R3QueryMetadata[], constantPool: ConstantPool): o.Expression[]|undefined {
|
||||
const queryDefinitions: o.Expression[] = [];
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
const query = queries[i];
|
||||
const predicate = getQueryPredicate(query, constantPool);
|
||||
|
||||
// e.g. r3.Q(null, somePredicate, false) or r3.Q(null, ['div'], false)
|
||||
const parameters = [
|
||||
o.literal(null, o.INFERRED_TYPE),
|
||||
predicate,
|
||||
o.literal(query.descendants),
|
||||
];
|
||||
|
||||
if (query.read) {
|
||||
parameters.push(query.read);
|
||||
}
|
||||
|
||||
queryDefinitions.push(o.importExpr(R3.query).callFn(parameters));
|
||||
}
|
||||
return queryDefinitions.length > 0 ? queryDefinitions : undefined;
|
||||
}
|
||||
|
||||
// Turn a directive selector into an R3-compatible selector for directive def
|
||||
function createDirectiveSelector(selector: string): o.Expression {
|
||||
return asLiteral(core.parseSelectorToR3Selector(selector));
|
||||
}
|
||||
|
||||
function createHostAttributesArray(meta: R3DirectiveMetadata): o.Expression|null {
|
||||
const values: o.Expression[] = [];
|
||||
const attributes = meta.host.attributes;
|
||||
for (let key of Object.getOwnPropertyNames(attributes)) {
|
||||
const value = attributes[key];
|
||||
values.push(o.literal(key), o.literal(value));
|
||||
}
|
||||
if (values.length > 0) {
|
||||
return o.literalArr(values);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return a host binding function or null if one is not necessary.
|
||||
function createHostBindingsFunction(
|
||||
meta: R3DirectiveMetadata, bindingParser: BindingParser): o.Expression|null {
|
||||
const statements: o.Statement[] = [];
|
||||
|
||||
const temporary = temporaryAllocator(statements, TEMPORARY_NAME);
|
||||
|
||||
const hostBindingSourceSpan = meta.typeSourceSpan;
|
||||
|
||||
// Calculate the queries
|
||||
for (let index = 0; index < meta.queries.length; index++) {
|
||||
const query = meta.queries[index];
|
||||
|
||||
// e.g. r3.qR(tmp = r3.ld(dirIndex)[1]) && (r3.ld(dirIndex)[0].someDir = tmp);
|
||||
const getDirectiveMemory = o.importExpr(R3.load).callFn([o.variable('dirIndex')]);
|
||||
// The query list is at the query index + 1 because the directive itself is in slot 0.
|
||||
const getQueryList = getDirectiveMemory.key(o.literal(index + 1));
|
||||
const assignToTemporary = temporary().set(getQueryList);
|
||||
const callQueryRefresh = o.importExpr(R3.queryRefresh).callFn([assignToTemporary]);
|
||||
const updateDirective = getDirectiveMemory.key(o.literal(0, o.INFERRED_TYPE))
|
||||
.prop(query.propertyName)
|
||||
.set(query.first ? temporary().prop('first') : temporary());
|
||||
const andExpression = callQueryRefresh.and(updateDirective);
|
||||
statements.push(andExpression.toStmt());
|
||||
}
|
||||
|
||||
const directiveSummary = metadataAsSummary(meta);
|
||||
|
||||
// Calculate the host property bindings
|
||||
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan);
|
||||
const bindingContext = o.importExpr(R3.load).callFn([o.variable('dirIndex')]);
|
||||
if (bindings) {
|
||||
for (const binding of bindings) {
|
||||
const bindingExpr = convertPropertyBinding(
|
||||
null, bindingContext, binding.expression, 'b', BindingForm.TrySimple,
|
||||
() => error('Unexpected interpolation'));
|
||||
statements.push(...bindingExpr.stmts);
|
||||
statements.push(o.importExpr(R3.elementProperty)
|
||||
.callFn([
|
||||
o.variable('elIndex'),
|
||||
o.literal(binding.name),
|
||||
o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]),
|
||||
])
|
||||
.toStmt());
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate host event bindings
|
||||
const eventBindings =
|
||||
bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan);
|
||||
if (eventBindings) {
|
||||
for (const binding of eventBindings) {
|
||||
const bindingExpr = convertActionBinding(
|
||||
null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation'));
|
||||
const bindingName = binding.name && sanitizeIdentifier(binding.name);
|
||||
const typeName = meta.name;
|
||||
const functionName =
|
||||
typeName && bindingName ? `${typeName}_${bindingName}_HostBindingHandler` : null;
|
||||
const handler = o.fn(
|
||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)],
|
||||
[...bindingExpr.stmts, new o.ReturnStatement(bindingExpr.allowDefault)], o.INFERRED_TYPE,
|
||||
null, functionName);
|
||||
statements.push(
|
||||
o.importExpr(R3.listener).callFn([o.literal(binding.name), handler]).toStmt());
|
||||
}
|
||||
}
|
||||
|
||||
if (statements.length > 0) {
|
||||
const typeName = meta.name;
|
||||
return o.fn(
|
||||
[
|
||||
new o.FnParam('dirIndex', o.NUMBER_TYPE),
|
||||
new o.FnParam('elIndex', o.NUMBER_TYPE),
|
||||
],
|
||||
statements, o.INFERRED_TYPE, null, typeName ? `${typeName}_HostBindings` : null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function metadataAsSummary(meta: R3DirectiveMetadata): CompileDirectiveSummary {
|
||||
// clang-format off
|
||||
return {
|
||||
hostAttributes: meta.host.attributes,
|
||||
hostListeners: meta.host.listeners,
|
||||
hostProperties: meta.host.properties,
|
||||
} as CompileDirectiveSummary;
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
function typeMapToExpressionMap(
|
||||
map: Map<string, StaticSymbol>, outputCtx: OutputContext): Map<string, o.Expression> {
|
||||
// Convert each map entry into another entry where the value is an expression importing the type.
|
||||
const entries = Array.from(map).map(
|
||||
([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]);
|
||||
return new Map(entries);
|
||||
}
|
721
packages/compiler/src/render3/view/template.ts
Normal file
721
packages/compiler/src/render3/view/template.ts
Normal file
@ -0,0 +1,721 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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, sanitizeIdentifier} from '../../compile_metadata';
|
||||
import {CompileReflector} from '../../compile_reflector';
|
||||
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
||||
import {ConstantPool} from '../../constant_pool';
|
||||
import * as core from '../../core';
|
||||
import {AST, AstMemoryEfficientTransformer, BindingPipe, BoundElementBindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast';
|
||||
import * as o from '../../output/output_ast';
|
||||
import {ParseSourceSpan} from '../../parse_util';
|
||||
import {CssSelector, SelectorMatcher} from '../../selector';
|
||||
import {OutputContext, error} from '../../util';
|
||||
import * as t from '../r3_ast';
|
||||
import {Identifiers as R3} from '../r3_identifiers';
|
||||
|
||||
import {R3QueryMetadata} from './api';
|
||||
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, getQueryPredicate, invalid, mapToExpression, noop, temporaryAllocator, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
const BINDING_INSTRUCTION_MAP: {[type: number]: o.ExternalReference} = {
|
||||
[BoundElementBindingType.Property]: R3.elementProperty,
|
||||
[BoundElementBindingType.Attribute]: R3.elementAttribute,
|
||||
[BoundElementBindingType.Class]: R3.elementClassNamed,
|
||||
[BoundElementBindingType.Style]: R3.elementStyleNamed,
|
||||
};
|
||||
|
||||
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
|
||||
private _dataIndex = 0;
|
||||
private _bindingContext = 0;
|
||||
private _prefixCode: o.Statement[] = [];
|
||||
private _creationCode: o.Statement[] = [];
|
||||
private _variableCode: o.Statement[] = [];
|
||||
private _bindingCode: o.Statement[] = [];
|
||||
private _postfixCode: o.Statement[] = [];
|
||||
private _temporary = temporaryAllocator(this._prefixCode, TEMPORARY_NAME);
|
||||
private _projectionDefinitionIndex = -1;
|
||||
private _valueConverter: ValueConverter;
|
||||
private _unsupported = unsupported;
|
||||
private _bindingScope: BindingScope;
|
||||
|
||||
// Whether we are inside a translatable element (`<p i18n>... somewhere here ... </p>)
|
||||
private _inI18nSection: boolean = false;
|
||||
private _i18nSectionIndex = -1;
|
||||
// Maps of placeholder to node indexes for each of the i18n section
|
||||
private _phToNodeIdxes: {[phName: string]: number[]}[] = [{}];
|
||||
|
||||
constructor(
|
||||
private constantPool: ConstantPool, private contextParameter: string,
|
||||
parentBindingScope: BindingScope, private level = 0, private contextName: string|null,
|
||||
private templateName: string|null, private viewQueries: R3QueryMetadata[],
|
||||
private directiveMatcher: SelectorMatcher|null, private directives: Set<o.Expression>,
|
||||
private pipeTypeByName: Map<string, o.Expression>, private pipes: Set<o.Expression>) {
|
||||
this._bindingScope =
|
||||
parentBindingScope.nestedScope((lhsVar: o.ReadVarExpr, expression: o.Expression) => {
|
||||
this._bindingCode.push(
|
||||
lhsVar.set(expression).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
||||
});
|
||||
this._valueConverter = new ValueConverter(
|
||||
constantPool, () => this.allocateDataSlot(),
|
||||
(name, localName, slot, value: o.ReadVarExpr) => {
|
||||
const pipeType = pipeTypeByName.get(name);
|
||||
if (pipeType) {
|
||||
this.pipes.add(pipeType);
|
||||
}
|
||||
this._bindingScope.set(localName, value);
|
||||
this._creationCode.push(
|
||||
o.importExpr(R3.pipe).callFn([o.literal(slot), o.literal(name)]).toStmt());
|
||||
});
|
||||
}
|
||||
|
||||
buildTemplateFunction(
|
||||
nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false,
|
||||
ngContentSelectors: string[] = []): o.FunctionExpr {
|
||||
// Create variable bindings
|
||||
for (const variable of variables) {
|
||||
const variableName = variable.name;
|
||||
const expression =
|
||||
o.variable(this.contextParameter).prop(variable.value || IMPLICIT_REFERENCE);
|
||||
const scopedName = this._bindingScope.freshReferenceName();
|
||||
// Add the reference to the local scope.
|
||||
this._bindingScope.set(variableName, o.variable(variableName + scopedName), expression);
|
||||
}
|
||||
|
||||
// Output a `ProjectionDef` instruction when some `<ng-content>` are present
|
||||
if (hasNgContent) {
|
||||
this._projectionDefinitionIndex = this.allocateDataSlot();
|
||||
const parameters: o.Expression[] = [o.literal(this._projectionDefinitionIndex)];
|
||||
|
||||
// Only selectors with a non-default value are generated
|
||||
if (ngContentSelectors.length > 1) {
|
||||
const r3Selectors = ngContentSelectors.map(s => core.parseSelectorToR3Selector(s));
|
||||
// `projectionDef` needs both the parsed and raw value of the selectors
|
||||
const parsed = this.constantPool.getConstLiteral(asLiteral(r3Selectors), true);
|
||||
const unParsed = this.constantPool.getConstLiteral(asLiteral(ngContentSelectors), true);
|
||||
parameters.push(parsed, unParsed);
|
||||
}
|
||||
|
||||
this.instruction(this._creationCode, null, R3.projectionDef, ...parameters);
|
||||
}
|
||||
|
||||
// Define and update any view queries
|
||||
for (let query of this.viewQueries) {
|
||||
// e.g. r3.Q(0, somePredicate, true);
|
||||
const querySlot = this.allocateDataSlot();
|
||||
const predicate = getQueryPredicate(query, this.constantPool);
|
||||
const args: o.Expression[] = [
|
||||
o.literal(querySlot, o.INFERRED_TYPE),
|
||||
predicate,
|
||||
o.literal(query.descendants, o.INFERRED_TYPE),
|
||||
];
|
||||
|
||||
if (query.read) {
|
||||
args.push(query.read);
|
||||
}
|
||||
this.instruction(this._creationCode, null, R3.query, ...args);
|
||||
|
||||
// (r3.qR(tmp = r3.ɵld(0)) && (ctx.someDir = tmp));
|
||||
const temporary = this._temporary();
|
||||
const getQueryList = o.importExpr(R3.load).callFn([o.literal(querySlot)]);
|
||||
const refresh = o.importExpr(R3.queryRefresh).callFn([temporary.set(getQueryList)]);
|
||||
const updateDirective = o.variable(CONTEXT_NAME)
|
||||
.prop(query.propertyName)
|
||||
.set(query.first ? temporary.prop('first') : temporary);
|
||||
this._bindingCode.push(refresh.and(updateDirective).toStmt());
|
||||
}
|
||||
|
||||
t.visitAll(this, nodes);
|
||||
|
||||
const creationCode = this._creationCode.length > 0 ?
|
||||
[o.ifStmt(
|
||||
o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(core.RenderFlags.Create), null, false),
|
||||
this._creationCode)] :
|
||||
[];
|
||||
|
||||
const updateCode = this._bindingCode.length > 0 ?
|
||||
[o.ifStmt(
|
||||
o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(core.RenderFlags.Update), null, false),
|
||||
this._bindingCode)] :
|
||||
[];
|
||||
|
||||
// Generate maps of placeholder name to node indexes
|
||||
// TODO(vicb): This is a WIP, not fully supported yet
|
||||
for (const phToNodeIdx of this._phToNodeIdxes) {
|
||||
if (Object.keys(phToNodeIdx).length > 0) {
|
||||
const scopedName = this._bindingScope.freshReferenceName();
|
||||
const phMap = o.variable(scopedName)
|
||||
.set(mapToExpression(phToNodeIdx, true))
|
||||
.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||
|
||||
this._prefixCode.push(phMap);
|
||||
}
|
||||
}
|
||||
|
||||
return o.fn(
|
||||
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(this.contextParameter, null)],
|
||||
[
|
||||
// Temporary variable declarations for query refresh (i.e. let _t: any;)
|
||||
...this._prefixCode,
|
||||
// Creating mode (i.e. if (rf & RenderFlags.Create) { ... })
|
||||
...creationCode,
|
||||
// Temporary variable declarations for local refs (i.e. const tmp = ld(1) as any)
|
||||
...this._variableCode,
|
||||
// Binding and refresh mode (i.e. if (rf & RenderFlags.Update) {...})
|
||||
...updateCode,
|
||||
// Nested templates (i.e. function CompTemplate() {})
|
||||
...this._postfixCode
|
||||
],
|
||||
o.INFERRED_TYPE, null, this.templateName);
|
||||
}
|
||||
|
||||
// LocalResolver
|
||||
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
|
||||
|
||||
visitContent(ngContent: t.Content) {
|
||||
const slot = this.allocateDataSlot();
|
||||
const selectorIndex = ngContent.selectorIndex;
|
||||
const parameters: o.Expression[] = [
|
||||
o.literal(slot),
|
||||
o.literal(this._projectionDefinitionIndex),
|
||||
];
|
||||
|
||||
const attributeAsList: string[] = [];
|
||||
|
||||
ngContent.attributes.forEach((attribute) => {
|
||||
const name = attribute.name;
|
||||
if (name !== 'select') {
|
||||
attributeAsList.push(name, attribute.value);
|
||||
}
|
||||
});
|
||||
|
||||
if (attributeAsList.length > 0) {
|
||||
parameters.push(o.literal(selectorIndex), asLiteral(attributeAsList));
|
||||
} else if (selectorIndex !== 0) {
|
||||
parameters.push(o.literal(selectorIndex));
|
||||
}
|
||||
|
||||
this.instruction(this._creationCode, ngContent.sourceSpan, R3.projection, ...parameters);
|
||||
}
|
||||
|
||||
visitElement(element: t.Element) {
|
||||
const elementIndex = this.allocateDataSlot();
|
||||
const referenceDataSlots = new Map<string, number>();
|
||||
const wasInI18nSection = this._inI18nSection;
|
||||
|
||||
const outputAttrs: {[name: string]: string} = {};
|
||||
const attrI18nMetas: {[name: string]: string} = {};
|
||||
let i18nMeta: string = '';
|
||||
|
||||
// Elements inside i18n sections are replaced with placeholders
|
||||
// TODO(vicb): nested elements are a WIP in this phase
|
||||
if (this._inI18nSection) {
|
||||
const phName = element.name.toLowerCase();
|
||||
if (!this._phToNodeIdxes[this._i18nSectionIndex][phName]) {
|
||||
this._phToNodeIdxes[this._i18nSectionIndex][phName] = [];
|
||||
}
|
||||
this._phToNodeIdxes[this._i18nSectionIndex][phName].push(elementIndex);
|
||||
}
|
||||
|
||||
// Handle i18n attributes
|
||||
for (const attr of element.attributes) {
|
||||
const name = attr.name;
|
||||
const value = attr.value;
|
||||
if (name === I18N_ATTR) {
|
||||
if (this._inI18nSection) {
|
||||
throw new Error(
|
||||
`Could not mark an element as translatable inside of a translatable section`);
|
||||
}
|
||||
this._inI18nSection = true;
|
||||
this._i18nSectionIndex++;
|
||||
this._phToNodeIdxes[this._i18nSectionIndex] = {};
|
||||
i18nMeta = value;
|
||||
} else if (name.startsWith(I18N_ATTR_PREFIX)) {
|
||||
attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value;
|
||||
} else {
|
||||
outputAttrs[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Match directives on non i18n attributes
|
||||
if (this.directiveMatcher) {
|
||||
const selector = createCssSelector(element.name, outputAttrs);
|
||||
this.directiveMatcher.match(
|
||||
selector, (sel: CssSelector, staticType: any) => { this.directives.add(staticType); });
|
||||
}
|
||||
|
||||
// Element creation mode
|
||||
const parameters: o.Expression[] = [
|
||||
o.literal(elementIndex),
|
||||
o.literal(element.name),
|
||||
];
|
||||
|
||||
// Add the attributes
|
||||
const i18nMessages: o.Statement[] = [];
|
||||
const attributes: o.Expression[] = [];
|
||||
let hasI18nAttr = false;
|
||||
|
||||
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
|
||||
const value = outputAttrs[name];
|
||||
attributes.push(o.literal(name));
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
hasI18nAttr = true;
|
||||
const meta = parseI18nMeta(attrI18nMetas[name]);
|
||||
const variable = this.constantPool.getTranslation(value, meta);
|
||||
attributes.push(variable);
|
||||
} else {
|
||||
attributes.push(o.literal(value));
|
||||
}
|
||||
});
|
||||
|
||||
let attrArg: o.Expression = o.TYPED_NULL_EXPR;
|
||||
|
||||
if (attributes.length > 0) {
|
||||
attrArg = hasI18nAttr ? getLiteralFactory(this.constantPool, o.literalArr(attributes)) :
|
||||
this.constantPool.getConstLiteral(o.literalArr(attributes), true);
|
||||
}
|
||||
|
||||
parameters.push(attrArg);
|
||||
|
||||
if (element.references && element.references.length > 0) {
|
||||
const references = flatten(element.references.map(reference => {
|
||||
const slot = this.allocateDataSlot();
|
||||
referenceDataSlots.set(reference.name, slot);
|
||||
// Generate the update temporary.
|
||||
const variableName = this._bindingScope.freshReferenceName();
|
||||
this._variableCode.push(o.variable(variableName, o.INFERRED_TYPE)
|
||||
.set(o.importExpr(R3.load).callFn([o.literal(slot)]))
|
||||
.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
||||
this._bindingScope.set(reference.name, o.variable(variableName));
|
||||
return [reference.name, reference.value];
|
||||
}));
|
||||
parameters.push(this.constantPool.getConstLiteral(asLiteral(references), true));
|
||||
} else {
|
||||
parameters.push(o.TYPED_NULL_EXPR);
|
||||
}
|
||||
|
||||
// Generate the instruction create element instruction
|
||||
if (i18nMessages.length > 0) {
|
||||
this._creationCode.push(...i18nMessages);
|
||||
}
|
||||
this.instruction(
|
||||
this._creationCode, element.sourceSpan, R3.createElement, ...trimTrailingNulls(parameters));
|
||||
|
||||
const implicit = o.variable(CONTEXT_NAME);
|
||||
|
||||
// Generate Listeners (outputs)
|
||||
element.outputs.forEach((outputAst: t.BoundEvent) => {
|
||||
const elName = sanitizeIdentifier(element.name);
|
||||
const evName = sanitizeIdentifier(outputAst.name);
|
||||
const functionName = `${this.templateName}_${elName}_${evName}_listener`;
|
||||
const localVars: o.Statement[] = [];
|
||||
const bindingScope =
|
||||
this._bindingScope.nestedScope((lhsVar: o.ReadVarExpr, rhsExpression: o.Expression) => {
|
||||
localVars.push(
|
||||
lhsVar.set(rhsExpression).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
||||
});
|
||||
const bindingExpr = convertActionBinding(
|
||||
bindingScope, implicit, outputAst.handler, 'b', () => error('Unexpected interpolation'));
|
||||
const handler = o.fn(
|
||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)], [...localVars, ...bindingExpr.render3Stmts],
|
||||
o.INFERRED_TYPE, null, functionName);
|
||||
this.instruction(
|
||||
this._creationCode, outputAst.sourceSpan, R3.listener, o.literal(outputAst.name),
|
||||
handler);
|
||||
});
|
||||
|
||||
|
||||
// Generate element input bindings
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
if (input.type === BoundElementBindingType.Animation) {
|
||||
this._unsupported('animations');
|
||||
}
|
||||
const convertedBinding = this.convertPropertyBinding(implicit, input.value);
|
||||
const instruction = BINDING_INSTRUCTION_MAP[input.type];
|
||||
if (instruction) {
|
||||
// TODO(chuckj): runtime: security context?
|
||||
const value = o.importExpr(R3.bind).callFn([convertedBinding]);
|
||||
this.instruction(
|
||||
this._bindingCode, input.sourceSpan, instruction, o.literal(elementIndex),
|
||||
o.literal(input.name), value);
|
||||
} else {
|
||||
this._unsupported(`binding type ${input.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Traverse element child nodes
|
||||
if (this._inI18nSection && element.children.length == 1 &&
|
||||
element.children[0] instanceof t.Text) {
|
||||
const text = element.children[0] as t.Text;
|
||||
this.visitSingleI18nTextChild(text, i18nMeta);
|
||||
} else {
|
||||
t.visitAll(this, element.children);
|
||||
}
|
||||
|
||||
// Finish element construction mode.
|
||||
this.instruction(
|
||||
this._creationCode, element.endSourceSpan || element.sourceSpan, R3.elementEnd);
|
||||
|
||||
// Restore the state before exiting this node
|
||||
this._inI18nSection = wasInI18nSection;
|
||||
}
|
||||
|
||||
visitTemplate(template: t.Template) {
|
||||
const templateIndex = this.allocateDataSlot();
|
||||
|
||||
let elName = '';
|
||||
if (template.children.length === 1 && template.children[0] instanceof t.Element) {
|
||||
// When the template as a single child, derive the context name from the tag
|
||||
elName = sanitizeIdentifier((template.children[0] as t.Element).name);
|
||||
}
|
||||
|
||||
const contextName = elName ? `${this.contextName}_${elName}` : '';
|
||||
|
||||
const templateName =
|
||||
contextName ? `${contextName}_Template_${templateIndex}` : `Template_${templateIndex}`;
|
||||
|
||||
const templateContext = `ctx${this.level}`;
|
||||
|
||||
const parameters: o.Expression[] = [
|
||||
o.literal(templateIndex),
|
||||
o.variable(templateName),
|
||||
o.TYPED_NULL_EXPR,
|
||||
];
|
||||
|
||||
const attributeNames: o.Expression[] = [];
|
||||
const attributeMap: {[name: string]: string} = {};
|
||||
|
||||
template.attributes.forEach(a => {
|
||||
attributeNames.push(asLiteral(a.name), asLiteral(''));
|
||||
attributeMap[a.name] = a.value;
|
||||
});
|
||||
|
||||
// Match directives on template attributes
|
||||
if (this.directiveMatcher) {
|
||||
const selector = createCssSelector('ng-template', attributeMap);
|
||||
this.directiveMatcher.match(
|
||||
selector, (cssSelector, staticType) => { this.directives.add(staticType); });
|
||||
}
|
||||
|
||||
if (attributeNames.length) {
|
||||
parameters.push(this.constantPool.getConstLiteral(o.literalArr(attributeNames), true));
|
||||
}
|
||||
|
||||
// e.g. C(1, C1Template)
|
||||
this.instruction(
|
||||
this._creationCode, template.sourceSpan, R3.containerCreate,
|
||||
...trimTrailingNulls(parameters));
|
||||
|
||||
// e.g. p(1, 'forOf', ɵb(ctx.items));
|
||||
const context = o.variable(CONTEXT_NAME);
|
||||
template.inputs.forEach(input => {
|
||||
const convertedBinding = this.convertPropertyBinding(context, input.value);
|
||||
this.instruction(
|
||||
this._bindingCode, template.sourceSpan, R3.elementProperty, o.literal(templateIndex),
|
||||
o.literal(input.name), o.importExpr(R3.bind).callFn([convertedBinding]));
|
||||
});
|
||||
|
||||
// Create the template function
|
||||
const templateVisitor = new TemplateDefinitionBuilder(
|
||||
this.constantPool, templateContext, this._bindingScope, this.level + 1, contextName,
|
||||
templateName, [], this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes);
|
||||
const templateFunctionExpr =
|
||||
templateVisitor.buildTemplateFunction(template.children, template.variables);
|
||||
this._postfixCode.push(templateFunctionExpr.toDeclStmt(templateName, null));
|
||||
}
|
||||
|
||||
// These should be handled in the template or element directly.
|
||||
readonly visitReference = invalid;
|
||||
readonly visitVariable = invalid;
|
||||
readonly visitAttribute = invalid;
|
||||
readonly visitBoundAttribute = invalid;
|
||||
readonly visitBoundEvent = invalid;
|
||||
|
||||
visitBoundText(text: t.BoundText) {
|
||||
const nodeIndex = this.allocateDataSlot();
|
||||
|
||||
this.instruction(this._creationCode, text.sourceSpan, R3.text, o.literal(nodeIndex));
|
||||
|
||||
this.instruction(
|
||||
this._bindingCode, text.sourceSpan, R3.textCreateBound, o.literal(nodeIndex),
|
||||
this.convertPropertyBinding(o.variable(CONTEXT_NAME), text.value));
|
||||
}
|
||||
|
||||
visitText(text: t.Text) {
|
||||
this.instruction(
|
||||
this._creationCode, text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()),
|
||||
o.literal(text.value));
|
||||
}
|
||||
|
||||
// When the content of the element is a single text node the translation can be inlined:
|
||||
//
|
||||
// `<p i18n="desc|mean">some content</p>`
|
||||
// compiles to
|
||||
// ```
|
||||
// /**
|
||||
// * @desc desc
|
||||
// * @meaning mean
|
||||
// */
|
||||
// const MSG_XYZ = goog.getMsg('some content');
|
||||
// i0.ɵT(1, MSG_XYZ);
|
||||
// ```
|
||||
visitSingleI18nTextChild(text: t.Text, i18nMeta: string) {
|
||||
const meta = parseI18nMeta(i18nMeta);
|
||||
const variable = this.constantPool.getTranslation(text.value, meta);
|
||||
this.instruction(
|
||||
this._creationCode, text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), variable);
|
||||
}
|
||||
|
||||
private allocateDataSlot() { return this._dataIndex++; }
|
||||
private bindingContext() { return `${this._bindingContext++}`; }
|
||||
|
||||
private instruction(
|
||||
statements: o.Statement[], span: ParseSourceSpan|null, reference: o.ExternalReference,
|
||||
...params: o.Expression[]) {
|
||||
statements.push(o.importExpr(reference, null, span).callFn(params, span).toStmt());
|
||||
}
|
||||
|
||||
private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression {
|
||||
const pipesConvertedValue = value.visit(this._valueConverter);
|
||||
const convertedPropertyBinding = convertPropertyBinding(
|
||||
this, implicit, pipesConvertedValue, this.bindingContext(), BindingForm.TrySimple,
|
||||
interpolate);
|
||||
this._bindingCode.push(...convertedPropertyBinding.stmts);
|
||||
return convertedPropertyBinding.currValExpr;
|
||||
}
|
||||
}
|
||||
|
||||
class ValueConverter extends AstMemoryEfficientTransformer {
|
||||
constructor(
|
||||
private constantPool: ConstantPool, private allocateSlot: () => number,
|
||||
private definePipe:
|
||||
(name: string, localName: string, slot: number, value: o.Expression) => void) {
|
||||
super();
|
||||
}
|
||||
|
||||
// AstMemoryEfficientTransformer
|
||||
visitPipe(pipe: BindingPipe, context: any): AST {
|
||||
// Allocate a slot to create the pipe
|
||||
const slot = this.allocateSlot();
|
||||
const slotPseudoLocal = `PIPE:${slot}`;
|
||||
const target = new PropertyRead(pipe.span, new ImplicitReceiver(pipe.span), slotPseudoLocal);
|
||||
const bindingId = pipeBinding(pipe.args);
|
||||
this.definePipe(pipe.name, slotPseudoLocal, slot, o.importExpr(bindingId));
|
||||
const value = pipe.exp.visit(this);
|
||||
const args = this.visitAll(pipe.args);
|
||||
|
||||
return new FunctionCall(
|
||||
pipe.span, target, [new LiteralPrimitive(pipe.span, slot), value, ...args]);
|
||||
}
|
||||
|
||||
visitLiteralArray(array: LiteralArray, context: any): AST {
|
||||
return new BuiltinFunctionCall(array.span, this.visitAll(array.expressions), values => {
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// calls to literal factories that compose the literal and will cache intermediate
|
||||
// values. Otherwise, just return an literal array that contains the values.
|
||||
const literal = o.literalArr(values);
|
||||
return values.every(a => a.isConstant()) ? this.constantPool.getConstLiteral(literal, true) :
|
||||
getLiteralFactory(this.constantPool, literal);
|
||||
});
|
||||
}
|
||||
|
||||
visitLiteralMap(map: LiteralMap, context: any): AST {
|
||||
return new BuiltinFunctionCall(map.span, this.visitAll(map.values), values => {
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// calls to literal factories that compose the literal and will cache intermediate
|
||||
// values. Otherwise, just return an literal array that contains the values.
|
||||
const literal = o.literalMap(values.map(
|
||||
(value, index) => ({key: map.keys[index].key, value, quoted: map.keys[index].quoted})));
|
||||
return values.every(a => a.isConstant()) ? this.constantPool.getConstLiteral(literal, true) :
|
||||
getLiteralFactory(this.constantPool, literal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Pipes always have at least one parameter, the value they operate on
|
||||
const pipeBindingIdentifiers = [R3.pipeBind1, R3.pipeBind2, R3.pipeBind3, R3.pipeBind4];
|
||||
|
||||
function pipeBinding(args: o.Expression[]): o.ExternalReference {
|
||||
return pipeBindingIdentifiers[args.length] || R3.pipeBindV;
|
||||
}
|
||||
|
||||
const pureFunctionIdentifiers = [
|
||||
R3.pureFunction0, R3.pureFunction1, R3.pureFunction2, R3.pureFunction3, R3.pureFunction4,
|
||||
R3.pureFunction5, R3.pureFunction6, R3.pureFunction7, R3.pureFunction8
|
||||
];
|
||||
function getLiteralFactory(
|
||||
constantPool: ConstantPool, literal: o.LiteralArrayExpr | o.LiteralMapExpr): o.Expression {
|
||||
const {literalFactory, literalFactoryArguments} = constantPool.getLiteralFactory(literal);
|
||||
literalFactoryArguments.length > 0 || error(`Expected arguments to a literal factory function`);
|
||||
let pureFunctionIdent =
|
||||
pureFunctionIdentifiers[literalFactoryArguments.length] || R3.pureFunctionV;
|
||||
|
||||
// Literal factories are pure functions that only need to be re-invoked when the parameters
|
||||
// change.
|
||||
return o.importExpr(pureFunctionIdent).callFn([literalFactory, ...literalFactoryArguments]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function which is executed whenever a variable is referenced for the first time in a given
|
||||
* scope.
|
||||
*
|
||||
* It is expected that the function creates the `const localName = expression`; statement.
|
||||
*/
|
||||
export type DeclareLocalVarCallback = (lhsVar: o.ReadVarExpr, rhsExpression: o.Expression) => void;
|
||||
|
||||
export class BindingScope implements LocalResolver {
|
||||
/**
|
||||
* Keeps a map from local variables to their expressions.
|
||||
*
|
||||
* This is used when one refers to variable such as: 'let abc = a.b.c`.
|
||||
* - key to the map is the string literal `"abc"`.
|
||||
* - value `lhs` is the left hand side which is an AST representing `abc`.
|
||||
* - value `rhs` is the right hand side which is an AST representing `a.b.c`.
|
||||
* - value `declared` is true if the `declareLocalVarCallback` has been called for this scope
|
||||
* already.
|
||||
*/
|
||||
private map = new Map < string, {
|
||||
lhs: o.ReadVarExpr;
|
||||
rhs: o.Expression|undefined;
|
||||
declared: boolean;
|
||||
}
|
||||
> ();
|
||||
private referenceNameIndex = 0;
|
||||
|
||||
static ROOT_SCOPE = new BindingScope().set('$event', o.variable('$event'));
|
||||
|
||||
private constructor(
|
||||
private parent: BindingScope|null = null,
|
||||
private declareLocalVarCallback: DeclareLocalVarCallback = noop) {}
|
||||
|
||||
get(name: string): o.Expression|null {
|
||||
let current: BindingScope|null = this;
|
||||
while (current) {
|
||||
let value = current.map.get(name);
|
||||
if (value != null) {
|
||||
if (current !== this) {
|
||||
// make a local copy and reset the `declared` state.
|
||||
value = {lhs: value.lhs, rhs: value.rhs, declared: false};
|
||||
// Cache the value locally.
|
||||
this.map.set(name, value);
|
||||
}
|
||||
if (value.rhs && !value.declared) {
|
||||
// if it is first time we are referencing the variable in the scope
|
||||
// than invoke the callback to insert variable declaration.
|
||||
this.declareLocalVarCallback(value.lhs, value.rhs);
|
||||
value.declared = true;
|
||||
}
|
||||
return value.lhs;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a local variable for later reference.
|
||||
*
|
||||
* @param name Name of the variable.
|
||||
* @param lhs AST representing the left hand side of the `let lhs = rhs;`.
|
||||
* @param rhs AST representing the right hand side of the `let lhs = rhs;`. The `rhs` can be
|
||||
* `undefined` for variable that are ambient such as `$event` and which don't have `rhs`
|
||||
* declaration.
|
||||
*/
|
||||
set(name: string, lhs: o.ReadVarExpr, rhs?: o.Expression): BindingScope {
|
||||
!this.map.has(name) ||
|
||||
error(`The name ${name} is already defined in scope to be ${this.map.get(name)}`);
|
||||
this.map.set(name, {lhs: lhs, rhs: rhs, declared: false});
|
||||
return this;
|
||||
}
|
||||
|
||||
getLocal(name: string): (o.Expression|null) { return this.get(name); }
|
||||
|
||||
nestedScope(declareCallback: DeclareLocalVarCallback): BindingScope {
|
||||
return new BindingScope(this, declareCallback);
|
||||
}
|
||||
|
||||
freshReferenceName(): string {
|
||||
let current: BindingScope = this;
|
||||
// Find the top scope as it maintains the global reference count
|
||||
while (current.parent) current = current.parent;
|
||||
const ref = `${REFERENCE_PREFIX}${current.referenceNameIndex++}`;
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `CssSelector` given a tag name and a map of attributes
|
||||
*/
|
||||
function createCssSelector(tag: string, attributes: {[name: string]: string}): CssSelector {
|
||||
const cssSelector = new CssSelector();
|
||||
|
||||
cssSelector.setElement(tag);
|
||||
|
||||
Object.getOwnPropertyNames(attributes).forEach((name) => {
|
||||
const value = attributes[name];
|
||||
|
||||
cssSelector.addAttribute(name, value);
|
||||
if (name.toLowerCase() === 'class') {
|
||||
const classes = value.trim().split(/\s+/g);
|
||||
classes.forEach(className => cssSelector.addClassName(className));
|
||||
}
|
||||
});
|
||||
|
||||
return cssSelector;
|
||||
}
|
||||
|
||||
// Parse i18n metas like:
|
||||
// - "@@id",
|
||||
// - "description[@@id]",
|
||||
// - "meaning|description[@@id]"
|
||||
function parseI18nMeta(i18n?: string): {description?: string, id?: string, meaning?: string} {
|
||||
let meaning: string|undefined;
|
||||
let description: string|undefined;
|
||||
let id: string|undefined;
|
||||
|
||||
if (i18n) {
|
||||
// TODO(vicb): figure out how to force a message ID with closure ?
|
||||
const idIndex = i18n.indexOf(ID_SEPARATOR);
|
||||
|
||||
const descIndex = i18n.indexOf(MEANING_SEPARATOR);
|
||||
let meaningAndDesc: string;
|
||||
[meaningAndDesc, id] =
|
||||
(idIndex > -1) ? [i18n.slice(0, idIndex), i18n.slice(idIndex + 2)] : [i18n, ''];
|
||||
[meaning, description] = (descIndex > -1) ?
|
||||
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
|
||||
['', meaningAndDesc];
|
||||
}
|
||||
|
||||
return {description, id, meaning};
|
||||
}
|
||||
|
||||
function interpolate(args: o.Expression[]): o.Expression {
|
||||
args = args.slice(1); // Ignore the length prefix added for render2
|
||||
switch (args.length) {
|
||||
case 3:
|
||||
return o.importExpr(R3.interpolation1).callFn(args);
|
||||
case 5:
|
||||
return o.importExpr(R3.interpolation2).callFn(args);
|
||||
case 7:
|
||||
return o.importExpr(R3.interpolation3).callFn(args);
|
||||
case 9:
|
||||
return o.importExpr(R3.interpolation4).callFn(args);
|
||||
case 11:
|
||||
return o.importExpr(R3.interpolation5).callFn(args);
|
||||
case 13:
|
||||
return o.importExpr(R3.interpolation6).callFn(args);
|
||||
case 15:
|
||||
return o.importExpr(R3.interpolation7).callFn(args);
|
||||
case 17:
|
||||
return o.importExpr(R3.interpolation8).callFn(args);
|
||||
}
|
||||
(args.length >= 19 && args.length % 2 == 1) ||
|
||||
error(`Invalid interpolation argument length ${args.length}`);
|
||||
return o.importExpr(R3.interpolationV).callFn([o.literalArr(args)]);
|
||||
}
|
122
packages/compiler/src/render3/view/util.ts
Normal file
122
packages/compiler/src/render3/view/util.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {ConstantPool} from '../../constant_pool';
|
||||
import * as o from '../../output/output_ast';
|
||||
import * as t from '../r3_ast';
|
||||
|
||||
import {R3QueryMetadata} from './api';
|
||||
|
||||
|
||||
/** Name of the temporary to use during data binding */
|
||||
export const TEMPORARY_NAME = '_t';
|
||||
|
||||
|
||||
|
||||
/** Name of the context parameter passed into a template function */
|
||||
export const CONTEXT_NAME = 'ctx';
|
||||
|
||||
/** Name of the RenderFlag passed into a template function */
|
||||
export const RENDER_FLAGS = 'rf';
|
||||
|
||||
/** The prefix reference variables */
|
||||
export const REFERENCE_PREFIX = '_r';
|
||||
|
||||
/** The name of the implicit context reference */
|
||||
export const IMPLICIT_REFERENCE = '$implicit';
|
||||
|
||||
/** Name of the i18n attributes **/
|
||||
export const I18N_ATTR = 'i18n';
|
||||
export const I18N_ATTR_PREFIX = 'i18n-';
|
||||
|
||||
/** I18n separators for metadata **/
|
||||
export const MEANING_SEPARATOR = '|';
|
||||
export const ID_SEPARATOR = '@@';
|
||||
|
||||
/**
|
||||
* Creates an allocator for a temporary variable.
|
||||
*
|
||||
* A variable declaration is added to the statements the first time the allocator is invoked.
|
||||
*/
|
||||
export function temporaryAllocator(statements: o.Statement[], name: string): () => o.ReadVarExpr {
|
||||
let temp: o.ReadVarExpr|null = null;
|
||||
return () => {
|
||||
if (!temp) {
|
||||
statements.push(new o.DeclareVarStmt(TEMPORARY_NAME, undefined, o.DYNAMIC_TYPE));
|
||||
temp = o.variable(name);
|
||||
}
|
||||
return temp;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function unsupported(feature: string): never {
|
||||
if (this) {
|
||||
throw new Error(`Builder ${this.constructor.name} doesn't support ${feature} yet`);
|
||||
}
|
||||
throw new Error(`Feature ${feature} is not supported yet`);
|
||||
}
|
||||
|
||||
export function invalid<T>(arg: o.Expression | o.Statement | t.Node): never {
|
||||
throw new Error(
|
||||
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`);
|
||||
}
|
||||
|
||||
export function asLiteral(value: any): o.Expression {
|
||||
if (Array.isArray(value)) {
|
||||
return o.literalArr(value.map(asLiteral));
|
||||
}
|
||||
return o.literal(value, o.INFERRED_TYPE);
|
||||
}
|
||||
|
||||
export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string}): o.Expression|
|
||||
null {
|
||||
if (Object.getOwnPropertyNames(keys).length > 0) {
|
||||
return mapToExpression(keys);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mapToExpression(map: {[key: string]: any}, quoted = false): o.Expression {
|
||||
return o.literalMap(
|
||||
Object.getOwnPropertyNames(map).map(key => ({key, quoted, value: asLiteral(map[key])})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing null nodes as they are implied.
|
||||
*/
|
||||
export function trimTrailingNulls(parameters: o.Expression[]): o.Expression[] {
|
||||
while (o.isNull(parameters[parameters.length - 1])) {
|
||||
parameters.pop();
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
export function getQueryPredicate(
|
||||
query: R3QueryMetadata, constantPool: ConstantPool): o.Expression {
|
||||
if (Array.isArray(query.predicate)) {
|
||||
return constantPool.getConstLiteral(
|
||||
o.literalArr(query.predicate.map(selector => o.literal(selector) as o.Expression)));
|
||||
} else {
|
||||
return query.predicate;
|
||||
}
|
||||
}
|
||||
|
||||
export function noop() {}
|
||||
|
||||
export class DefinitionMap {
|
||||
values: {key: string, quoted: boolean, value: o.Expression}[] = [];
|
||||
|
||||
set(key: string, value: o.Expression|null): void {
|
||||
if (value) {
|
||||
this.values.push({key, value, quoted: false});
|
||||
}
|
||||
}
|
||||
|
||||
toLiteralMap(): o.LiteralMapExpr { return o.literalMap(this.values); }
|
||||
}
|
Reference in New Issue
Block a user