Skip to content

Commit

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

Optimize common case
  • Loading branch information
devversion committed Sep 27, 2023
1 parent be9d552 commit 7e9fff0
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 15 deletions.
61 changes: 46 additions & 15 deletions packages/common/src/directives/ng_template_outlet.ts
Expand Up @@ -38,6 +38,7 @@ import {Directive, EmbeddedViewRef, Injector, Input, OnChanges, SimpleChange, Si
})
export class NgTemplateOutlet<C = unknown> implements OnChanges {
private _viewRef: EmbeddedViewRef<C>|null = null;
private _contextForwardProxyInstalled = false;

/**
* A context object to attach to the {@link EmbeddedViewRef}. This should be an
Expand Down Expand Up @@ -68,22 +69,21 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {
// If there is no outlet, clear the destroyed view ref.
if (!this.ngTemplateOutlet) {
this._viewRef = null;
this._contextForwardProxyInstalled = false;
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);
},
});
// If we start with an unset outlet context (which is the common case), we create
// the view without the proxy context to avoid the memory footprint.
// Note: We only take `undefined` here. `null` is an explicit value that indicates
// that there will be future contexts in which case we can optimize and avoid needing a
// view re-creation then (e.g. the `async` pipe yields `null` initially).
const viewContext = this.ngTemplateOutletContext === undefined ?
undefined :
this._createContextForwardProxy();

this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, proxyContext, {
this._contextForwardProxyInstalled = viewContext !== undefined;
this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, viewContext, {
injector: this.ngTemplateOutletInjector ?? undefined,
});
}
Expand All @@ -93,11 +93,42 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {
* We need to re-create existing embedded view if either is true:
* - the outlet changed.
* - the injector changed.
* - the context changed to a value and we previously did not setup the context proxy.
*/
private _shouldRecreateView(changes: SimpleChanges): boolean {
const outletChange = changes['ngTemplateOutlet'];
const injectorChange = changes['ngTemplateOutletInjector'];
// If there is a context change to an actual value- and we never initialized
// the `EmbeddedViewRef` proxy context/or view. In such cases, the view needs to be re-created
// so that we can install the proxy that forwards to the new user-supplied context.
// THIS IS AN OPTIMIZATION: We do not want to create the `Proxy` if we start with an unset
// context (i.e. `undefined`) as this is the common case and we can avoid the allocations.
if (changes['ngTemplateOutletContext'] && this.ngTemplateOutletContext !== undefined &&
this._contextForwardProxyInstalled === false) {
return true;
}

// If the outlet changes, or the injector, we re-create the view.
return !!changes['ngTemplateOutlet'] || !!changes['ngTemplateOutletInjector'];
}

return !!outletChange || !!injectorChange;
/**
* 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.
*/
private _createContextForwardProxy(): C {
return <C>new Proxy({}, {
set: (_target, prop, newValue) => {
if (!this.ngTemplateOutletContext) {
return false;
}
return Reflect.set(this.ngTemplateOutletContext, prop, newValue);
},
get: (_target, prop, receiver) => {
if (!this.ngTemplateOutletContext) {
return undefined;
}
return Reflect.get(this.ngTemplateOutletContext, prop, receiver);
},
});
}
}
24 changes: 24 additions & 0 deletions packages/common/test/directives/ng_template_outlet_spec.ts
Expand Up @@ -318,6 +318,30 @@ describe('NgTemplateOutlet', () => {

expect(fixture.nativeElement.textContent).toBe('Hello World');
});

it('should properly bind context if context is unset initially', () => {
@Component({
imports: [NgTemplateOutlet],
template: `
<ng-template #tpl let-name>Name:{{name}}</ng-template>
<ng-template [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="ctx"></ng-template>
`,
standalone: true,
})
class TestComponent {
ctx: {$implicit: string}|undefined = undefined;
}

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toBe('Name:');

fixture.componentInstance.ctx = {$implicit: 'Angular'};
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toBe('Name:Angular');
});
});

const templateToken = new InjectionToken<string>('templateToken');
Expand Down

0 comments on commit 7e9fff0

Please sign in to comment.