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');
});
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', () => {
const files = {
app: {

View File

@ -13,7 +13,10 @@ import {ParseSourceSpan} from '../parse_util';
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 {
/**
@ -99,6 +102,11 @@ export function convertActionBinding(
const actionStmts: o.Statement[] = [];
flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts);
prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts);
if (visitor.usesImplicitReceiver) {
localResolver.notifyImplicitReceiverUse();
}
const lastIndex = actionStmts.length - 1;
let preventDefaultVar: o.ReadVarExpr = null !;
if (lastIndex >= 0) {
@ -160,6 +168,10 @@ export function convertPropertyBinding(
const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
const stmts: o.Statement[] = getStatementsFromVisitor(visitor, bindingId);
if (visitor.usesImplicitReceiver) {
localResolver.notifyImplicitReceiverUse();
}
if (visitor.temporaryCount === 0 && form == BindingForm.TrySimple) {
return new ConvertPropertyBindingResult([], outputExpr);
}
@ -192,6 +204,10 @@ export function convertUpdateArguments(
const outputExpr: o.InvokeFunctionExpr =
expressionWithArgumentsToExtract.visit(visitor, _Mode.Expression);
if (visitor.usesImplicitReceiver) {
localResolver.notifyImplicitReceiverUse();
}
const stmts = getStatementsFromVisitor(visitor, bindingId);
// 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 _currentTemporary: number = 0;
public temporaryCount: number = 0;
public usesImplicitReceiver: boolean = false;
constructor(
private _localResolver: LocalResolver, private _implicitReceiver: o.Expression,
@ -387,6 +404,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any {
ensureExpressionMode(mode, ast);
this.usesImplicitReceiver = true;
return this._implicitReceiver;
}
@ -462,11 +480,15 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
return this.convertSafeAccess(ast, leftMostSafe, mode);
} else {
const args = this.visitAll(ast.args, _Mode.Expression);
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
let result: any = null;
const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
const varExpr = this._getLocal(ast.name);
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);
}
}
@ -492,9 +514,15 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
return this.convertSafeAccess(ast, leftMostSafe, mode);
} else {
let result: any = null;
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
const receiver = this._visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
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) {
result = receiver.prop(ast.name);
@ -505,6 +533,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any {
const receiver: o.Expression = this._visit(ast.receiver, _Mode.Expression);
const prevUsesImplicitReceiver = this.usesImplicitReceiver;
let varExpr: o.ReadPropExpr|null = null;
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
// write expression.
varExpr = localExpr;
// Restore the previous "usesImplicitReceiver" state since the implicit
// receiver has been replaced with a resolved local expression.
this.usesImplicitReceiver = prevUsesImplicitReceiver;
} else {
// Otherwise it's an error.
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 {
notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null {
if (name === EventHandlerVars.event.name) {
return EventHandlerVars.event;

View File

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

View File

@ -59,7 +59,7 @@ export function renderFlagCheckIfStmt(
}
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[] {
const {type, name, target, phase, handler} = eventAst;
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())}.`);
}
const implicitReceiverExpr = (scope === null || scope.bindingLevel === 0) ?
o.variable(CONTEXT_NAME) :
scope.getOrCreateSharedContextVar(0);
const bindingExpr = convertActionBinding(
scope, bindingContext, handler, 'b', () => error('Unexpected interpolation'),
scope, implicitReceiverExpr, handler, 'b', () => error('Unexpected interpolation'),
eventAst.handlerSpan);
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.
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(
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
private contextName: string|null, private i18nContext: I18nContext|null,
@ -306,6 +313,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// LocalResolver
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
// LocalResolver
notifyImplicitReceiverUse(): void { this._bindingScope.notifyImplicitReceiverUse(); }
i18nTranslate(
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: 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) {
bindings.forEach(binding => {
this.updateInstruction(
index, span, R3.i18nExp,
() => [this.convertPropertyBinding(o.variable(CONTEXT_NAME), binding)]);
index, span, R3.i18nExp, () => [this.convertPropertyBinding(binding)]);
});
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);
}
const implicit = o.variable(CONTEXT_NAME);
if (this.i18n) {
this.i18n.appendElement(element.i18n !, elementIndex);
}
@ -649,7 +656,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
hasBindings = true;
this.updateInstruction(
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
// file
this.processStylingInstruction(
elementIndex, implicit,
elementIndex,
stylingBuilder.buildStylingInstruction(element.sourceSpan, this.constantPool), true);
// Generate Listeners (outputs)
@ -697,7 +704,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
for (let i = 0; i <= limit; i++) {
const instruction = stylingInstructions[i];
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
@ -726,7 +733,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.updateInstruction(elementIndex, input.sourceSpan, R3.property, () => {
return [
o.literal(bindingName),
(hasValue ? this.convertPropertyBinding(implicit, value, /* skipBindFn */ true) :
(hasValue ? this.convertPropertyBinding(value, /* skipBindFn */ true) :
emptyValueBindInstruction),
];
});
@ -764,7 +771,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} else {
// [prop]="value"
this.boundUpdateInstruction(
R3.property, elementIndex, attrName, input, implicit, value, params);
R3.property, elementIndex, attrName, input, value, params);
}
} else if (inputType === BindingType.Attribute) {
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;
// [attr.name]="value" or attr.name="{{value}}"
this.boundUpdateInstruction(
R3.attribute, elementIndex, attrName, input, implicit, boundValue, params);
R3.attribute, elementIndex, attrName, input, boundValue, params);
}
} else {
// class prop
this.updateInstruction(elementIndex, input.sourceSpan, R3.classProp, () => {
return [
o.literal(elementIndex), o.literal(attrName),
this.convertPropertyBinding(implicit, value), ...params
o.literal(elementIndex), o.literal(attrName), this.convertPropertyBinding(value),
...params
];
});
}
@ -817,9 +824,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
*/
boundUpdateInstruction(
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, () => {
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[]) {
this.updateInstruction(
elementIndex, input.sourceSpan, instruction,
() =>
[o.literal(attrName),
...this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value), ...params]);
() => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]);
}
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;
const context = o.variable(CONTEXT_NAME);
this.templatePropertyBindings(template, templateIndex, context, template.templateAttrs);
this.templatePropertyBindings(template, templateIndex, template.templateAttrs);
// Only add normal input/output binding instructions on explicit ng-template elements.
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
// Add the input bindings
this.templatePropertyBindings(template, templateIndex, context, template.inputs);
this.templatePropertyBindings(template, templateIndex, template.inputs);
// Generate listeners for directive output
template.outputs.forEach((outputAst: t.BoundEvent) => {
this.creationInstruction(
@ -948,12 +952,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (value instanceof Interpolation) {
this.updateInstruction(
nodeIndex, text.sourceSpan, getTextInterpolationExpression(value),
() => this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value));
() => this.getUpdateInstructionArguments(value));
} else {
this.updateInstruction(
nodeIndex, text.sourceSpan, R3.textBinding,
() =>
[o.literal(nodeIndex), this.convertPropertyBinding(o.variable(CONTEXT_NAME), value)]);
() => [o.literal(nodeIndex), this.convertPropertyBinding(value)]);
}
}
@ -1019,8 +1022,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private bindingContext() { return `${this._bindingContext++}`; }
private templatePropertyBindings(
template: t.Template, templateIndex: number, context: o.ReadVarExpr,
attrs: (t.BoundAttribute|t.TextAttribute)[]) {
template: t.Template, templateIndex: number, attrs: (t.BoundAttribute|t.TextAttribute)[]) {
attrs.forEach(input => {
if (input instanceof t.BoundAttribute) {
const value = input.value.visit(this._valueConverter);
@ -1029,7 +1031,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.allocateBindingSlots(value);
this.updateInstruction(
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(
elementIndex: number, implicit: any, instruction: Instruction|null, createMode: boolean) {
elementIndex: number, instruction: Instruction|null, createMode: boolean) {
if (instruction) {
const paramsFn = () =>
instruction.buildParams(value => this.convertPropertyBinding(implicit, value, true));
instruction.buildParams(value => this.convertPropertyBinding(value, true));
if (createMode) {
this.creationInstruction(instruction.sourceSpan, instruction.reference, paramsFn);
} else {
@ -1090,23 +1092,38 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._bindingSlots += value instanceof Interpolation ? value.expressions.length : 1;
}
private convertExpressionBinding(implicit: o.Expression, value: AST): o.Expression {
const convertedPropertyBinding =
convertPropertyBinding(this, implicit, value, this.bindingContext(), BindingForm.TrySimple);
/**
* Gets an expression that refers to the implicit receiver. The implicit
* 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;
return o.importExpr(R3.bind).callFn([valExpr]);
}
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean):
o.Expression {
private convertPropertyBinding(value: AST, skipBindFn?: boolean): o.Expression {
const interpolationFn =
value instanceof Interpolation ? interpolate : () => error('Unexpected interpolation');
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);
const valExpr = convertedPropertyBinding.currValExpr;
return value instanceof Interpolation || skipBindFn ? 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
* the temp variables state with temp variables that were identified as needing to be created
* 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.
*/
private getUpdateInstructionArguments(contextExpression: o.Expression, value: AST):
o.Expression[] {
private getUpdateInstructionArguments(value: AST): o.Expression[] {
const {args, stmts} =
convertUpdateArguments(this, contextExpression, value, this.bindingContext());
convertUpdateArguments(this, this.getImplicitReceiverExpr(), value, this.bindingContext());
this._tempVariables.push(...stmts);
return args;
}
@ -1265,8 +1281,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
sanitizeIdentifier(eventName);
const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`;
const scope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
const context = o.variable(CONTEXT_NAME);
return prepareEventListenerParameters(outputAst, context, handlerName, scope);
return prepareEventListenerParameters(outputAst, handlerName, scope);
};
}
}
@ -1544,14 +1559,38 @@ export class BindingScope implements LocalResolver {
return this;
}
// Implemented as part of LocalResolver.
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 {
const newScope = new BindingScope(level, this);
if (level > 0) newScope.generateSharedContextVar(0);
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 {
const sharedCtxObj = this.map.get(SHARED_CONTEXT_KEY + retrievalLevel);
return sharedCtxObj && sharedCtxObj.declare ? sharedCtxObj.lhs : null;

View File

@ -79,6 +79,7 @@ interface Expression {
const DYNAMIC_VAR_NAME = '_any';
class TypeCheckLocalResolver implements LocalResolver {
notifyImplicitReceiverUse(): void {}
getLocal(name: string): o.Expression|null {
if (name === EventHandlerVars.event.name) {
// 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 {
if (name == EventHandlerVars.event.name) {
return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic));

View File

@ -687,6 +687,12 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
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):
BuiltinConverter {
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');
});
});