Skip to content

Commit e53d4ec

Browse files
devknollalxhub
authored andcommitted
feat(core): add afterRender and afterNextRender (angular#50607)
Add and expose the after*Render functions as developer preview PR Close angular#50607
1 parent 8913d3e commit e53d4ec

File tree

26 files changed

+950
-30
lines changed

26 files changed

+950
-30
lines changed

goldens/public-api/core/errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export const enum RuntimeErrorCode {
107107
// (undocumented)
108108
RECURSIVE_APPLICATION_REF_TICK = 101,
109109
// (undocumented)
110+
RECURSIVE_APPLICATION_RENDER = 102,
111+
// (undocumented)
110112
RENDERER_NOT_FOUND = 407,
111113
// (undocumented)
112114
REQUIRE_SYNC_WITHOUT_SYNC_EMIT = 601,

goldens/public-api/core/index.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ export interface AfterContentInit {
2424
ngAfterContentInit(): void;
2525
}
2626

27+
// @public
28+
export function afterNextRender(callback: VoidFunction, options?: AfterRenderOptions): AfterRenderRef;
29+
30+
// @public
31+
export function afterRender(callback: VoidFunction, options?: AfterRenderOptions): AfterRenderRef;
32+
33+
// @public
34+
export interface AfterRenderOptions {
35+
injector?: Injector;
36+
}
37+
38+
// @public
39+
export interface AfterRenderRef {
40+
destroy(): void;
41+
}
42+
2743
// @public
2844
export interface AfterViewChecked {
2945
ngAfterViewChecked(): void;

packages/core/src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {Sanitizer} from './sanitization/sanitizer';
4040
export {createNgModule, createNgModuleRef, createEnvironmentInjector} from './render3/ng_module_ref';
4141
export {createComponent, reflectComponentType, ComponentMirror} from './render3/component';
4242
export {isStandalone} from './render3/definition';
43+
export {AfterRenderRef, AfterRenderOptions, afterRender, afterNextRender} from './render3/after_render_hooks';
4344
export {ApplicationConfig, mergeApplicationConfig} from './application_config';
4445
export {makeStateKey, StateKey, TransferState} from './transfer_state';
4546
export {booleanAttribute, numberAttribute} from './util/coercion';

packages/core/src/core_render3_private_export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,6 @@ export {
282282
export {
283283
noSideEffects as ɵnoSideEffects,
284284
} from './util/closure';
285-
285+
export { AfterRenderEventManager as ɵAfterRenderEventManager } from './render3/after_render_hooks';
286286

287287
// clang-format on

packages/core/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const enum RuntimeErrorCode {
3030
// Change Detection Errors
3131
EXPRESSION_CHANGED_AFTER_CHECKED = -100,
3232
RECURSIVE_APPLICATION_REF_TICK = 101,
33+
RECURSIVE_APPLICATION_RENDER = 102,
3334

3435
// Dependency Injection Errors
3536
CYCLIC_DI_DEPENDENCY = -200,

packages/core/src/hydration/api.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {enableLocateOrCreateElementContainerNodeImpl} from '../render3/instructi
2020
import {enableApplyRootElementTransformImpl} from '../render3/instructions/shared';
2121
import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template';
2222
import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
23+
import {isPlatformBrowser} from '../render3/util/misc_utils';
2324
import {TransferState} from '../transfer_state';
2425
import {NgZone} from '../zone';
2526

@@ -66,15 +67,6 @@ function enableHydrationRuntimeSupport() {
6667
}
6768
}
6869

69-
/**
70-
* Detects whether the code is invoked in a browser.
71-
* Later on, this check should be replaced with a tree-shakable
72-
* flag (e.g. `!isServer`).
73-
*/
74-
function isBrowser(): boolean {
75-
return inject(PLATFORM_ID) === 'browser';
76-
}
77-
7870
/**
7971
* Outputs a message with hydration stats into a console.
8072
*/
@@ -129,7 +121,7 @@ export function withDomHydration(): EnvironmentProviders {
129121
provide: IS_HYDRATION_DOM_REUSE_ENABLED,
130122
useFactory: () => {
131123
let isEnabled = true;
132-
if (isBrowser()) {
124+
if (isPlatformBrowser()) {
133125
// On the client, verify that the server response contains
134126
// hydration annotations. Otherwise, keep hydration disabled.
135127
const transferState = inject(TransferState, {optional: true});
@@ -161,7 +153,7 @@ export function withDomHydration(): EnvironmentProviders {
161153
// on the client. Moving forward, the `isBrowser` check should
162154
// be replaced with a tree-shakable alternative (e.g. `isServer`
163155
// flag).
164-
if (isBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
156+
if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
165157
enableHydrationRuntimeSupport();
166158
}
167159
},
@@ -174,13 +166,13 @@ export function withDomHydration(): EnvironmentProviders {
174166
// environment and when hydration is configured properly.
175167
// On a server, an application is rendered from scratch,
176168
// so the host content needs to be empty.
177-
return isBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED);
169+
return isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED);
178170
}
179171
},
180172
{
181173
provide: APP_BOOTSTRAP_LISTENER,
182174
useFactory: () => {
183-
if (isBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
175+
if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
184176
const appRef = inject(ApplicationRef);
185177
const injector = inject(Injector);
186178
return () => {
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {assertInInjectionContext, Injector, ɵɵdefineInjectable} from '../di';
10+
import {inject} from '../di/injector_compatibility';
11+
import {RuntimeError, RuntimeErrorCode} from '../errors';
12+
import {DestroyRef} from '../linker/destroy_ref';
13+
14+
import {isPlatformBrowser} from './util/misc_utils';
15+
16+
/**
17+
* Options passed to `afterRender` and `afterNextRender`.
18+
*
19+
* @developerPreview
20+
*/
21+
export interface AfterRenderOptions {
22+
/**
23+
* The `Injector` to use during creation.
24+
*
25+
* If this is not provided, the current injection context will be used instead (via `inject`).
26+
*/
27+
injector?: Injector;
28+
}
29+
30+
/**
31+
* A callback that runs after render.
32+
*
33+
* @developerPreview
34+
*/
35+
export interface AfterRenderRef {
36+
/**
37+
* Shut down the callback, preventing it from being called again.
38+
*/
39+
destroy(): void;
40+
}
41+
42+
/**
43+
* Register a callback to be invoked each time the application
44+
* finishes rendering.
45+
*
46+
* Note that the callback will run
47+
* - in the order it was registered
48+
* - once per render
49+
* - on browser platforms only
50+
*
51+
* <div class="alert is-important">
52+
*
53+
* Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.
54+
* You must use caution when directly reading or writing the DOM and layout.
55+
*
56+
* </div>
57+
*
58+
* @param callback A callback function to register
59+
*
60+
* @usageNotes
61+
*
62+
* Use `afterRender` to read or write the DOM after each render.
63+
*
64+
* ### Example
65+
* ```ts
66+
* @Component({
67+
* selector: 'my-cmp',
68+
* template: `<span #content>{{ ... }}</span>`,
69+
* })
70+
* export class MyComponent {
71+
* @ViewChild('content') contentRef: ElementRef;
72+
*
73+
* constructor() {
74+
* afterRender(() => {
75+
* console.log('content height: ' + this.contentRef.nativeElement.scrollHeight);
76+
* });
77+
* }
78+
* }
79+
* ```
80+
*
81+
* @developerPreview
82+
*/
83+
export function afterRender(callback: VoidFunction, options?: AfterRenderOptions): AfterRenderRef {
84+
!options && assertInInjectionContext(afterRender);
85+
const injector = options?.injector ?? inject(Injector);
86+
87+
if (!isPlatformBrowser(injector)) {
88+
return {destroy() {}};
89+
}
90+
91+
let destroy: VoidFunction|undefined;
92+
const unregisterFn = injector.get(DestroyRef).onDestroy(() => destroy?.());
93+
const manager = injector.get(AfterRenderEventManager);
94+
const instance = new AfterRenderCallback(callback);
95+
96+
destroy = () => {
97+
manager.unregister(instance);
98+
unregisterFn();
99+
};
100+
manager.register(instance);
101+
return {destroy};
102+
}
103+
104+
/**
105+
* Register a callback to be invoked the next time the application
106+
* finishes rendering.
107+
*
108+
* Note that the callback will run
109+
* - in the order it was registered
110+
* - on browser platforms only
111+
*
112+
* <div class="alert is-important">
113+
*
114+
* Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.
115+
* You must use caution when directly reading or writing the DOM and layout.
116+
*
117+
* </div>
118+
*
119+
* @param callback A callback function to register
120+
*
121+
* @usageNotes
122+
*
123+
* Use `afterNextRender` to read or write the DOM once,
124+
* for example to initialize a non-Angular library.
125+
*
126+
* ### Example
127+
* ```ts
128+
* @Component({
129+
* selector: 'my-chart-cmp',
130+
* template: `<div #chart>{{ ... }}</div>`,
131+
* })
132+
* export class MyChartCmp {
133+
* @ViewChild('chart') chartRef: ElementRef;
134+
* chart: MyChart|null;
135+
*
136+
* constructor() {
137+
* afterNextRender(() => {
138+
* this.chart = new MyChart(this.chartRef.nativeElement);
139+
* });
140+
* }
141+
* }
142+
* ```
143+
*
144+
* @developerPreview
145+
*/
146+
export function afterNextRender(
147+
callback: VoidFunction, options?: AfterRenderOptions): AfterRenderRef {
148+
!options && assertInInjectionContext(afterNextRender);
149+
const injector = options?.injector ?? inject(Injector);
150+
151+
if (!isPlatformBrowser(injector)) {
152+
return {destroy() {}};
153+
}
154+
155+
let destroy: VoidFunction|undefined;
156+
const unregisterFn = injector.get(DestroyRef).onDestroy(() => destroy?.());
157+
const manager = injector.get(AfterRenderEventManager);
158+
const instance = new AfterRenderCallback(() => {
159+
destroy?.();
160+
callback();
161+
});
162+
163+
destroy = () => {
164+
manager.unregister(instance);
165+
unregisterFn();
166+
};
167+
manager.register(instance);
168+
return {destroy};
169+
}
170+
171+
/**
172+
* A wrapper around a function to be used as an after render callback.
173+
* @private
174+
*/
175+
class AfterRenderCallback {
176+
private callback: VoidFunction;
177+
178+
constructor(callback: VoidFunction) {
179+
this.callback = callback;
180+
}
181+
182+
invoke() {
183+
this.callback();
184+
}
185+
}
186+
187+
/**
188+
* Implements `afterRender` and `afterNextRender` callback manager logic.
189+
*/
190+
export class AfterRenderEventManager {
191+
private callbacks = new Set<AfterRenderCallback>();
192+
private deferredCallbacks = new Set<AfterRenderCallback>();
193+
private renderDepth = 0;
194+
private runningCallbacks = false;
195+
196+
/**
197+
* Mark the beginning of a render operation (i.e. CD cycle).
198+
* Throws if called from an `afterRender` callback.
199+
*/
200+
begin() {
201+
if (this.runningCallbacks) {
202+
throw new RuntimeError(
203+
RuntimeErrorCode.RECURSIVE_APPLICATION_RENDER,
204+
ngDevMode &&
205+
'A new render operation began before the previous operation ended. ' +
206+
'Did you trigger change detection from afterRender or afterNextRender?');
207+
}
208+
209+
this.renderDepth++;
210+
}
211+
212+
/**
213+
* Mark the end of a render operation. Registered callbacks
214+
* are invoked if there are no more pending operations.
215+
*/
216+
end() {
217+
this.renderDepth--;
218+
219+
if (this.renderDepth === 0) {
220+
try {
221+
this.runningCallbacks = true;
222+
for (const callback of this.callbacks) {
223+
callback.invoke();
224+
}
225+
} finally {
226+
this.runningCallbacks = false;
227+
for (const callback of this.deferredCallbacks) {
228+
this.callbacks.add(callback);
229+
}
230+
this.deferredCallbacks.clear();
231+
}
232+
}
233+
}
234+
235+
register(callback: AfterRenderCallback) {
236+
// If we're currently running callbacks, new callbacks should be deferred
237+
// until the next render operation.
238+
const target = this.runningCallbacks ? this.deferredCallbacks : this.callbacks;
239+
target.add(callback);
240+
}
241+
242+
unregister(callback: AfterRenderCallback) {
243+
this.callbacks.delete(callback);
244+
this.deferredCallbacks.delete(callback);
245+
}
246+
247+
ngOnDestroy() {
248+
this.callbacks.clear();
249+
this.deferredCallbacks.clear();
250+
}
251+
252+
/** @nocollapse */
253+
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
254+
token: AfterRenderEventManager,
255+
providedIn: 'root',
256+
factory: () => new AfterRenderEventManager(),
257+
});
258+
}

packages/core/src/render3/component_ref.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {assertDefined, assertGreaterThan, assertIndexInRange} from '../util/asse
2626
import {VERSION} from '../version';
2727
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider_flags';
2828

29+
import {AfterRenderEventManager} from './after_render_hooks';
2930
import {assertComponentType} from './assert';
3031
import {attachPatchData} from './context_discovery';
3132
import {getComponentDef} from './definition';
@@ -189,10 +190,13 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
189190

190191
const effectManager = rootViewInjector.get(EffectManager, null);
191192

193+
const afterRenderEventManager = rootViewInjector.get(AfterRenderEventManager, null);
194+
192195
const environment: LViewEnvironment = {
193196
rendererFactory,
194197
sanitizer,
195198
effectManager,
199+
afterRenderEventManager,
196200
};
197201

198202
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);

0 commit comments

Comments
 (0)