Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion goldens/size-tracking/aio-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
"dark-theme": 34598
}
}
}
}
14 changes: 14 additions & 0 deletions modules/benchmarks/src/ng_template_outlet_context/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "perf_lib",
testonly = True,
srcs = ["ng_template_outlet_context.perf-spec.ts"],
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
deps = [
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
"@npm//protractor",
],
)
39 changes: 39 additions & 0 deletions modules/benchmarks/src/ng_template_outlet_context/ng2/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")

package(default_visibility = ["//modules/benchmarks:__subpackages__"])

ng_module(
name = "ng2",
srcs = glob(["*.ts"]),
strict_templates = True,
tsconfig = "//modules/benchmarks:tsconfig-build.json",
deps = [
"//modules/benchmarks/src:util_lib",
"//packages/core",
"//packages/platform-browser",
],
)

app_bundle(
name = "bundle",
entry_point = ":index.ts",
deps = [
":ng2",
],
)

http_server(
name = "prodserver",
srcs = ["index.html"],
deps = [
":bundle.debug.min.js",
"//packages/zone.js/bundles:zone.umd.js",
],
)

benchmark_test(
name = "perf",
server = ":prodserver",
deps = ["//modules/benchmarks/src/ng_template_outlet_context:perf_lib"],
)
16 changes: 16 additions & 0 deletions modules/benchmarks/src/ng_template_outlet_context/ng2/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<!-- Prevent the browser from requesting any favicon. -->
<link rel="icon" href="data:," />
</head>

<body>
<div>
<app-component>Loading...</app-component>
</div>

<script src="/angular/packages/zone.js/bundles/zone.umd.js"></script>
<script src="/bundle.debug.min.js"></script>
</body>
</html>
82 changes: 82 additions & 0 deletions modules/benchmarks/src/ng_template_outlet_context/ng2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {NgIf, NgTemplateOutlet} from '@angular/common';
import {Component, enableProdMode, Input} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';

@Component({
selector: 'deep',
standalone: true,
imports: [NgIf],
template: `<deep *ngIf="depth > 1" [depth]="depth - 1" /> Level: {{depth}}`,
})
class Deep {
@Input({required: true}) depth: number;
}

@Component({
selector: 'app-component',
standalone: true,
imports: [NgTemplateOutlet, Deep],
template: `
<button id="swapOutFull" (click)="swapOutFull()">Swap out full context</button>
<button id="modifyProperty" (click)="modifyProperty()">Modify property</button>
<button id="modifyDeepProperty" (click)="modifyDeepProperty()">Modify deep property</button>
<button id="addNewProperty" (click)="addNewProperty()">Add new property</button>

<ng-template #templateRef let-implicit let-a="a" let-b="b" let-deep="deep" let-new="new">
<p>Implicit: {{implicit}}</p>
<p>A: {{a}}</p>
<p>B: {{b}}</p>
<p>Deep: {{deep.next.text}}</p>
<p>New: {{new}}</p>

<deep [depth]="20" />
</ng-template>

<div>
<p>Outlet</p>
<ng-template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="context" />
</div>
`,
})
class AppComponent {
context:
{$implicit: unknown, a: unknown, b: unknown, deep: {next: {text: unknown}}, new?: unknown} = {
$implicit: 'Default Implicit',
a: 'Default A',
b: 'Default B',
deep: {next: {text: 'Default deep text'}},
};

swapOutFull() {
this.context = {
$implicit: 'New Implicit new Object',
a: 'New A new Object',
b: 'New B new Object',
deep: {next: {text: 'New Deep text new Object'}},
};
}

modifyProperty() {
this.context.a = 'Modified a';
}

modifyDeepProperty() {
this.context.deep.next.text = 'Modified deep a';
}

addNewProperty() {
this.context.new = 'New property set';
}
}

