Skip to content

Commit a3e1719

Browse files
crisbetoatscott
authored andcommitted
fix(core): allow EmbeddedViewRef context to be updated (angular#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 angular#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 angular#40360
1 parent ddf7970 commit a3e1719

5 files changed

Lines changed: 93 additions & 2 deletions

File tree

goldens/public-api/core/core.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export declare class ElementRef<T = any> {
301301
}
302302

303303
export declare abstract class EmbeddedViewRef<C> extends ViewRef {
304-
abstract get context(): C;
304+
abstract context: C;
305305
abstract get rootNodes(): any[];
306306
}
307307

packages/core/src/linker/view_ref.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export abstract class EmbeddedViewRef<C> extends ViewRef {
9393
/**
9494
* The context for this view, inherited from the anchor element.
9595
*/
96-
abstract get context(): C;
96+
abstract context: C;
9797

9898
/**
9999
* The root nodes for this embedded view.

packages/core/src/render3/view_ref.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
6161
return this._lView[CONTEXT] as T;
6262
}
6363

64+
set context(value: T) {
65+
this._lView[CONTEXT] = value;
66+
}
67+
6468
get destroyed(): boolean {
6569
return (this._lView[FLAGS] & LViewFlags.Destroyed) === LViewFlags.Destroyed;
6670
}

packages/core/src/view/refs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ export class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
264264
return this._view.context;
265265
}
266266

267+
set context(value: any) {
268+
this._view.context = value;
269+
}
270+
267271
get destroyed(): boolean {
268272
return (this._view.state & ViewState.Destroyed) !== 0;
269273
}

packages/core/test/acceptance/template_ref_spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,87 @@ describe('TemplateRef', () => {
273273
});
274274
});
275275
});
276+
277+
describe('context', () => {
278+
@Component({
279+
template: `
280+
<ng-template #templateRef let-name="name">{{name}}</ng-template>
281+
<ng-container #containerRef></ng-container>
282+
`
283+
})
284+
class App {
285+
@ViewChild('templateRef') templateRef!: TemplateRef<any>;
286+
@ViewChild('containerRef', {read: ViewContainerRef}) containerRef!: ViewContainerRef;
287+
}
288+
289+
it('should update if the context of a view ref is mutated', () => {
290+
TestBed.configureTestingModule({declarations: [App]});
291+
const fixture = TestBed.createComponent(App);
292+
fixture.detectChanges();
293+
const context = {name: 'Frodo'};
294+
const viewRef = fixture.componentInstance.templateRef.createEmbeddedView(context);
295+
fixture.componentInstance.containerRef.insert(viewRef);
296+
fixture.detectChanges();
297+
298+
expect(fixture.nativeElement.textContent).toBe('Frodo');
299+
300+
context.name = 'Bilbo';
301+
fixture.detectChanges();
302+
303+
expect(fixture.nativeElement.textContent).toBe('Bilbo');
304+
});
305+
306+
it('should update if the context of a view ref is replaced', () => {
307+
TestBed.configureTestingModule({declarations: [App]});
308+
const fixture = TestBed.createComponent(App);
309+
fixture.detectChanges();
310+
const viewRef = fixture.componentInstance.templateRef.createEmbeddedView({name: 'Frodo'});
311+
fixture.componentInstance.containerRef.insert(viewRef);
312+
fixture.detectChanges();
313+
314+
expect(fixture.nativeElement.textContent).toBe('Frodo');
315+
316+
viewRef.context = {name: 'Bilbo'};
317+
fixture.detectChanges();
318+
319+
expect(fixture.nativeElement.textContent).toBe('Bilbo');
320+
});
321+
322+
it('should use the latest context information inside template listeners', () => {
323+
const events: string[] = [];
324+
325+
@Component({
326+
template: `
327+
<ng-template #templateRef let-name="name">
328+
<button (click)="log(name)"></button>
329+
</ng-template>
330+
<ng-container #containerRef></ng-container>
331+
`
332+
})
333+
class ListenerTest {
334+
@ViewChild('templateRef') templateRef!: TemplateRef<any>;
335+
@ViewChild('containerRef', {read: ViewContainerRef}) containerRef!: ViewContainerRef;
336+
337+
log(name: string) {
338+
events.push(name);
339+
}
340+
}
341+
342+
TestBed.configureTestingModule({declarations: [ListenerTest]});
343+
const fixture = TestBed.createComponent(ListenerTest);
344+
fixture.detectChanges();
345+
const viewRef = fixture.componentInstance.templateRef.createEmbeddedView({name: 'Frodo'});
346+
fixture.componentInstance.containerRef.insert(viewRef);
347+
fixture.detectChanges();
348+
349+
const button = fixture.nativeElement.querySelector('button');
350+
button.click();
351+
expect(events).toEqual(['Frodo']);
352+
353+
viewRef.context = {name: 'Bilbo'};
354+
fixture.detectChanges();
355+
button.click();
356+
expect(events).toEqual(['Frodo', 'Bilbo']);
357+
});
358+
});
276359
});

0 commit comments

Comments
 (0)