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
This commit is contained in:
Kristiyan Kostadinov 2021-01-08 14:29:45 +02:00 committed by atscott
parent ddf7970b78
commit a3e17190e7
5 changed files with 93 additions and 2 deletions

View File

@ -301,7 +301,7 @@ export declare class ElementRef<T = any> {
}
export declare abstract class EmbeddedViewRef<C> extends ViewRef {
abstract get context(): C;
abstract context: C;
abstract get rootNodes(): any[];
}

View File

@ -93,7 +93,7 @@ export abstract class EmbeddedViewRef<C> 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.

View File

@ -61,6 +61,10 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, 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;
}

View File

@ -264,6 +264,10 @@ export class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
return this._view.context;
}
set context(value: any) {
this._view.context = value;
}
get destroyed(): boolean {
return (this._view.state & ViewState.Destroyed) !== 0;
}

View File

@ -273,4 +273,87 @@ describe('TemplateRef', () => {
});
});
});
describe('context', () => {
@Component({
template: `
<ng-template #templateRef let-name="name">{{name}}</ng-template>
<ng-container #containerRef></ng-container>
`
})
class App {
@ViewChild('templateRef') templateRef!: TemplateRef<any>;
@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: `
<ng-template #templateRef let-name="name">
<button (click)="log(name)"></button>
</ng-template>
<ng-container #containerRef></ng-container>
`
})
class ListenerTest {
@ViewChild('templateRef') templateRef!: TemplateRef<any>;
@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']);
});
});
});