fix(compiler): generate view restoration for keyed write inside template listener (#42603)

If an implcit receiver is accessed in a listener inside of an `ng-template`, we generate some extra code in order to ensure that we're assigning to the correct object. The problem is that the logic wasn't covering keyed writes which caused it to write to the wrong object and throw an assertion error at runtime.

These changes expand the logic to cover keyed writes.

Fixes #41267.

PR Close #42603
This commit is contained in:
Kristiyan Kostadinov 2021-06-19 10:59:00 +02:00 committed by Dylan Hunn
parent 8793d1a046
commit f52df99fe3
9 changed files with 143 additions and 1 deletions

View File

@ -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: `
<ng-template #template>
<button (click)="this['mes' + 'sage'] = 'hello'">Click me</button>
</ng-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: `
<ng-template #template>
<button (click)="this['mes' + 'sage'] = 'hello'">Click me</button>
</ng-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<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never>;
}
export declare class MyModule {
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof MyComponent], never, never>;
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}

View File

@ -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"
}
]
}
]
}

View File

@ -0,0 +1,17 @@
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<ng-template #template>
<button (click)="this['mes' + 'sage'] = 'hello'">Click me</button>
</ng-template>
`
})
export class MyComponent {
message = '';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {
}

View File

@ -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();
}
}

View File

@ -19,6 +19,7 @@ export interface LocalResolver {
getLocal(name: string): o.Expression|null;
notifyImplicitReceiverUse(): void;
globals?: Set<string>;
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<string>) {}
notifyImplicitReceiverUse(): void {}
maybeRestoreView(): void {}
getLocal(name: string): o.Expression|null {
if (name === EventHandlerVars.event.name) {
return EventHandlerVars.event;

View File

@ -339,6 +339,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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 {

View File

@ -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));

View File

@ -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) {

View File

@ -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: `
<ng-template #template>
<button (click)="this['mes' + 'sage'] = 'hello'">Click me</button>
</ng-template>
<ng-container [ngTemplateOutlet]="template"></ng-container>
`
})
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');
});
});