diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js index 3125914021..b2ce2b31a0 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js @@ -584,3 +584,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE ****************************************************************************************************/ export {}; +/**************************************************************************************************** + * PARTIAL FILE: implicit_receiver_keyed_write_inside_template.js + ****************************************************************************************************/ +import { Component, NgModule } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { + constructor() { + this.message = ''; + } +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "my-component", ngImport: i0, template: ` + + + + `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` + + + + ` + }] + }] }); +export class MyModule { +} +MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyComponent] }); +MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ + type: NgModule, + args: [{ declarations: [MyComponent] }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: implicit_receiver_keyed_write_inside_template.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyComponent { + message: string; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class MyModule { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵinj: i0.ɵɵInjectorDeclaration; +} + diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/TEST_CASES.json index 7a46d6f143..992de375c8 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/TEST_CASES.json @@ -264,6 +264,23 @@ "failureMessage": "Incorrect event listener" } ] + }, + { + "description": "should generate the view restoration statements if a keyed write is used in an event listener from within an ng-template", + "inputFiles": [ + "implicit_receiver_keyed_write_inside_template.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "implicit_receiver_keyed_write_inside_template_template.js", + "generated": "implicit_receiver_keyed_write_inside_template.js" + } + ], + "failureMessage": "Incorrect template" + } + ] } ] } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/implicit_receiver_keyed_write_inside_template.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/implicit_receiver_keyed_write_inside_template.ts new file mode 100644 index 0000000000..63ff6a62cd --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/implicit_receiver_keyed_write_inside_template.ts @@ -0,0 +1,17 @@ +import {Component, NgModule} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` + + + + ` +}) +export class MyComponent { + message = ''; +} + +@NgModule({declarations: [MyComponent]}) +export class MyModule { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/implicit_receiver_keyed_write_inside_template_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/implicit_receiver_keyed_write_inside_template_template.js new file mode 100644 index 0000000000..bac6f51471 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/implicit_receiver_keyed_write_inside_template_template.js @@ -0,0 +1,13 @@ +function MyComponent_ng_template_0_Template(rf, $ctx$) { + if (rf & 1) { + const _r3 = $i0$.ɵɵgetCurrentView(); + $i0$.ɵɵelementStart(0, "button", 1); + $i0$.ɵɵlistener("click", function MyComponent_ng_template_0_Template_button_click_0_listener() { + $i0$.ɵɵrestoreView(_r3); + const $ctx_2$ = $i0$.ɵɵnextContext(); + return ($ctx_2$["mes" + "sage"] = "hello"); + }); + $i0$.ɵɵtext(1, "Click me"); + $i0$.ɵɵelementEnd(); + } +} diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 476651660f..cd0b50f4b4 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -19,6 +19,7 @@ export interface LocalResolver { getLocal(name: string): o.Expression|null; notifyImplicitReceiverUse(): void; globals?: Set; + maybeRestoreView(retrievalLevel: number, localRefLookup: boolean): void; } export class ConvertActionBindingResult { @@ -487,6 +488,11 @@ class _AstToIrVisitor implements cdAst.AstVisitor { const obj: o.Expression = this._visit(ast.receiver, _Mode.Expression); const key: o.Expression = this._visit(ast.key, _Mode.Expression); const value: o.Expression = this._visit(ast.value, _Mode.Expression); + + if (obj === this._implicitReceiver) { + this._localResolver.maybeRestoreView(0, false); + } + return convertToStatementIfNeeded(mode, obj.key(key).set(value)); } @@ -982,6 +988,7 @@ function flattenStatements(arg: any, output: o.Statement[]) { class DefaultLocalResolver implements LocalResolver { constructor(public globals?: Set) {} notifyImplicitReceiverUse(): void {} + maybeRestoreView(): void {} getLocal(name: string): o.Expression|null { if (name === EventHandlerVars.event.name) { return EventHandlerVars.event; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 686b93ceeb..e4ed819a7f 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -339,6 +339,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this._bindingScope.notifyImplicitReceiverUse(); } + // LocalResolver + maybeRestoreView(retrievalLevel: number, localRefLookup: boolean): void { + this._bindingScope.maybeRestoreView(retrievalLevel, localRefLookup); + } + private i18nTranslate( message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr, transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr { diff --git a/packages/compiler/src/view_compiler/type_check_compiler.ts b/packages/compiler/src/view_compiler/type_check_compiler.ts index 6eaa3f6552..9518b0a77d 100644 --- a/packages/compiler/src/view_compiler/type_check_compiler.ts +++ b/packages/compiler/src/view_compiler/type_check_compiler.ts @@ -78,6 +78,7 @@ const DYNAMIC_VAR_NAME = '_any'; class TypeCheckLocalResolver implements LocalResolver { notifyImplicitReceiverUse(): void {} + maybeRestoreView(): void {} getLocal(name: string): o.Expression|null { if (name === EventHandlerVars.event.name) { // References to the event should not be type-checked. @@ -290,6 +291,8 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { } notifyImplicitReceiverUse(): void {} + maybeRestoreView(): 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 b8c9f76c80..e8c4ef6ba9 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -680,11 +680,15 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { } notifyImplicitReceiverUse(): void { - // Not needed in View Engine as View Engine walks through the generated + // Not needed in ViewEngine as ViewEngine 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. } + maybeRestoreView(): void { + // Not necessary in ViewEngine, because view restoration is an Ivy concept. + } + private _createLiteralArrayConverter(sourceSpan: ParseSourceSpan, argCount: number): BuiltinConverter { if (argCount === 0) { diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts index 51bf82f4ff..118cbb84ce 100644 --- a/packages/core/test/acceptance/listener_spec.ts +++ b/packages/core/test/acceptance/listener_spec.ts @@ -528,4 +528,28 @@ describe('event listeners', () => { expect(eventVariable).toBe(10); expect(eventObject?.type).toBe('click'); }); + + it('should be able to use a keyed write on `this` from a listener inside an ng-template', () => { + @Component({ + template: ` + + + + + + ` + }) + class MyComp { + message = ''; + } + + TestBed.configureTestingModule({declarations: [MyComp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + const button = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.message).toBe('hello'); + }); });