Skip to content

Commit

Permalink
refactor(common): update NgTemplateOutlet to no longer rely on cont…
Browse files Browse the repository at this point in the history
…ext swapping

The context of an embedded view ref at some point was switched from a
getter to an actual assignable property. This is something we reverted
with the previous commit as it introduces additional complexity for our
generated code (in terms of closures capturing the `ctx`).

This change impacted the template outlet code because we actively relied
on swapping out the full context if the user changes it. Previousl,
before we allowed to swap out the context (in v16), we mutated the
initial view context if it didn't change structurally- and in other
cases the view was re-created. We improved this performance aspect with
the changes to allow for the context to be swapped out + actually also
fixed a bug where the initial context object was mutated and the user
could observe this change.

This commit adjusts for context not being replacable- while still
keeping the bugs fixed and preserving the performance wins of not
having to destroy/re-create the view whenever the context changes.
  • Loading branch information
devversion committed Sep 25, 2023
1 parent 824f6f0 commit f19a60d
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 14 deletions.
2 changes: 1 addition & 1 deletion goldens/size-tracking/aio-payloads.json
Expand Up @@ -19,4 +19,4 @@
"dark-theme": 31808
}
}
}
}
59 changes: 46 additions & 13 deletions packages/common/src/directives/ng_template_outlet.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
import {Directive, EmbeddedViewRef, Injector, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';

/**
* @ngModule CommonModule
Expand All @@ -32,39 +32,72 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange
*
* @publicApi
*/
@Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges {
private _viewRef: EmbeddedViewRef<any>|null = null;
@Directive({
selector: '[ngTemplateOutlet]',
standalone: true,
})
export class NgTemplateOutlet<C = unknown> implements OnChanges {
private _viewRef: EmbeddedViewRef<C>|null = null;

/**
* A context object to attach to the {@link EmbeddedViewRef}. This should be an
* object, the object's keys will be available for binding by the local template `let`
* declarations.
* Using the key `$implicit` in the context object will set its value as default.
*/
@Input() public ngTemplateOutletContext: Object|null = null;
@Input() public ngTemplateOutletContext: C|null = null;

/**
* A string defining the template reference and optionally the context object for the template.
*/
@Input() public ngTemplateOutlet: TemplateRef<any>|null = null;
@Input() public ngTemplateOutlet: TemplateRef<C>|null = null;

/** Injector to be used within the embedded view. */
@Input() public ngTemplateOutletInjector: Injector|null = null;

constructor(private _viewContainerRef: ViewContainerRef) {}

ngOnChanges(changes: SimpleChanges) {
if (changes['ngTemplateOutlet']) {
if (this._shouldRecreateView(changes)) {
const viewContainerRef = this._viewContainerRef;

if (this._viewRef) {
viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
}

this._viewRef = this.ngTemplateOutlet ?
viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, this.ngTemplateOutletContext) :
null;
} else if (
this._viewRef && changes['ngTemplateOutletContext'] && this.ngTemplateOutletContext) {
this._viewRef.context = this.ngTemplateOutletContext;
// If there is no outlet, clear the destroyed view ref.
if (!this.ngTemplateOutlet) {
this._viewRef = null;
return;
}

// For a given outlet instance, we create a proxy object that delegates
// to the user-specified context. This allows changing, or swapping out
// the context object completely without having to destroy/re-create the view.
const proxyContext = <C>new Proxy({}, {
get: (_target, prop, receiver) => {
if (!this.ngTemplateOutletContext) {
return undefined;
}
return Reflect.get(this.ngTemplateOutletContext, prop, receiver);
},
});

this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, proxyContext, {
injector: this.ngTemplateOutletInjector ?? undefined,
});
}
}

/**
* We need to re-create existing embedded view if either is true:
* - the outlet changed.
* - the injector changed.
*/
private _shouldRecreateView(changes: SimpleChanges): boolean {
const outletChange = changes['ngTemplateOutlet'];
const injectorChange = changes['ngTemplateOutletInjector'];

return !!outletChange || !!injectorChange;
}
}

0 comments on commit f19a60d

Please sign in to comment.