enableProdMode();
bootstrapApplication(AppComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {runBenchmark, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
import {$} from 'protractor';

interface Worker {
id: string;
prepare?(): void;
work(): void;
}

const SwapFullContext = {
id: 'swapFullContext',
work: () => {
$('#swapOutFull').click();
}
};

const ModifyContextProperty = {
id: 'modifyContextProperty',
work: () => {
$('#modifyProperty').click();
}
};

const ModifyContextDeepProperty = {
id: 'modifyContextDeepProperty',
work: () => {
$('#modifyDeepProperty').click();
}
};

const AddNewContextProperty = {
id: 'addNewContextProperty',
work: () => {
$('#addNewProperty').click();
}
};

const scenarios = [
SwapFullContext,
ModifyContextProperty,
ModifyContextDeepProperty,
AddNewContextProperty,
];

describe('ng_template_outlet_context benchmark spec', () => {
afterEach(verifyNoBrowserErrors);

scenarios.forEach((worker) => {
describe(worker.id, () => {
it('should run for ng2', async () => {
await runBenchmarkScenario(
{url: '/', id: `ngTemplateOutletContext.ng2.${worker.id}`, worker: worker});
});
});
});

function runBenchmarkScenario(config: {id: string, url: string, worker: Worker}) {
return runBenchmark({
id: config.id,
url: config.url,
ignoreBrowserSynchronization: true,
prepare: config.worker.prepare,
work: config.worker.work
});
}
});
3 changes: 1 addition & 2 deletions modules/benchmarks/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,5 @@
"no-floating-promises": true,
"no-unused-expression": true,
"no-unused-variable": true
},
"include": ["../../node_modules/@angular/build-tooling/bazel/benchmark/driver-utilities/"]
}
}
59 changes: 43 additions & 16 deletions packages/common/src/directives/ng_template_outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

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

/**
* @ngModule CommonModule
Expand Down Expand Up @@ -57,30 +57,57 @@ export class NgTemplateOutlet<C = unknown> implements OnChanges {

constructor(private _viewContainerRef: ViewContainerRef) {}

/** @nodoc */
ngOnChanges(changes: SimpleChanges) {
if (changes['ngTemplateOutlet'] || changes['ngTemplateOutletInjector']) {
if (this._shouldRecreateView(changes)) {
const viewContainerRef = this._viewContainerRef;

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

if (this.ngTemplateOutlet) {
const {
ngTemplateOutlet: template,
ngTemplateOutletContext: context,
ngTemplateOutletInjector: injector,
} = this;
this._viewRef =
viewContainerRef.createEmbeddedView(
template, context, injector ? {injector} : undefined) as EmbeddedViewRef<C>;
} else {
// If there is no outlet, clear the destroyed view ref.
if (!this.ngTemplateOutlet) {
this._viewRef = null;
return;
}
} else if (
this._viewRef && changes['ngTemplateOutletContext'] && this.ngTemplateOutletContext) {
this._viewRef.context = this.ngTemplateOutletContext;

// Create a context forward `Proxy` that will always bind to the user-specified context,
// without having to destroy and re-create views whenever the context changes.
const viewContext = this._createContextForwardProxy();
this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, viewContext, {
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 {
return !!changes['ngTemplateOutlet'] || !!changes['ngTemplateOutletInjector'];
}

/**
* 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
Original file line number Diff line number Diff line change
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
12 changes: 12 additions & 0 deletions packages/core/src/render3/view_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,19 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, InternalViewRef, ChangeDe
return this._lView[CONTEXT] as unknown as T;
}

/**
* @deprecated Replacing the full context object is not supported. Modify the context
* directly, or consider using a `Proxy` if you need to replace the full object.
* // TODO(devversion): Remove this.
*/
set context(value: T) {
if (ngDevMode) {
// Note: We have a warning message here because the `@deprecated` JSDoc will not be picked
// up for assignments on the setter. We want to let users know about the deprecated usage.
console.warn(
'Angular: Replacing the `context` object of an `EmbeddedViewRef` is deprecated.');
}

this._lView[CONTEXT] = value as unknown as {};
}

Expand Down
Loading