From a3e17190e7e7e0329ed3643299c24d5fd510b7d6 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 8 Jan 2021 14:29:45 +0200 Subject: [PATCH] fix(core): allow EmbeddedViewRef context to be updated (#40360) Currently `EmbeddedViewRef.context` is read-only which means that the only way to update it is to mutate the object which can lead to some undesirable outcomes if the template and the context are provided by an external consumer (see #24515). These changes make the property writeable since there doesn't appear to be a specific reason why it was readonly to begin with. PR Close #40360 --- goldens/public-api/core/core.d.ts | 2 +- packages/core/src/linker/view_ref.ts | 2 +- packages/core/src/render3/view_ref.ts | 4 + packages/core/src/view/refs.ts | 4 + .../core/test/acceptance/template_ref_spec.ts | 83 +++++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/goldens/public-api/core/core.d.ts b/goldens/public-api/core/core.d.ts index 56dfa25b8c..a9f381b639 100644 --- a/goldens/public-api/core/core.d.ts +++ b/goldens/public-api/core/core.d.ts @@ -301,7 +301,7 @@ export declare class ElementRef { } export declare abstract class EmbeddedViewRef extends ViewRef { - abstract get context(): C; + abstract context: C; abstract get rootNodes(): any[]; } diff --git a/packages/core/src/linker/view_ref.ts b/packages/core/src/linker/view_ref.ts index bafc35a488..315f86e6d7 100644 --- a/packages/core/src/linker/view_ref.ts +++ b/packages/core/src/linker/view_ref.ts @@ -93,7 +93,7 @@ export abstract class EmbeddedViewRef extends ViewRef { /** * The context for this view, inherited from the anchor element. */ - abstract get context(): C; + abstract context: C; /** * The root nodes for this embedded view. diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index b88e8b51ba..8f7fdc72fa 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -61,6 +61,10 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int return this._lView[CONTEXT] as T; } + set context(value: T) { + this._lView[CONTEXT] = value; + } + get destroyed(): boolean { return (this._lView[FLAGS] & LViewFlags.Destroyed) === LViewFlags.Destroyed; } diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 7d219ace1c..b724fcc800 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -264,6 +264,10 @@ export class ViewRef_ implements EmbeddedViewRef, InternalViewRef { return this._view.context; } + set context(value: any) { + this._view.context = value; + } + get destroyed(): boolean { return (this._view.state & ViewState.Destroyed) !== 0; } diff --git a/packages/core/test/acceptance/template_ref_spec.ts b/packages/core/test/acceptance/template_ref_spec.ts index ab14c0c765..84ed32d582 100644 --- a/packages/core/test/acceptance/template_ref_spec.ts +++ b/packages/core/test/acceptance/template_ref_spec.ts @@ -273,4 +273,87 @@ describe('TemplateRef', () => { }); }); }); + + describe('context', () => { + @Component({ + template: ` + {{name}} + + ` + }) + class App { + @ViewChild('templateRef') templateRef!: TemplateRef; + @ViewChild('containerRef', {read: ViewContainerRef}) containerRef!: ViewContainerRef; + } + + it('should update if the context of a view ref is mutated', () => { + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const context = {name: 'Frodo'}; + const viewRef = fixture.componentInstance.templateRef.createEmbeddedView(context); + fixture.componentInstance.containerRef.insert(viewRef); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Frodo'); + + context.name = 'Bilbo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Bilbo'); + }); + + it('should update if the context of a view ref is replaced', () => { + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const viewRef = fixture.componentInstance.templateRef.createEmbeddedView({name: 'Frodo'}); + fixture.componentInstance.containerRef.insert(viewRef); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Frodo'); + + viewRef.context = {name: 'Bilbo'}; + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Bilbo'); + }); + + it('should use the latest context information inside template listeners', () => { + const events: string[] = []; + + @Component({ + template: ` + + + + + ` + }) + class ListenerTest { + @ViewChild('templateRef') templateRef!: TemplateRef; + @ViewChild('containerRef', {read: ViewContainerRef}) containerRef!: ViewContainerRef; + + log(name: string) { + events.push(name); + } + } + + TestBed.configureTestingModule({declarations: [ListenerTest]}); + const fixture = TestBed.createComponent(ListenerTest); + fixture.detectChanges(); + const viewRef = fixture.componentInstance.templateRef.createEmbeddedView({name: 'Frodo'}); + fixture.componentInstance.containerRef.insert(viewRef); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector('button'); + button.click(); + expect(events).toEqual(['Frodo']); + + viewRef.context = {name: 'Bilbo'}; + fixture.detectChanges(); + button.click(); + expect(events).toEqual(['Frodo', 'Bilbo']); + }); + }); });