From 58be2ff88400d7dc876d357f8cc3eb01b0912aae Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 6 Jun 2019 20:01:51 +0200 Subject: [PATCH] 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 {{this.myProperty}} ``` 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 ``` 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 --- .../r3_view_compiler_template_spec.ts | 53 ++++++++ .../src/compiler_util/expression_converter.ts | 35 ++++- .../compiler/src/render3/view/compiler.ts | 8 +- .../compiler/src/render3/view/template.ts | 125 ++++++++++++------ .../src/view_compiler/type_check_compiler.ts | 2 + .../src/view_compiler/view_compiler.ts | 6 + .../test/acceptance/embedded_views_spec.ts | 75 +++++++++++ 7 files changed, 255 insertions(+), 49 deletions(-) create mode 100644 packages/core/test/acceptance/embedded_views_spec.ts diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts index 89d54b6cc5..7345a25952 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts @@ -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: \` +
+
+ \` + }) + 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: { diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 67cc3ceeb8..971aec3223 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -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(); 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; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 5d6378730d..9ec1f5a2b4 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -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(); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 61477223e5..7c219556d1 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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; diff --git a/packages/compiler/src/view_compiler/type_check_compiler.ts b/packages/compiler/src/view_compiler/type_check_compiler.ts index 8d5bfeb6a7..f617c47e51 100644 --- a/packages/compiler/src/view_compiler/type_check_compiler.ts +++ b/packages/compiler/src/view_compiler/type_check_compiler.ts @@ -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)); diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index e97e3291f6..8310f7e104 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -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) { diff --git a/packages/core/test/acceptance/embedded_views_spec.ts b/packages/core/test/acceptance/embedded_views_spec.ts new file mode 100644 index 0000000000..a4fd8ba31b --- /dev/null +++ b/packages/core/test/acceptance/embedded_views_spec.ts @@ -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: ``, + }) + 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: `{{this.a}}`}) + 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: ` + + {{this.myProp}}{{myProp}} + ` + }) + class TestCmp { + myProp = 'Hello'; + } + + TestBed.configureTestingModule({declarations: [TestCmp]}); + const fixture = TestBed.createComponent(TestCmp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('HelloHello'); + }); + +});