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]: 0d6c9d36a1/packages/compiler/src/view_compiler/view_compiler.ts (L206-L208)

Resolves FW-1366.

PR Close #30897
This commit is contained in:
Paul Gschwendtner
2019-06-06 20:01:51 +02:00
committed by Andrew Kushnir
parent 7912db3829
commit 58be2ff884
7 changed files with 255 additions and 49 deletions

View File

@ -178,6 +178,59 @@ describe('compiler compliance: template', () => {
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should correctly bind to implicit receiver in template', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div *ngIf="true" (click)="greet(this)"></div>
<div *ngIf="true" [id]="this"></div>
\`
})
export class MyComponent {
greet(val: any) {}
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
function MyComponent_div_0_Template(rf, ctx) {
if (rf & 1) {
const $_r2$ = i0.ɵɵgetCurrentView();
$r3$.ɵɵelementStart(0, "div", $_c1$);
$r3$.ɵɵlistener("click", function MyComponent_div_0_Template_div_click_0_listener($event) {
i0.ɵɵrestoreView($_r2$);
const $ctx_r1$ = i0.ɵɵnextContext();
return $ctx_r1$.greet($ctx_r1$);
});
$r3$.ɵɵelementEnd();
}
}
// ...
function MyComponent_div_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelement(0, "div", $_c3$);
} if (rf & 2) {
const $ctx_0$ = i0.ɵɵnextContext();
$r3$.ɵɵselect(0);
$r3$.ɵɵproperty("id", $ctx_0$);
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should support ngFor context variables', () => { it('should support ngFor context variables', () => {
const files = { const files = {
app: { app: {

View File

@ -13,7 +13,10 @@ import {ParseSourceSpan} from '../parse_util';
export class EventHandlerVars { static event = o.variable('$event'); } export class EventHandlerVars { static event = o.variable('$event'); }
export interface LocalResolver { getLocal(name: string): o.Expression|null; } export interface LocalResolver {
getLocal(name: string): o.Expression|null;
notifyImplicitReceiverUse(): void;
}
export class ConvertActionBindingResult { export class ConvertActionBindingResult {
/** /**
@ -99,6 +102,11 @@ export function convertActionBinding(
const actionStmts: o.Statement[] = []; const actionStmts: o.Statement[] = [];
flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts); flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts);
prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts); prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts);
if (visitor.usesImplicitReceiver) {
localResolver.notifyImplicitReceiverUse();
}
const lastIndex = actionStmts.length - 1; const lastIndex = actionStmts.length - 1;
let preventDefaultVar: o.ReadVarExpr = null !; let preventDefaultVar: o.ReadVarExpr = null !;
if (lastIndex >= 0) { if (lastIndex >= 0) {
@ -160,6 +168,10 @@ export function convertPropertyBinding(
const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression); const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
const stmts: o.Statement[] = getStatementsFromVisitor(visitor, bindingId); const stmts: o.Statement[] = getStatementsFromVisitor(visitor, bindingId);
if (visitor.usesImplicitReceiver) {
localResolver.notifyImplicitReceiverUse();
}
if (visitor.temporaryCount === 0 && form == BindingForm.TrySimple) { if (visitor.temporaryCount === 0 && form == BindingForm.TrySimple) {
return new ConvertPropertyBindingResult([], outputExpr); return new ConvertPropertyBindingResult([], outputExpr);
} }
@ -192,6 +204,10 @@ export function convertUpdateArguments(
const outputExpr: o.InvokeFunctionExpr = const outputExpr: o.InvokeFunctionExpr =
expressionWithArgumentsToExtract.visit(visitor, _Mode.Expression); expressionWithArgumentsToExtract.visit(visitor, _Mode.Expression);
if (visitor.usesImplicitReceiver) {
localResolver.notifyImplicitReceiverUse();
}
const stmts = getStatementsFromVisitor(visitor, bindingId); const stmts = getStatementsFromVisitor(visitor, bindingId);
// Removing the first argument, because it was a length for ViewEngine, not Ivy. // Removing the first argument, because it was a length for ViewEngine, not Ivy.
@ -290,6 +306,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
private _resultMap = new Map<cdAst.AST, o.Expression>(); private _resultMap = new Map<cdAst.AST, o.Expression>();
private _currentTemporary: number = 0; private _currentTemporary: number = 0;
public temporaryCount: number = 0; public temporaryCount: number = 0;
public usesImplicitReceiver: boolean = false;
constructor( constructor(
private _localResolver: LocalResolver, private _implicitReceiver: o.Expression, private _localResolver: LocalResolver, private _implicitReceiver: o.Expression,
@ -387,6 +404,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any { visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any {
ensureExpressionMode(mode, ast); ensureExpressionMode(mode, ast);
this.usesImplicitReceiver = true;
return this._implicitReceiver; return this._implicitReceiver;
} }
@ -462,11 +480,15 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
return this.convertSafeAccess(ast, leftMostSafe, mode); return this.convertSafeAccess(ast, leftMostSafe, mode);
} else { } else {
const args = this.visitAll(ast.args, _Mode.Expression); const args = this.visitAll(ast.args, _Mode.Expression);
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
let result: any = null; let result: any = null;
const receiver = this._visit(ast.receiver, _Mode.Expression); const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) { if (receiver === this._implicitReceiver) {
const varExpr = this._getLocal(ast.name); const varExpr = this._getLocal(ast.name);
if (varExpr) { if (varExpr) {
// Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver;
result = varExpr.callFn(args); result = varExpr.callFn(args);
} }
} }
@ -492,9 +514,15 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
return this.convertSafeAccess(ast, leftMostSafe, mode); return this.convertSafeAccess(ast, leftMostSafe, mode);
} else { } else {
let result: any = null; let result: any = null;
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
const receiver = this._visit(ast.receiver, _Mode.Expression); const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) { if (receiver === this._implicitReceiver) {
result = this._getLocal(ast.name); result = this._getLocal(ast.name);
if (result) {
// Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver;
}
} }
if (result == null) { if (result == null) {
result = receiver.prop(ast.name); result = receiver.prop(ast.name);
@ -505,6 +533,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any { visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any {
const receiver: o.Expression = this._visit(ast.receiver, _Mode.Expression); const receiver: o.Expression = this._visit(ast.receiver, _Mode.Expression);
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
let varExpr: o.ReadPropExpr|null = null; let varExpr: o.ReadPropExpr|null = null;
if (receiver === this._implicitReceiver) { if (receiver === this._implicitReceiver) {
@ -515,6 +544,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
// to a 'context.property' value and will be used as the target of the // to a 'context.property' value and will be used as the target of the
// write expression. // write expression.
varExpr = localExpr; varExpr = localExpr;
// Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver;
} else { } else {
// Otherwise it's an error. // Otherwise it's an error.
throw new Error('Cannot assign to a reference or variable!'); throw new Error('Cannot assign to a reference or variable!');
@ -753,6 +785,7 @@ function flattenStatements(arg: any, output: o.Statement[]) {
} }
class DefaultLocalResolver implements LocalResolver { class DefaultLocalResolver implements LocalResolver {
notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null { getLocal(name: string): o.Expression|null {
if (name === EventHandlerVars.event.name) { if (name === EventHandlerVars.event.name) {
return EventHandlerVars.event; return EventHandlerVars.event;

View File

@ -632,7 +632,7 @@ function createHostBindingsFunction(
const eventBindings = const eventBindings =
bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan); bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan);
if (eventBindings && eventBindings.length) { if (eventBindings && eventBindings.length) {
const listeners = createHostListeners(bindingContext, eventBindings, name); const listeners = createHostListeners(eventBindings, name);
createStatements.push(...listeners); createStatements.push(...listeners);
} }
@ -781,16 +781,14 @@ function getBindingNameAndInstruction(binding: ParsedProperty):
return {bindingName, instruction, isAttribute: !!attrMatches}; return {bindingName, instruction, isAttribute: !!attrMatches};
} }
function createHostListeners( function createHostListeners(eventBindings: ParsedEvent[], name?: string): o.Statement[] {
bindingContext: o.Expression, eventBindings: ParsedEvent[], name?: string): o.Statement[] {
return eventBindings.map(binding => { return eventBindings.map(binding => {
let bindingName = binding.name && sanitizeIdentifier(binding.name); let bindingName = binding.name && sanitizeIdentifier(binding.name);
const bindingFnName = binding.type === ParsedEventType.Animation ? const bindingFnName = binding.type === ParsedEventType.Animation ?
prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) : prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) :
bindingName; bindingName;
const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null; const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null;
const params = prepareEventListenerParameters( const params = prepareEventListenerParameters(BoundEvent.fromParsedEvent(binding), handlerName);
BoundEvent.fromParsedEvent(binding), bindingContext, handlerName);
const instruction = const instruction =
binding.type == ParsedEventType.Animation ? R3.componentHostSyntheticListener : R3.listener; binding.type == ParsedEventType.Animation ? R3.componentHostSyntheticListener : R3.listener;
return o.importExpr(instruction).callFn(params).toStmt(); return o.importExpr(instruction).callFn(params).toStmt();

View File

@ -59,7 +59,7 @@ export function renderFlagCheckIfStmt(
} }
export function prepareEventListenerParameters( export function prepareEventListenerParameters(
eventAst: t.BoundEvent, bindingContext: o.Expression, handlerName: string | null = null, eventAst: t.BoundEvent, handlerName: string | null = null,
scope: BindingScope | null = null): o.Expression[] { scope: BindingScope | null = null): o.Expression[] {
const {type, name, target, phase, handler} = eventAst; const {type, name, target, phase, handler} = eventAst;
if (target && !GLOBAL_TARGET_RESOLVERS.has(target)) { if (target && !GLOBAL_TARGET_RESOLVERS.has(target)) {
@ -67,8 +67,11 @@ export function prepareEventListenerParameters(
Supported list of global targets: ${Array.from(GLOBAL_TARGET_RESOLVERS.keys())}.`); Supported list of global targets: ${Array.from(GLOBAL_TARGET_RESOLVERS.keys())}.`);
} }
const implicitReceiverExpr = (scope === null || scope.bindingLevel === 0) ?
o.variable(CONTEXT_NAME) :
scope.getOrCreateSharedContextVar(0);
const bindingExpr = convertActionBinding( const bindingExpr = convertActionBinding(
scope, bindingContext, handler, 'b', () => error('Unexpected interpolation'), scope, implicitReceiverExpr, handler, 'b', () => error('Unexpected interpolation'),
eventAst.handlerSpan); eventAst.handlerSpan);
const statements = []; const statements = [];
@ -152,6 +155,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// track it to properly adjust projection slot index in the `projection` instruction. // track it to properly adjust projection slot index in the `projection` instruction.
private _ngContentSelectorsOffset = 0; private _ngContentSelectorsOffset = 0;
// Expression that should be used as implicit receiver when converting template
// expressions to output AST.
private _implicitReceiverExpr: o.ReadVarExpr|null = null;
constructor( constructor(
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0, private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
private contextName: string|null, private i18nContext: I18nContext|null, private contextName: string|null, private i18nContext: I18nContext|null,
@ -306,6 +313,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// LocalResolver // LocalResolver
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); } getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
// LocalResolver
notifyImplicitReceiverUse(): void { this._bindingScope.notifyImplicitReceiverUse(); }
i18nTranslate( i18nTranslate(
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr, message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr { transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
@ -448,8 +458,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (bindings.size) { if (bindings.size) {
bindings.forEach(binding => { bindings.forEach(binding => {
this.updateInstruction( this.updateInstruction(
index, span, R3.i18nExp, index, span, R3.i18nExp, () => [this.convertPropertyBinding(binding)]);
() => [this.convertPropertyBinding(o.variable(CONTEXT_NAME), binding)]);
}); });
this.updateInstruction(index, span, R3.i18nApply, [o.literal(index)]); this.updateInstruction(index, span, R3.i18nApply, [o.literal(index)]);
} }
@ -598,8 +607,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.addNamespaceInstruction(currentNamespace, element); this.addNamespaceInstruction(currentNamespace, element);
} }
const implicit = o.variable(CONTEXT_NAME);
if (this.i18n) { if (this.i18n) {
this.i18n.appendElement(element.i18n !, elementIndex); this.i18n.appendElement(element.i18n !, elementIndex);
} }
@ -649,7 +656,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
hasBindings = true; hasBindings = true;
this.updateInstruction( this.updateInstruction(
elementIndex, element.sourceSpan, R3.i18nExp, elementIndex, element.sourceSpan, R3.i18nExp,
() => [this.convertExpressionBinding(implicit, expression)]); () => [this.convertExpressionBinding(expression)]);
}); });
} }
} }
@ -671,7 +678,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// (things like `styleProp`, `classProp`, etc..) are applied later on in this // (things like `styleProp`, `classProp`, etc..) are applied later on in this
// file // file
this.processStylingInstruction( this.processStylingInstruction(
elementIndex, implicit, elementIndex,
stylingBuilder.buildStylingInstruction(element.sourceSpan, this.constantPool), true); stylingBuilder.buildStylingInstruction(element.sourceSpan, this.constantPool), true);
// Generate Listeners (outputs) // Generate Listeners (outputs)
@ -697,7 +704,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
for (let i = 0; i <= limit; i++) { for (let i = 0; i <= limit; i++) {
const instruction = stylingInstructions[i]; const instruction = stylingInstructions[i];
this._bindingSlots += instruction.allocateBindingSlots; this._bindingSlots += instruction.allocateBindingSlots;
this.processStylingInstruction(elementIndex, implicit, instruction, false); this.processStylingInstruction(elementIndex, instruction, false);
} }
// the reason why `undefined` is used is because the renderer understands this as a // the reason why `undefined` is used is because the renderer understands this as a
@ -726,7 +733,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.updateInstruction(elementIndex, input.sourceSpan, R3.property, () => { this.updateInstruction(elementIndex, input.sourceSpan, R3.property, () => {
return [ return [
o.literal(bindingName), o.literal(bindingName),
(hasValue ? this.convertPropertyBinding(implicit, value, /* skipBindFn */ true) : (hasValue ? this.convertPropertyBinding(value, /* skipBindFn */ true) :
emptyValueBindInstruction), emptyValueBindInstruction),
]; ];
}); });
@ -764,7 +771,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} else { } else {
// [prop]="value" // [prop]="value"
this.boundUpdateInstruction( this.boundUpdateInstruction(
R3.property, elementIndex, attrName, input, implicit, value, params); R3.property, elementIndex, attrName, input, value, params);
} }
} else if (inputType === BindingType.Attribute) { } else if (inputType === BindingType.Attribute) {
if (value instanceof Interpolation && getInterpolationArgsLength(value) > 1) { if (value instanceof Interpolation && getInterpolationArgsLength(value) > 1) {
@ -776,14 +783,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const boundValue = value instanceof Interpolation ? value.expressions[0] : value; const boundValue = value instanceof Interpolation ? value.expressions[0] : value;
// [attr.name]="value" or attr.name="{{value}}" // [attr.name]="value" or attr.name="{{value}}"
this.boundUpdateInstruction( this.boundUpdateInstruction(
R3.attribute, elementIndex, attrName, input, implicit, boundValue, params); R3.attribute, elementIndex, attrName, input, boundValue, params);
} }
} else { } else {
// class prop // class prop
this.updateInstruction(elementIndex, input.sourceSpan, R3.classProp, () => { this.updateInstruction(elementIndex, input.sourceSpan, R3.classProp, () => {
return [ return [
o.literal(elementIndex), o.literal(attrName), o.literal(elementIndex), o.literal(attrName), this.convertPropertyBinding(value),
this.convertPropertyBinding(implicit, value), ...params ...params
]; ];
}); });
} }
@ -817,9 +824,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
*/ */
boundUpdateInstruction( boundUpdateInstruction(
instruction: o.ExternalReference, elementIndex: number, attrName: string, instruction: o.ExternalReference, elementIndex: number, attrName: string,
input: t.BoundAttribute, implicit: o.ReadVarExpr, value: any, params: any[]) { input: t.BoundAttribute, value: any, params: any[]) {
this.updateInstruction(elementIndex, input.sourceSpan, instruction, () => { this.updateInstruction(elementIndex, input.sourceSpan, instruction, () => {
return [o.literal(attrName), this.convertPropertyBinding(implicit, value, true), ...params]; return [o.literal(attrName), this.convertPropertyBinding(value, true), ...params];
}); });
} }
@ -832,9 +839,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
input: t.BoundAttribute, value: any, params: any[]) { input: t.BoundAttribute, value: any, params: any[]) {
this.updateInstruction( this.updateInstruction(
elementIndex, input.sourceSpan, instruction, elementIndex, input.sourceSpan, instruction,
() => () => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]);
[o.literal(attrName),
...this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value), ...params]);
} }
visitTemplate(template: t.Template) { visitTemplate(template: t.Template) {
@ -904,13 +909,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}); });
// handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al; // handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al;
const context = o.variable(CONTEXT_NAME); this.templatePropertyBindings(template, templateIndex, template.templateAttrs);
this.templatePropertyBindings(template, templateIndex, context, template.templateAttrs);
// Only add normal input/output binding instructions on explicit ng-template elements. // Only add normal input/output binding instructions on explicit ng-template elements.
if (template.tagName === NG_TEMPLATE_TAG_NAME) { if (template.tagName === NG_TEMPLATE_TAG_NAME) {
// Add the input bindings // Add the input bindings
this.templatePropertyBindings(template, templateIndex, context, template.inputs); this.templatePropertyBindings(template, templateIndex, template.inputs);
// Generate listeners for directive output // Generate listeners for directive output
template.outputs.forEach((outputAst: t.BoundEvent) => { template.outputs.forEach((outputAst: t.BoundEvent) => {
this.creationInstruction( this.creationInstruction(
@ -948,12 +952,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (value instanceof Interpolation) { if (value instanceof Interpolation) {
this.updateInstruction( this.updateInstruction(
nodeIndex, text.sourceSpan, getTextInterpolationExpression(value), nodeIndex, text.sourceSpan, getTextInterpolationExpression(value),
() => this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value)); () => this.getUpdateInstructionArguments(value));
} else { } else {
this.updateInstruction( this.updateInstruction(
nodeIndex, text.sourceSpan, R3.textBinding, nodeIndex, text.sourceSpan, R3.textBinding,
() => () => [o.literal(nodeIndex), this.convertPropertyBinding(value)]);
[o.literal(nodeIndex), this.convertPropertyBinding(o.variable(CONTEXT_NAME), value)]);
} }
} }
@ -1019,8 +1022,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private bindingContext() { return `${this._bindingContext++}`; } private bindingContext() { return `${this._bindingContext++}`; }
private templatePropertyBindings( private templatePropertyBindings(
template: t.Template, templateIndex: number, context: o.ReadVarExpr, template: t.Template, templateIndex: number, attrs: (t.BoundAttribute|t.TextAttribute)[]) {
attrs: (t.BoundAttribute|t.TextAttribute)[]) {
attrs.forEach(input => { attrs.forEach(input => {
if (input instanceof t.BoundAttribute) { if (input instanceof t.BoundAttribute) {
const value = input.value.visit(this._valueConverter); const value = input.value.visit(this._valueConverter);
@ -1029,7 +1031,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.allocateBindingSlots(value); this.allocateBindingSlots(value);
this.updateInstruction( this.updateInstruction(
templateIndex, template.sourceSpan, R3.property, templateIndex, template.sourceSpan, R3.property,
() => [o.literal(input.name), this.convertPropertyBinding(context, value, true)]); () => [o.literal(input.name), this.convertPropertyBinding(value, true)]);
} }
} }
}); });
@ -1049,10 +1051,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
private processStylingInstruction( private processStylingInstruction(
elementIndex: number, implicit: any, instruction: Instruction|null, createMode: boolean) { elementIndex: number, instruction: Instruction|null, createMode: boolean) {
if (instruction) { if (instruction) {
const paramsFn = () => const paramsFn = () =>
instruction.buildParams(value => this.convertPropertyBinding(implicit, value, true)); instruction.buildParams(value => this.convertPropertyBinding(value, true));
if (createMode) { if (createMode) {
this.creationInstruction(instruction.sourceSpan, instruction.reference, paramsFn); this.creationInstruction(instruction.sourceSpan, instruction.reference, paramsFn);
} else { } else {
@ -1090,23 +1092,38 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._bindingSlots += value instanceof Interpolation ? value.expressions.length : 1; this._bindingSlots += value instanceof Interpolation ? value.expressions.length : 1;
} }
private convertExpressionBinding(implicit: o.Expression, value: AST): o.Expression { /**
const convertedPropertyBinding = * Gets an expression that refers to the implicit receiver. The implicit
convertPropertyBinding(this, implicit, value, this.bindingContext(), BindingForm.TrySimple); * receiver is always the root level context.
*/
private getImplicitReceiverExpr(): o.ReadVarExpr {
if (this._implicitReceiverExpr) {
return this._implicitReceiverExpr;
}
return this._implicitReceiverExpr = this.level === 0 ?
o.variable(CONTEXT_NAME) :
this._bindingScope.getOrCreateSharedContextVar(0);
}
private convertExpressionBinding(value: AST): o.Expression {
const convertedPropertyBinding = convertPropertyBinding(
this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.TrySimple);
const valExpr = convertedPropertyBinding.currValExpr; const valExpr = convertedPropertyBinding.currValExpr;
return o.importExpr(R3.bind).callFn([valExpr]); return o.importExpr(R3.bind).callFn([valExpr]);
} }
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean): private convertPropertyBinding(value: AST, skipBindFn?: boolean): o.Expression {
o.Expression {
const interpolationFn = const interpolationFn =
value instanceof Interpolation ? interpolate : () => error('Unexpected interpolation'); value instanceof Interpolation ? interpolate : () => error('Unexpected interpolation');
const convertedPropertyBinding = convertPropertyBinding( const convertedPropertyBinding = convertPropertyBinding(
this, implicit, value, this.bindingContext(), BindingForm.TrySimple, interpolationFn); this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.TrySimple,
interpolationFn);
const valExpr = convertedPropertyBinding.currValExpr;
this._tempVariables.push(...convertedPropertyBinding.stmts); this._tempVariables.push(...convertedPropertyBinding.stmts);
const valExpr = convertedPropertyBinding.currValExpr;
return value instanceof Interpolation || skipBindFn ? valExpr : return value instanceof Interpolation || skipBindFn ? valExpr :
o.importExpr(R3.bind).callFn([valExpr]); o.importExpr(R3.bind).callFn([valExpr]);
} }
@ -1115,13 +1132,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
* Gets a list of argument expressions to pass to an update instruction expression. Also updates * Gets a list of argument expressions to pass to an update instruction expression. Also updates
* the temp variables state with temp variables that were identified as needing to be created * the temp variables state with temp variables that were identified as needing to be created
* while visiting the arguments. * while visiting the arguments.
* @param contextExpression The expression for the context variable used to create arguments
* @param value The original expression we will be resolving an arguments list from. * @param value The original expression we will be resolving an arguments list from.
*/ */
private getUpdateInstructionArguments(contextExpression: o.Expression, value: AST): private getUpdateInstructionArguments(value: AST): o.Expression[] {
o.Expression[] {
const {args, stmts} = const {args, stmts} =
convertUpdateArguments(this, contextExpression, value, this.bindingContext()); convertUpdateArguments(this, this.getImplicitReceiverExpr(), value, this.bindingContext());
this._tempVariables.push(...stmts); this._tempVariables.push(...stmts);
return args; return args;
} }
@ -1265,8 +1281,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
sanitizeIdentifier(eventName); sanitizeIdentifier(eventName);
const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`; const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`;
const scope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel); const scope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
const context = o.variable(CONTEXT_NAME); return prepareEventListenerParameters(outputAst, handlerName, scope);
return prepareEventListenerParameters(outputAst, context, handlerName, scope);
}; };
} }
} }
@ -1544,14 +1559,38 @@ export class BindingScope implements LocalResolver {
return this; return this;
} }
// Implemented as part of LocalResolver.
getLocal(name: string): (o.Expression|null) { return this.get(name); } getLocal(name: string): (o.Expression|null) { return this.get(name); }
// Implemented as part of LocalResolver.
notifyImplicitReceiverUse(): void {
if (this.bindingLevel !== 0) {
// Since the implicit receiver is accessed in an embedded view, we need to
// ensure that we declare a shared context variable for the current template
// in the update variables.
this.map.get(SHARED_CONTEXT_KEY + 0) !.declare = true;
}
}
nestedScope(level: number): BindingScope { nestedScope(level: number): BindingScope {
const newScope = new BindingScope(level, this); const newScope = new BindingScope(level, this);
if (level > 0) newScope.generateSharedContextVar(0); if (level > 0) newScope.generateSharedContextVar(0);
return newScope; return newScope;
} }
/**
* Gets or creates a shared context variable and returns its expression. Note that
* this does not mean that the shared variable will be declared. Variables in the
* binding scope will be only declared if they are used.
*/
getOrCreateSharedContextVar(retrievalLevel: number): o.ReadVarExpr {
const bindingKey = SHARED_CONTEXT_KEY + retrievalLevel;
if (!this.map.has(bindingKey)) {
this.generateSharedContextVar(retrievalLevel);
}
return this.map.get(bindingKey) !.lhs;
}
getSharedContextName(retrievalLevel: number): o.ReadVarExpr|null { getSharedContextName(retrievalLevel: number): o.ReadVarExpr|null {
const sharedCtxObj = this.map.get(SHARED_CONTEXT_KEY + retrievalLevel); const sharedCtxObj = this.map.get(SHARED_CONTEXT_KEY + retrievalLevel);
return sharedCtxObj && sharedCtxObj.declare ? sharedCtxObj.lhs : null; return sharedCtxObj && sharedCtxObj.declare ? sharedCtxObj.lhs : null;

View File

@ -79,6 +79,7 @@ interface Expression {
const DYNAMIC_VAR_NAME = '_any'; const DYNAMIC_VAR_NAME = '_any';
class TypeCheckLocalResolver implements LocalResolver { class TypeCheckLocalResolver implements LocalResolver {
notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null { getLocal(name: string): o.Expression|null {
if (name === EventHandlerVars.event.name) { if (name === EventHandlerVars.event.name) {
// References to the event should not be type-checked. // References to the event should not be type-checked.
@ -284,6 +285,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
} }
} }
notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null { getLocal(name: string): o.Expression|null {
if (name == EventHandlerVars.event.name) { if (name == EventHandlerVars.event.name) {
return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic)); return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic));

View File

@ -687,6 +687,12 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
return null; return null;
} }
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): private _createLiteralArrayConverter(sourceSpan: ParseSourceSpan, argCount: number):
BuiltinConverter { BuiltinConverter {
if (argCount === 0) { if (argCount === 0) {

View File

@ -0,0 +1,75 @@
/**
* @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 {Component, Input} from '@angular/core';
import {TestBed} from '@angular/core/testing';
describe('embedded views', () => {
it('should correctly resolve the implicit receiver in expressions', () => {
const items: string[] = [];
@Component({
selector: 'child-cmp',
template: 'Child',
})
class ChildCmp {
@Input() addItemFn: Function|undefined;
}
@Component({
template: `<child-cmp *ngIf="true" [addItemFn]="addItem.bind(this)"></child-cmp>`,
})
class TestCmp {
item: string = 'CmpItem';
addItem() { items.push(this.item); }
}
TestBed.configureTestingModule({declarations: [ChildCmp, TestCmp]});
const fixture = TestBed.createComponent(TestCmp);
fixture.detectChanges();
const childCmp: ChildCmp = fixture.debugElement.children[0].componentInstance;
childCmp.addItemFn !();
childCmp.addItemFn !();
expect(items).toEqual(['CmpItem', 'CmpItem']);
});
it('should resolve template input variables through the implicit receiver', () => {
@Component({template: `<ng-template let-a [ngIf]="true">{{this.a}}</ng-template>`})
class TestCmp {
}
TestBed.configureTestingModule({declarations: [TestCmp]});
const fixture = TestBed.createComponent(TestCmp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('true');
});
it('should component instance variables through the implicit receiver', () => {
@Component({
template: `
<ng-template [ngIf]="true">
<ng-template [ngIf]="true">{{this.myProp}}{{myProp}}</ng-template>
</ng-template>`
})
class TestCmp {
myProp = 'Hello';
}
TestBed.configureTestingModule({declarations: [TestCmp]});
const fixture = TestBed.createComponent(TestCmp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('HelloHello');
});
});