1046 lines
42 KiB
TypeScript
Raw Normal View History

/**
* @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
*/
feat(core): allow users to define timing of ViewChild/ContentChild queries (#28810) Prior to this commit, the timing of `ViewChild`/`ContentChild` query resolution depended on the results of each query. If any results for a particular query were nested inside embedded views (e.g. *ngIfs), that query would be resolved after change detection ran. Otherwise, the query would be resolved as soon as nodes were created. This inconsistency in resolution timing had the potential to cause confusion because query results would sometimes be available in ngOnInit, but sometimes wouldn't be available until ngAfterContentInit or ngAfterViewInit. Code depending on a query result could suddenly stop working as soon as an *ngIf or an *ngFor was added to the template. With this commit, users can dictate when they want a particular `ViewChild` or `ContentChild` query to be resolved with the `static` flag. For example, one can mark a particular query as `static: false` to ensure change detection always runs before its results are set: ```ts @ContentChild('foo', {static: false}) foo !: ElementRef; ``` This means that even if there isn't a query result wrapped in an *ngIf or an *ngFor now, adding one to the template later won't change the timing of the query resolution and potentially break your component. Similarly, if you know that your query needs to be resolved earlier (e.g. you need results in an ngOnInit hook), you can mark it as `static: true`. ```ts @ViewChild(TemplateRef, {static: true}) foo !: TemplateRef; ``` Note: this means that your component will not support *ngIf results. If you do not supply a `static` option when creating your `ViewChild` or `ContentChild` query, the default query resolution timing will kick in. Note: This new option only applies to `ViewChild` and `ContentChild` queries, not `ViewChildren` or `ContentChildren` queries, as those types already resolve after CD runs. PR Close #28810
2019-02-18 14:38:14 -08:00
import {CompileDirectiveMetadata, CompilePipeSummary, CompileQueryMetadata, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata';
import {CompileReflector} from '../compile_reflector';
import {BindingForm, BuiltinConverter, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins, EventHandlerVars, LocalResolver} from '../compiler_util/expression_converter';
import {ArgumentType, BindingFlags, ChangeDetectionStrategy, NodeFlags, QueryBindingType, QueryValueType, ViewFlags} from '../core';
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
import {Identifiers} from '../identifiers';
import {LifecycleHooks} from '../lifecycle_reflector';
import {isNgContainer} from '../ml_parser/tags';
import * as o from '../output/output_ast';
import {convertValueToOutputAst} from '../output/value_util';
import {ParseSourceSpan} from '../parse_util';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, VariableAst} from '../template_parser/template_ast';
import {OutputContext} from '../util';
import {componentFactoryResolverProviderDef, depDef, lifecycleHookToNodeFlag, providerDef} from './provider_compiler';
const CLASS_ATTR = 'class';
const STYLE_ATTR = 'style';
const IMPLICIT_TEMPLATE_VAR = '\$implicit';
export class ViewCompileResult {
constructor(public viewClassVar: string, public rendererTypeVar: string) {}
}
export class ViewCompiler {
constructor(private _reflector: CompileReflector) {}
compileComponent(
outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[],
styles: o.Expression, usedPipes: CompilePipeSummary[]): ViewCompileResult {
let embeddedViewCount = 0;
let renderComponentVarName: string = undefined!;
if (!component.isHost) {
const template = component.template !;
const customRenderData: o.LiteralMapEntry[] = [];
if (template.animations && template.animations.length) {
customRenderData.push(new o.LiteralMapEntry(
'animation', convertValueToOutputAst(outputCtx, template.animations), true));
}
const renderComponentVar = o.variable(rendererTypeName(component.type.reference));
renderComponentVarName = renderComponentVar.name!;
outputCtx.statements.push(
renderComponentVar
.set(o.importExpr(Identifiers.createRendererType2).callFn([new o.LiteralMapExpr([
new o.LiteralMapEntry('encapsulation', o.literal(template.encapsulation), false),
new o.LiteralMapEntry('styles', styles, false),
new o.LiteralMapEntry('data', new o.LiteralMapExpr(customRenderData), false)
])]))
.toDeclStmt(
o.importType(Identifiers.RendererType2),
[o.StmtModifier.Final, o.StmtModifier.Exported]));
}
const viewBuilderFactory = (parent: ViewBuilder|null): ViewBuilder => {
const embeddedViewIndex = embeddedViewCount++;
return new ViewBuilder(
this._reflector, outputCtx, parent, component, embeddedViewIndex, usedPipes,
viewBuilderFactory);
};
const visitor = viewBuilderFactory(null);
visitor.visitAll([], template);
outputCtx.statements.push(...visitor.build());
return new ViewCompileResult(visitor.viewName, renderComponentVarName);
}
}
interface ViewBuilderFactory {
(parent: ViewBuilder): ViewBuilder;
}
interface UpdateExpression {
context: o.Expression;
nodeIndex: number;
bindingIndex: number;
sourceSpan: ParseSourceSpan;
value: AST;
}
const LOG_VAR = o.variable('_l');
const VIEW_VAR = o.variable('_v');
const CHECK_VAR = o.variable('_ck');
const COMP_VAR = o.variable('_co');
const EVENT_NAME_VAR = o.variable('en');
const ALLOW_DEFAULT_VAR = o.variable(`ad`);
class ViewBuilder implements TemplateAstVisitor, LocalResolver {
private compType: o.Type;
private nodes: (() => {
sourceSpan: ParseSourceSpan | null,
nodeDef: o.Expression,
nodeFlags: NodeFlags,
updateDirectives?: UpdateExpression[],
updateRenderer?: UpdateExpression[]
})[] = [];
private purePipeNodeIndices: {[pipeName: string]: number} = Object.create(null);
// Need Object.create so that we don't have builtin values...
private refNodeIndices: {[refName: string]: number} = Object.create(null);
private variables: VariableAst[] = [];
private children: ViewBuilder[] = [];
public readonly viewName: string;
constructor(
private reflector: CompileReflector, private outputCtx: OutputContext,
private parent: ViewBuilder|null, private component: CompileDirectiveMetadata,
private embeddedViewIndex: number, private usedPipes: CompilePipeSummary[],
private viewBuilderFactory: ViewBuilderFactory) {
// TODO(tbosch): The old view compiler used to use an `any` type
// for the context in any embedded view. We keep this behaivor for now
// to be able to introduce the new view compiler without too many errors.
this.compType = this.embeddedViewIndex > 0 ?
o.DYNAMIC_TYPE :
o.expressionType(outputCtx.importExpr(this.component.type.reference))!;
this.viewName = viewClassName(this.component.type.reference, this.embeddedViewIndex);
}
visitAll(variables: VariableAst[], astNodes: TemplateAst[]) {
this.variables = variables;
// create the pipes for the pure pipes immediately, so that we know their indices.
if (!this.parent) {
this.usedPipes.forEach((pipe) => {
if (pipe.pure) {
this.purePipeNodeIndices[pipe.name] = this._createPipe(null, pipe);
}
});
}
if (!this.parent) {
this.component.viewQueries.forEach((query, queryIndex) => {
// Note: queries start with id 1 so we can use the number in a Bloom filter!
const queryId = queryIndex + 1;
const bindingType = query.first ? QueryBindingType.First : QueryBindingType.All;
const flags = NodeFlags.TypeViewQuery | calcStaticDynamicQueryFlags(query);
this.nodes.push(() => ({
sourceSpan: null,
nodeFlags: flags,
nodeDef: o.importExpr(Identifiers.queryDef).callFn([
o.literal(flags), o.literal(queryId),
new o.LiteralMapExpr([new o.LiteralMapEntry(
query.propertyName, o.literal(bindingType), false)])
])
}));
});
}
templateVisitAll(this, astNodes);
if (this.parent && (astNodes.length === 0 || needsAdditionalRootNode(astNodes))) {
// if the view is an embedded view, then we need to add an additional root node in some cases
this.nodes.push(() => ({
sourceSpan: null,
nodeFlags: NodeFlags.TypeElement,
nodeDef: o.importExpr(Identifiers.anchorDef).callFn([
o.literal(NodeFlags.None), o.NULL_EXPR, o.NULL_EXPR, o.literal(0)
])
}));
}
}
build(targetStatements: o.Statement[] = []): o.Statement[] {
this.children.forEach((child) => child.build(targetStatements));
const {updateRendererStmts, updateDirectivesStmts, nodeDefExprs} =
this._createNodeExpressions();
const updateRendererFn = this._createUpdateFn(updateRendererStmts);
const updateDirectivesFn = this._createUpdateFn(updateDirectivesStmts);
let viewFlags = ViewFlags.None;
if (!this.parent && this.component.changeDetection === ChangeDetectionStrategy.OnPush) {
viewFlags |= ViewFlags.OnPush;
}
const viewFactory = new o.DeclareFunctionStmt(
this.viewName, [new o.FnParam(LOG_VAR.name!)],
[new o.ReturnStatement(o.importExpr(Identifiers.viewDef).callFn([
o.literal(viewFlags),
o.literalArr(nodeDefExprs),
updateDirectivesFn,
updateRendererFn,
]))],
o.importType(Identifiers.ViewDefinition),
this.embeddedViewIndex === 0 ? [o.StmtModifier.Exported] : []);
targetStatements.push(viewFactory);
return targetStatements;
}
private _createUpdateFn(updateStmts: o.Statement[]): o.Expression {
let updateFn: o.Expression;
if (updateStmts.length > 0) {
const preStmts: o.Statement[] = [];
if (!this.component.isHost && o.findReadVarNames(updateStmts).has(COMP_VAR.name!)) {
preStmts.push(COMP_VAR.set(VIEW_VAR.prop('component')).toDeclStmt(this.compType));
}
updateFn = o.fn(
[
new o.FnParam(CHECK_VAR.name!, o.INFERRED_TYPE),
new o.FnParam(VIEW_VAR.name!, o.INFERRED_TYPE)
],
[...preStmts, ...updateStmts], o.INFERRED_TYPE);
} else {
updateFn = o.NULL_EXPR;
}
return updateFn;
}
visitNgContent(ast: NgContentAst, context: any): any {
// ngContentDef(ngContentIndex: number, index: number): NodeDef;
this.nodes.push(() => ({
sourceSpan: ast.sourceSpan,
nodeFlags: NodeFlags.TypeNgContent,
nodeDef: o.importExpr(Identifiers.ngContentDef)
.callFn([o.literal(ast.ngContentIndex), o.literal(ast.index)])
}));
}
visitText(ast: TextAst, context: any): any {
// Static text nodes have no check function
const checkIndex = -1;
this.nodes.push(() => ({
sourceSpan: ast.sourceSpan,
nodeFlags: NodeFlags.TypeText,
nodeDef: o.importExpr(Identifiers.textDef).callFn([
o.literal(checkIndex),
o.literal(ast.ngContentIndex),
o.literalArr([o.literal(ast.value)]),
])
}));
}
visitBoundText(ast: BoundTextAst, context: any): any {
const nodeIndex = this.nodes.length;
// reserve the space in the nodeDefs array
this.nodes.push(null!);
const astWithSource = <ASTWithSource>ast.value;
const inter = <Interpolation>astWithSource.ast;
const updateRendererExpressions = inter.expressions.map(
(expr, bindingIndex) => this._preprocessUpdateExpression(
{nodeIndex, bindingIndex, sourceSpan: ast.sourceSpan, context: COMP_VAR, value: expr}));
// Check index is the same as the node index during compilation
// They might only differ at runtime
const checkIndex = nodeIndex;
this.nodes[nodeIndex] = () => ({
sourceSpan: ast.sourceSpan,
nodeFlags: NodeFlags.TypeText,
nodeDef: o.importExpr(Identifiers.textDef).callFn([
o.literal(checkIndex),
o.literal(ast.ngContentIndex),
o.literalArr(inter.strings.map(s => o.literal(s))),
]),
updateRenderer: updateRendererExpressions
});
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
const nodeIndex = this.nodes.length;
// reserve the space in the nodeDefs array
this.nodes.push(null!);
const {flags, queryMatchesExpr, hostEvents} = this._visitElementOrTemplate(nodeIndex, ast);
const childVisitor = this.viewBuilderFactory(this);
this.children.push(childVisitor);
childVisitor.visitAll(ast.variables, ast.children);
const childCount = this.nodes.length - nodeIndex - 1;
// anchorDef(
// flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
// childCount: number, handleEventFn?: ElementHandleEventFn, templateFactory?:
// ViewDefinitionFactory): NodeDef;
this.nodes[nodeIndex] = () => ({
sourceSpan: ast.sourceSpan,
nodeFlags: NodeFlags.TypeElement | flags,
nodeDef: o.importExpr(Identifiers.anchorDef).callFn([
o.literal(flags),
queryMatchesExpr,
o.literal(ast.ngContentIndex),
o.literal(childCount),
this._createElementHandleEventFn(nodeIndex, hostEvents),
o.variable(childVisitor.viewName),
])
});
}
visitElement(ast: ElementAst, context: any): any {
const nodeIndex = this.nodes.length;
// reserve the space in the nodeDefs array so we can add children
this.nodes.push(null!);
// Using a null element name creates an anchor.
const elName: string|null = isNgContainer(ast.name) ? null : ast.name;
const {flags, usedEvents, queryMatchesExpr, hostBindings: dirHostBindings, hostEvents} =
this._visitElementOrTemplate(nodeIndex, ast);
let inputDefs: o.Expression[] = [];
let updateRendererExpressions: UpdateExpression[] = [];
let outputDefs: o.Expression[] = [];
if (elName) {
const hostBindings: any[] = ast.inputs
.map((inputAst) => ({
context: COMP_VAR as o.Expression,
inputAst,
dirAst: null as any,
}))
.concat(dirHostBindings);
if (hostBindings.length) {
updateRendererExpressions =
hostBindings.map((hostBinding, bindingIndex) => this._preprocessUpdateExpression({
context: hostBinding.context,
nodeIndex,
bindingIndex,
sourceSpan: hostBinding.inputAst.sourceSpan,
value: hostBinding.inputAst.value
}));
inputDefs = hostBindings.map(
hostBinding => elementBindingDef(hostBinding.inputAst, hostBinding.dirAst));
}
outputDefs = usedEvents.map(
([target, eventName]) => o.literalArr([o.literal(target), o.literal(eventName)]));
}
templateVisitAll(this, ast.children);
const childCount = this.nodes.length - nodeIndex - 1;
const compAst = ast.directives.find(dirAst => dirAst.directive.isComponent);
let compRendererType = o.NULL_EXPR as o.Expression;
let compView = o.NULL_EXPR as o.Expression;
if (compAst) {
compView = this.outputCtx.importExpr(compAst.directive.componentViewType);
compRendererType = this.outputCtx.importExpr(compAst.directive.rendererType);
}
// Check index is the same as the node index during compilation
// They might only differ at runtime
const checkIndex = nodeIndex;
this.nodes[nodeIndex] = () => ({
sourceSpan: ast.sourceSpan,
nodeFlags: NodeFlags.TypeElement | flags,
nodeDef: o.importExpr(Identifiers.elementDef).callFn([
o.literal(checkIndex),
o.literal(flags),
queryMatchesExpr,
o.literal(ast.ngContentIndex),
o.literal(childCount),
o.literal(elName),
elName ? fixedAttrsDef(ast) : o.NULL_EXPR,
inputDefs.length ? o.literalArr(inputDefs) : o.NULL_EXPR,
outputDefs.length ? o.literalArr(outputDefs) : o.NULL_EXPR,
this._createElementHandleEventFn(nodeIndex, hostEvents),
compView,
compRendererType,
]),
updateRenderer: updateRendererExpressions
});
}
private _visitElementOrTemplate(nodeIndex: number, ast: {
hasViewContainer: boolean,
outputs: BoundEventAst[],
directives: DirectiveAst[],
providers: ProviderAst[],
references: ReferenceAst[],
queryMatches: QueryMatch[]
}): {
flags: NodeFlags,
usedEvents: [string|null, string][],
queryMatchesExpr: o.Expression,
hostBindings:
{context: o.Expression, inputAst: BoundElementPropertyAst, dirAst: DirectiveAst}[],
hostEvents: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[],
} {
let flags = NodeFlags.None;
if (ast.hasViewContainer) {
flags |= NodeFlags.EmbeddedViews;
}
const usedEvents = new Map<string, [string | null, string]>();
ast.outputs.forEach((event) => {
const {name, target} = elementEventNameAndTarget(event, null);
usedEvents.set(elementEventFullName(target, name), [target, name]);
});
ast.directives.forEach((dirAst) => {
dirAst.hostEvents.forEach((event) => {
const {name, target} = elementEventNameAndTarget(event, dirAst);
usedEvents.set(elementEventFullName(target, name), [target, name]);
});
});
const hostBindings:
{context: o.Expression, inputAst: BoundElementPropertyAst, dirAst: DirectiveAst}[] = [];
const hostEvents: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[] = [];
this._visitComponentFactoryResolverProvider(ast.directives);
ast.providers.forEach(providerAst => {
let dirAst: DirectiveAst = undefined!;
ast.directives.forEach(localDirAst => {
if (localDirAst.directive.type.reference === tokenReference(providerAst.token)) {
dirAst = localDirAst;
}
});
if (dirAst) {
const {hostBindings: dirHostBindings, hostEvents: dirHostEvents} =
this._visitDirective(providerAst, dirAst, ast.references, ast.queryMatches, usedEvents);
hostBindings.push(...dirHostBindings);
hostEvents.push(...dirHostEvents);
} else {
this._visitProvider(providerAst, ast.queryMatches);
}
});
let queryMatchExprs: o.Expression[] = [];
ast.queryMatches.forEach((match) => {
let valueType: QueryValueType = undefined!;
if (tokenReference(match.value) ===
this.reflector.resolveExternalReference(Identifiers.ElementRef)) {
valueType = QueryValueType.ElementRef;
} else if (
tokenReference(match.value) ===
this.reflector.resolveExternalReference(Identifiers.ViewContainerRef)) {
valueType = QueryValueType.ViewContainerRef;
} else if (
tokenReference(match.value) ===
this.reflector.resolveExternalReference(Identifiers.TemplateRef)) {
valueType = QueryValueType.TemplateRef;
}
if (valueType != null) {
queryMatchExprs.push(o.literalArr([o.literal(match.queryId), o.literal(valueType)]));
}
});
ast.references.forEach((ref) => {
let valueType: QueryValueType = undefined!;
if (!ref.value) {
valueType = QueryValueType.RenderElement;
} else if (
tokenReference(ref.value) ===
this.reflector.resolveExternalReference(Identifiers.TemplateRef)) {
valueType = QueryValueType.TemplateRef;
}
if (valueType != null) {
this.refNodeIndices[ref.name] = nodeIndex;
queryMatchExprs.push(o.literalArr([o.literal(ref.name), o.literal(valueType)]));
}
});
ast.outputs.forEach((outputAst) => {
hostEvents.push({context: COMP_VAR, eventAst: outputAst, dirAst: null!});
});
return {
flags,
usedEvents: Array.from(usedEvents.values()),
queryMatchesExpr: queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR,
hostBindings,
hostEvents: hostEvents
};
}
private _visitDirective(
providerAst: ProviderAst, dirAst: DirectiveAst, refs: ReferenceAst[],
queryMatches: QueryMatch[], usedEvents: Map<string, any>): {
hostBindings:
{context: o.Expression, inputAst: BoundElementPropertyAst, dirAst: DirectiveAst}[],
hostEvents: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[]
} {
const nodeIndex = this.nodes.length;
// reserve the space in the nodeDefs array so we can add children
this.nodes.push(null!);
dirAst.directive.queries.forEach((query, queryIndex) => {
const queryId = dirAst.contentQueryStartId + queryIndex;
const flags = NodeFlags.TypeContentQuery | calcStaticDynamicQueryFlags(query);
const bindingType = query.first ? QueryBindingType.First : QueryBindingType.All;
this.nodes.push(() => ({
sourceSpan: dirAst.sourceSpan,
nodeFlags: flags,
nodeDef: o.importExpr(Identifiers.queryDef).callFn([
o.literal(flags), o.literal(queryId),
new o.LiteralMapExpr([new o.LiteralMapEntry(
query.propertyName, o.literal(bindingType), false)])
]),
}));
});
// Note: the operation below might also create new nodeDefs,
// but we don't want them to be a child of a directive,
// as they might be a provider/pipe on their own.
// I.e. we only allow queries as children of directives nodes.
const childCount = this.nodes.length - nodeIndex - 1;
let {flags, queryMatchExprs, providerExpr, depsExpr} =
this._visitProviderOrDirective(providerAst, queryMatches);
refs.forEach((ref) => {
if (ref.value && tokenReference(ref.value) === tokenReference(providerAst.token)) {
this.refNodeIndices[ref.name] = nodeIndex;
queryMatchExprs.push(
o.literalArr([o.literal(ref.name), o.literal(QueryValueType.Provider)]));
}
});
if (dirAst.directive.isComponent) {
flags |= NodeFlags.Component;
}
const inputDefs = dirAst.inputs.map((inputAst, inputIndex) => {
const mapValue = o.literalArr([o.literal(inputIndex), o.literal(inputAst.directiveName)]);
// Note: it's important to not quote the key so that we can capture renames by minifiers!
return new o.LiteralMapEntry(inputAst.directiveName, mapValue, false);
});
const outputDefs: o.LiteralMapEntry[] = [];
const dirMeta = dirAst.directive;
Object.keys(dirMeta.outputs).forEach((propName) => {
const eventName = dirMeta.outputs[propName];
if (usedEvents.has(eventName)) {
// Note: it's important to not quote the key so that we can capture renames by minifiers!
outputDefs.push(new o.LiteralMapEntry(propName, o.literal(eventName), false));
}
});
let updateDirectiveExpressions: UpdateExpression[] = [];
if (dirAst.inputs.length || (flags & (NodeFlags.DoCheck | NodeFlags.OnInit)) > 0) {
updateDirectiveExpressions =
dirAst.inputs.map((input, bindingIndex) => this._preprocessUpdateExpression({
nodeIndex,
bindingIndex,
sourceSpan: input.sourceSpan,
context: COMP_VAR,
value: input.value
}));
}
const dirContextExpr =
o.importExpr(Identifiers.nodeValue).callFn([VIEW_VAR, o.literal(nodeIndex)]);
const hostBindings = dirAst.hostProperties.map((inputAst) => ({
context: dirContextExpr,
dirAst,
inputAst,
}));
const hostEvents = dirAst.hostEvents.map((hostEventAst) => ({
context: dirContextExpr,
eventAst: hostEventAst,
dirAst,
}));
// Check index is the same as the node index during compilation
// They might only differ at runtime
const checkIndex = nodeIndex;
this.nodes[nodeIndex] = () => ({
sourceSpan: dirAst.sourceSpan,
nodeFlags: NodeFlags.TypeDirective | flags,
nodeDef: o.importExpr(Identifiers.directiveDef).callFn([
o.literal(checkIndex),
o.literal(flags),
queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR,
o.literal(childCount),
providerExpr,
depsExpr,
inputDefs.length ? new o.LiteralMapExpr(inputDefs) : o.NULL_EXPR,
outputDefs.length ? new o.LiteralMapExpr(outputDefs) : o.NULL_EXPR,
]),
updateDirectives: updateDirectiveExpressions,
directive: dirAst.directive.type,
});
return {hostBindings, hostEvents};
}
private _visitProvider(providerAst: ProviderAst, queryMatches: QueryMatch[]): void {
this._addProviderNode(this._visitProviderOrDirective(providerAst, queryMatches));
}
private _visitComponentFactoryResolverProvider(directives: DirectiveAst[]) {
const componentDirMeta = directives.find(dirAst => dirAst.directive.isComponent);
if (componentDirMeta && componentDirMeta.directive.entryComponents.length) {
const {providerExpr, depsExpr, flags, tokenExpr} = componentFactoryResolverProviderDef(
this.reflector, this.outputCtx, NodeFlags.PrivateProvider,
componentDirMeta.directive.entryComponents);
this._addProviderNode({
providerExpr,
depsExpr,
flags,
tokenExpr,
queryMatchExprs: [],
sourceSpan: componentDirMeta.sourceSpan
});
}
}
private _addProviderNode(data: {
flags: NodeFlags,
queryMatchExprs: o.Expression[],
providerExpr: o.Expression,
depsExpr: o.Expression,
tokenExpr: o.Expression,
sourceSpan: ParseSourceSpan
}) {
// providerDef(
// flags: NodeFlags, matchedQueries: [string, QueryValueType][], token:any,
// value: any, deps: ([DepFlags, any] | any)[]): NodeDef;
this.nodes.push(
() => ({
sourceSpan: data.sourceSpan,
nodeFlags: data.flags,
nodeDef: o.importExpr(Identifiers.providerDef).callFn([
o.literal(data.flags),
data.queryMatchExprs.length ? o.literalArr(data.queryMatchExprs) : o.NULL_EXPR,
data.tokenExpr, data.providerExpr, data.depsExpr
])
}));
}
private _visitProviderOrDirective(providerAst: ProviderAst, queryMatches: QueryMatch[]): {
flags: NodeFlags,
tokenExpr: o.Expression,
sourceSpan: ParseSourceSpan,
queryMatchExprs: o.Expression[],
providerExpr: o.Expression,
depsExpr: o.Expression
} {
let flags = NodeFlags.None;
let queryMatchExprs: o.Expression[] = [];
queryMatches.forEach((match) => {
if (tokenReference(match.value) === tokenReference(providerAst.token)) {
queryMatchExprs.push(
o.literalArr([o.literal(match.queryId), o.literal(QueryValueType.Provider)]));
}
});
const {providerExpr, depsExpr, flags: providerFlags, tokenExpr} =
providerDef(this.outputCtx, providerAst);
return {
flags: flags | providerFlags,
queryMatchExprs,
providerExpr,
depsExpr,
tokenExpr,
sourceSpan: providerAst.sourceSpan
};
}
getLocal(name: string): o.Expression|null {
if (name == EventHandlerVars.event.name) {
return EventHandlerVars.event;
}
let currViewExpr: o.Expression = VIEW_VAR;
for (let currBuilder: ViewBuilder|null = this; currBuilder; currBuilder = currBuilder.parent,
currViewExpr = currViewExpr.prop('parent').cast(o.DYNAMIC_TYPE)) {
// check references
const refNodeIndex = currBuilder.refNodeIndices[name];
if (refNodeIndex != null) {
return o.importExpr(Identifiers.nodeValue).callFn([currViewExpr, o.literal(refNodeIndex)]);
}
// check variables
const varAst = currBuilder.variables.find((varAst) => varAst.name === name);
if (varAst) {
const varValue = varAst.value || IMPLICIT_TEMPLATE_VAR;
return currViewExpr.prop('context').prop(varValue);
}
}
return null;
}
fix(ivy): unable to bind to implicit receiver in embedded views (#30897) To provide some context: The implicit receiver is part of the parsed Angular template AST. Any property reads in bindings, interpolations etc. read from a given object (usually the component instance). In that case there is an _implicit_ receiver which can also be specified explicitly by just using `this`. e.g. ```html <ng-template>{{this.myProperty}}</ng-template> ``` This works as expected in Ivy and View Engine, but breaks in case the implicit receiver is not used for property reads. For example: ```html <my-dir [myFn]="greetFn.bind(this)"></my-dir> ``` In that case the `this` will not be properly translated into the generated template function code because the Ivy compiler currently always treats the `ctx` variable as the implicit receiver. This is **not correct** and breaks compatibility with View Engine. Rather we need to ensure that we retrieve the root context for the standalone implicit receiver similar to how it works for property reads (as seen in the example above with `this.myProperty`) Note that this requires some small changes to the `expression_converter` because we only want to generate the `eenextContent()` instruction if the implicit receiver is _actually_ used/needed. View Engine determines if that is the case by recursively walking through the converted output AST and checking for usages of the `o.variable('_co')` variable ([see here][ve_check]). This would work too for Ivy, but involves most likely more code duplication since templates are isolated in different functions and it another pass through the output AST for every template expression. [ve_check]: https://github.com/angular/angular/blob/0d6c9d36a174f7dc6eb1029e459beecc2dfb0026/packages/compiler/src/view_compiler/view_compiler.ts#L206-L208 Resolves FW-1366. PR Close #30897
2019-06-06 20:01:51 +02:00
notifyImplicitReceiverUse(): void {
// Not needed in View Engine as View Engine walks through the generated
// expressions to figure out if the implicit receiver is used and needs
// to be generated as part of the pre-update statements.
}
private _createLiteralArrayConverter(sourceSpan: ParseSourceSpan, argCount: number):
BuiltinConverter {
if (argCount === 0) {
const valueExpr = o.importExpr(Identifiers.EMPTY_ARRAY);
return () => valueExpr;
}
const checkIndex = this.nodes.length;
this.nodes.push(() => ({
sourceSpan,
nodeFlags: NodeFlags.TypePureArray,
nodeDef: o.importExpr(Identifiers.pureArrayDef).callFn([
o.literal(checkIndex),
o.literal(argCount),
])
}));
return (args: o.Expression[]) => callCheckStmt(checkIndex, args);
}
private _createLiteralMapConverter(
sourceSpan: ParseSourceSpan, keys: {key: string, quoted: boolean}[]): BuiltinConverter {
if (keys.length === 0) {
const valueExpr = o.importExpr(Identifiers.EMPTY_MAP);
return () => valueExpr;
}
const map = o.literalMap(keys.map((e, i) => ({...e, value: o.literal(i)})));
const checkIndex = this.nodes.length;
this.nodes.push(() => ({
sourceSpan,
nodeFlags: NodeFlags.TypePureObject,
nodeDef: o.importExpr(Identifiers.pureObjectDef).callFn([
o.literal(checkIndex),
map,
])
}));
return (args: o.Expression[]) => callCheckStmt(checkIndex, args);
}
private _createPipeConverter(expression: UpdateExpression, name: string, argCount: number):
BuiltinConverter {
const pipe = this.usedPipes.find((pipeSummary) => pipeSummary.name === name)!;
if (pipe.pure) {
const checkIndex = this.nodes.length;
this.nodes.push(() => ({
sourceSpan: expression.sourceSpan,
nodeFlags: NodeFlags.TypePurePipe,
nodeDef: o.importExpr(Identifiers.purePipeDef).callFn([
o.literal(checkIndex),
o.literal(argCount),
])
}));
// find underlying pipe in the component view
let compViewExpr: o.Expression = VIEW_VAR;
let compBuilder: ViewBuilder = this;
while (compBuilder.parent) {
compBuilder = compBuilder.parent;
compViewExpr = compViewExpr.prop('parent').cast(o.DYNAMIC_TYPE);
}
const pipeNodeIndex = compBuilder.purePipeNodeIndices[name];
const pipeValueExpr: o.Expression =
o.importExpr(Identifiers.nodeValue).callFn([compViewExpr, o.literal(pipeNodeIndex)]);
return (args: o.Expression[]) => callUnwrapValue(
expression.nodeIndex, expression.bindingIndex,
callCheckStmt(checkIndex, [pipeValueExpr].concat(args)));
} else {
const nodeIndex = this._createPipe(expression.sourceSpan, pipe);
const nodeValueExpr =
o.importExpr(Identifiers.nodeValue).callFn([VIEW_VAR, o.literal(nodeIndex)]);
return (args: o.Expression[]) => callUnwrapValue(
expression.nodeIndex, expression.bindingIndex,
nodeValueExpr.callMethod('transform', args));
}
}
private _createPipe(sourceSpan: ParseSourceSpan|null, pipe: CompilePipeSummary): number {
const nodeIndex = this.nodes.length;
let flags = NodeFlags.None;
pipe.type.lifecycleHooks.forEach((lifecycleHook) => {
// for pipes, we only support ngOnDestroy
if (lifecycleHook === LifecycleHooks.OnDestroy) {
flags |= lifecycleHookToNodeFlag(lifecycleHook);
}
});
const depExprs = pipe.type.diDeps.map((diDep) => depDef(this.outputCtx, diDep));
// function pipeDef(
// flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[]): NodeDef
this.nodes.push(
() => ({
sourceSpan,
nodeFlags: NodeFlags.TypePipe,
nodeDef: o.importExpr(Identifiers.pipeDef).callFn([
o.literal(flags), this.outputCtx.importExpr(pipe.type.reference), o.literalArr(depExprs)
])
}));
return nodeIndex;
}
/**
* For the AST in `UpdateExpression.value`:
* - create nodes for pipes, literal arrays and, literal maps,
* - update the AST to replace pipes, literal arrays and, literal maps with calls to check fn.
*
* WARNING: This might create new nodeDefs (for pipes and literal arrays and literal maps)!
*/
private _preprocessUpdateExpression(expression: UpdateExpression): UpdateExpression {
return {
nodeIndex: expression.nodeIndex,
bindingIndex: expression.bindingIndex,
sourceSpan: expression.sourceSpan,
context: expression.context,
value: convertPropertyBindingBuiltins(
{
createLiteralArrayConverter: (argCount: number) =>
this._createLiteralArrayConverter(expression.sourceSpan, argCount),
createLiteralMapConverter: (keys: {key: string, quoted: boolean}[]) =>
this._createLiteralMapConverter(expression.sourceSpan, keys),
createPipeConverter: (name: string, argCount: number) =>
this._createPipeConverter(expression, name, argCount)
},
expression.value)
};
}
private _createNodeExpressions(): {
updateRendererStmts: o.Statement[],
updateDirectivesStmts: o.Statement[],
nodeDefExprs: o.Expression[]
} {
const self = this;
let updateBindingCount = 0;
const updateRendererStmts: o.Statement[] = [];
const updateDirectivesStmts: o.Statement[] = [];
const nodeDefExprs = this.nodes.map((factory, nodeIndex) => {
const {nodeDef, nodeFlags, updateDirectives, updateRenderer, sourceSpan} = factory();
if (updateRenderer) {
updateRendererStmts.push(
...createUpdateStatements(nodeIndex, sourceSpan, updateRenderer, false));
}
if (updateDirectives) {
updateDirectivesStmts.push(...createUpdateStatements(
nodeIndex, sourceSpan, updateDirectives,
(nodeFlags & (NodeFlags.DoCheck | NodeFlags.OnInit)) > 0));
}
// We use a comma expression to call the log function before
// the nodeDef function, but still use the result of the nodeDef function
// as the value.
// Note: We only add the logger to elements / text nodes,
// so we don't generate too much code.
const logWithNodeDef = nodeFlags & NodeFlags.CatRenderNode ?
new o.CommaExpr([LOG_VAR.callFn([]).callFn([]), nodeDef]) :
nodeDef;
return o.applySourceSpanToExpressionIfNeeded(logWithNodeDef, sourceSpan);
});
return {updateRendererStmts, updateDirectivesStmts, nodeDefExprs};
function createUpdateStatements(
nodeIndex: number, sourceSpan: ParseSourceSpan|null, expressions: UpdateExpression[],
allowEmptyExprs: boolean): o.Statement[] {
const updateStmts: o.Statement[] = [];
const exprs = expressions.map(({sourceSpan, context, value}) => {
const bindingId = `${updateBindingCount++}`;
const nameResolver = context === COMP_VAR ? self : null;
const {stmts, currValExpr} =
convertPropertyBinding(nameResolver, context, value, bindingId, BindingForm.General);
updateStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
return o.applySourceSpanToExpressionIfNeeded(currValExpr, sourceSpan);
});
if (expressions.length || allowEmptyExprs) {
updateStmts.push(o.applySourceSpanToStatementIfNeeded(
callCheckStmt(nodeIndex, exprs).toStmt(), sourceSpan));
}
return updateStmts;
}
}
private _createElementHandleEventFn(
nodeIndex: number,
handlers: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[]) {
const handleEventStmts: o.Statement[] = [];
let handleEventBindingCount = 0;
handlers.forEach(({context, eventAst, dirAst}) => {
const bindingId = `${handleEventBindingCount++}`;
const nameResolver = context === COMP_VAR ? this : null;
const {stmts, allowDefault} =
convertActionBinding(nameResolver, context, eventAst.handler, bindingId);
const trueStmts = stmts;
if (allowDefault) {
trueStmts.push(ALLOW_DEFAULT_VAR.set(allowDefault.and(ALLOW_DEFAULT_VAR)).toStmt());
}
const {target: eventTarget, name: eventName} = elementEventNameAndTarget(eventAst, dirAst);
const fullEventName = elementEventFullName(eventTarget, eventName);
handleEventStmts.push(o.applySourceSpanToStatementIfNeeded(
new o.IfStmt(o.literal(fullEventName).identical(EVENT_NAME_VAR), trueStmts),
eventAst.sourceSpan));
});
let handleEventFn: o.Expression;
if (handleEventStmts.length > 0) {
const preStmts: o.Statement[] =
[ALLOW_DEFAULT_VAR.set(o.literal(true)).toDeclStmt(o.BOOL_TYPE)];
if (!this.component.isHost && o.findReadVarNames(handleEventStmts).has(COMP_VAR.name!)) {
preStmts.push(COMP_VAR.set(VIEW_VAR.prop('component')).toDeclStmt(this.compType));
}
handleEventFn = o.fn(
[
new o.FnParam(VIEW_VAR.name!, o.INFERRED_TYPE),
new o.FnParam(EVENT_NAME_VAR.name!, o.INFERRED_TYPE),
new o.FnParam(EventHandlerVars.event.name!, o.INFERRED_TYPE)
],
[...preStmts, ...handleEventStmts, new o.ReturnStatement(ALLOW_DEFAULT_VAR)],
o.INFERRED_TYPE);
} else {
handleEventFn = o.NULL_EXPR;
}
return handleEventFn;
}
visitDirective(ast: DirectiveAst, context: {usedEvents: Set<string>}): any {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {}
visitReference(ast: ReferenceAst, context: any): any {}
visitVariable(ast: VariableAst, context: any): any {}
visitEvent(ast: BoundEventAst, context: any): any {}
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {}
visitAttr(ast: AttrAst, context: any): any {}
}
function needsAdditionalRootNode(astNodes: TemplateAst[]): boolean {
const lastAstNode = astNodes[astNodes.length - 1];
if (lastAstNode instanceof EmbeddedTemplateAst) {
return lastAstNode.hasViewContainer;
}
if (lastAstNode instanceof ElementAst) {
if (isNgContainer(lastAstNode.name) && lastAstNode.children.length) {
return needsAdditionalRootNode(lastAstNode.children);
}
return lastAstNode.hasViewContainer;
}
return lastAstNode instanceof NgContentAst;
}
function elementBindingDef(inputAst: BoundElementPropertyAst, dirAst: DirectiveAst): o.Expression {
const inputType = inputAst.type;
switch (inputType) {
case PropertyBindingType.Attribute:
return o.literalArr([
o.literal(BindingFlags.TypeElementAttribute), o.literal(inputAst.name),
o.literal(inputAst.securityContext)
]);
case PropertyBindingType.Property:
return o.literalArr([
o.literal(BindingFlags.TypeProperty), o.literal(inputAst.name),
o.literal(inputAst.securityContext)
]);
case PropertyBindingType.Animation:
const bindingType = BindingFlags.TypeProperty |
(dirAst && dirAst.directive.isComponent ? BindingFlags.SyntheticHostProperty :
BindingFlags.SyntheticProperty);
return o.literalArr([
o.literal(bindingType), o.literal('@' + inputAst.name), o.literal(inputAst.securityContext)
]);
case PropertyBindingType.Class:
return o.literalArr(
[o.literal(BindingFlags.TypeElementClass), o.literal(inputAst.name), o.NULL_EXPR]);
case PropertyBindingType.Style:
return o.literalArr([
o.literal(BindingFlags.TypeElementStyle), o.literal(inputAst.name), o.literal(inputAst.unit)
]);
default:
// This default case is not needed by TypeScript compiler, as the switch is exhaustive.
// However Closure Compiler does not understand that and reports an error in typed mode.
// The `throw new Error` below works around the problem, and the unexpected: never variable
// makes sure tsc still checks this code is unreachable.
const unexpected: never = inputType;
throw new Error(`unexpected ${unexpected}`);
}
}
function fixedAttrsDef(elementAst: ElementAst): o.Expression {
const mapResult: {[key: string]: string} = Object.create(null);
elementAst.attrs.forEach(attrAst => {
mapResult[attrAst.name] = attrAst.value;
});
elementAst.directives.forEach(dirAst => {
Object.keys(dirAst.directive.hostAttributes).forEach(name => {
const value = dirAst.directive.hostAttributes[name];
const prevValue = mapResult[name];
mapResult[name] = prevValue != null ? mergeAttributeValue(name, prevValue, value) : value;
});
});
// Note: We need to sort to get a defined output order
// for tests and for caching generated artifacts...
return o.literalArr(Object.keys(mapResult).sort().map(
(attrName) => o.literalArr([o.literal(attrName), o.literal(mapResult[attrName])])));
}
function mergeAttributeValue(attrName: string, attrValue1: string, attrValue2: string): string {
if (attrName == CLASS_ATTR || attrName == STYLE_ATTR) {
return `${attrValue1} ${attrValue2}`;
} else {
return attrValue2;
}
}
function callCheckStmt(nodeIndex: number, exprs: o.Expression[]): o.Expression {
if (exprs.length > 10) {
return CHECK_VAR.callFn(
[VIEW_VAR, o.literal(nodeIndex), o.literal(ArgumentType.Dynamic), o.literalArr(exprs)]);
} else {
return CHECK_VAR.callFn(
[VIEW_VAR, o.literal(nodeIndex), o.literal(ArgumentType.Inline), ...exprs]);
}
}
function callUnwrapValue(nodeIndex: number, bindingIdx: number, expr: o.Expression): o.Expression {
return o.importExpr(Identifiers.unwrapValue).callFn([
VIEW_VAR, o.literal(nodeIndex), o.literal(bindingIdx), expr
]);
}
function elementEventNameAndTarget(
eventAst: BoundEventAst, dirAst: DirectiveAst|null): {name: string, target: string|null} {
if (eventAst.isAnimation) {
return {
name: `@${eventAst.name}.${eventAst.phase}`,
target: dirAst && dirAst.directive.isComponent ? 'component' : null
};
} else {
return eventAst;
}
}
function calcStaticDynamicQueryFlags(query: CompileQueryMetadata) {
let flags = NodeFlags.None;
// Note: We only make queries static that query for a single item and the user specifically
// set the to be static. This is because of backwards compatibility with the old view compiler...
if (query.first && query.static) {
flags |= NodeFlags.StaticQuery;
} else {
flags |= NodeFlags.DynamicQuery;
}
return flags;
}
export function elementEventFullName(target: string|null, name: string): string {
return target ? `${target}:${name}` : name;
}