fix(common): don't recreate view when context shape doesn't change (#18277)
Problem description: when using ngTemplateOutlet with context as an object literal in a template and binding to the context's property the embedded view would get re-created even if context object remains essentially the same (the same shape, just update to one properties). This happens since currently change detection will re-create object references when an object literal is used and one of its properties gets updated through a binding. Solution: this commit changes ngTemplateOutlet logic so we take context object shape into account before deciding if we should re-create view or just update existing context. Fixes #13407
This commit is contained in:

committed by
Victor Berchet

parent
5b7432b6ea
commit
685cc26ab2
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
|
||||
/**
|
||||
* @ngModule CommonModule
|
||||
@ -49,13 +49,58 @@ export class NgTemplateOutlet implements OnChanges {
|
||||
set ngOutletContext(context: Object) { this.ngTemplateOutletContext = context; }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this._viewRef) {
|
||||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef));
|
||||
}
|
||||
const recreateView = this._shouldRecreateView(changes);
|
||||
|
||||
if (this.ngTemplateOutlet) {
|
||||
this._viewRef = this._viewContainerRef.createEmbeddedView(
|
||||
this.ngTemplateOutlet, this.ngTemplateOutletContext);
|
||||
if (recreateView) {
|
||||
if (this._viewRef) {
|
||||
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef));
|
||||
}
|
||||
|
||||
if (this.ngTemplateOutlet) {
|
||||
this._viewRef = this._viewContainerRef.createEmbeddedView(
|
||||
this.ngTemplateOutlet, this.ngTemplateOutletContext);
|
||||
}
|
||||
} else {
|
||||
if (this._viewRef && this.ngTemplateOutletContext) {
|
||||
this._updateExistingContext(this.ngTemplateOutletContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to re-create existing embedded view if:
|
||||
* - templateRef has changed
|
||||
* - context has changes
|
||||
*
|
||||
* To mark context object as changed when the corresponding object
|
||||
* shape changes (new properties are added or existing properties are removed).
|
||||
* In other words we consider context with the same properties as "the same" even
|
||||
* if object reference changes (see https://github.com/angular/angular/issues/13407).
|
||||
*/
|
||||
private _shouldRecreateView(changes: SimpleChanges): boolean {
|
||||
const ctxChange = changes['ngTemplateOutletContext'];
|
||||
return !!changes['ngTemplateOutlet'] || (ctxChange && this._hasContextShapeChanged(ctxChange));
|
||||
}
|
||||
|
||||
private _hasContextShapeChanged(ctxChange: SimpleChange): boolean {
|
||||
const prevCtxKeys = Object.keys(ctxChange.previousValue || {});
|
||||
const currCtxKeys = Object.keys(ctxChange.currentValue || {});
|
||||
|
||||
if (prevCtxKeys.length === currCtxKeys.length) {
|
||||
for (let propName of currCtxKeys) {
|
||||
if (prevCtxKeys.indexOf(propName) === -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateExistingContext(ctx: Object): void {
|
||||
for (let propName of Object.keys(ctx)) {
|
||||
(<any>this._viewRef.context)[propName] = (<any>this.ngTemplateOutletContext)[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user