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'); + }); + +});