Skip to content

Commit

Permalink
feat(core): Redesign the afterRender & afterNextRender phases API (
Browse files Browse the repository at this point in the history
…#55648)

Previously `afterRender` and `afterNextRender` allowed the user to pass
a phase to run the callback in as part of the `AfterRenderOptions`. This
worked, but made it cumbersome to coordinate work between phases.

```ts
let size: DOMRect|null = null;

afterRender(() => {
  size = nativeEl.getBoundingClientRect();
}, {phase: AfterRenderPhase.EarlyRead});

afterRender(() => {
  otherNativeEl.style.width = size!.width + 'px';
}, {phase: AfterRenderPhase.Write});
```

This PR replaces the old phases API with a new one that allows passing a
callback per phase in a single `afterRender` / `afterNextRender` call.
The return value of each phase's callback is passed to the subsequent
callbacks that were part of that `afterRender` call.

```ts
afterRender({
  earlyRead: () => nativeEl.getBoundingClientRect(),
  write: (rect) => {
    otherNativeEl.style.width = rect.width + 'px';
  }
});
```

This API also retains the ability to pass a single callback, which will
be run in the `mixedReadWrite` phase.

```ts
afterRender(() => {
  // read some stuff ...
  // write some stuff ...
});
```

PR Close #55648
  • Loading branch information
mmalerba authored and alxhub committed Jun 10, 2024
1 parent 6db94d5 commit a655e46
Show file tree
Hide file tree
Showing 5 changed files with 535 additions and 202 deletions.
48 changes: 30 additions & 18 deletions adev/src/content/guide/components/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,30 +239,42 @@ Render callbacks do not run during server-side rendering or during build-time pr

#### afterRender phases

When using `afterRender` or `afterNextRender`, you can optionally specify a `phase`. The phase
gives you control over the sequencing of DOM operations, letting you sequence _write_ operations
before _read_ operations in order to minimize
[layout thrashing](https://web.dev/avoid-large-complex-layouts-and-layout-thrashing).
When using `afterRender` or `afterNextRender`, you can optionally split the work into phases. The
phase gives you control over the sequencing of DOM operations, letting you sequence _write_
operations before _read_ operations in order to minimize
[layout thrashing](https://web.dev/avoid-large-complex-layouts-and-layout-thrashing). In order to
communicate across phases, a phase function may return a result value that can be accessed in the
next phase.

```ts
import {Component, ElementRef, afterNextRender, AfterRenderPhase} from '@angular/core';
import {Component, ElementRef, afterNextRender} from '@angular/core';

@Component({...})
export class UserProfile {
private prevPadding = 0;
private elementHeight = 0;

constructor(elementRef: ElementRef) {
const nativeElement = elementRef.nativeElement;

// Use the `Write` phase to write to a geometric property.
afterNextRender(() => {
nativeElement.style.padding = computePadding();
}, {phase: AfterRenderPhase.Write});

// Use the `Read` phase to read geometric properties after all writes have occurred.
afterNextRender(() => {
this.elementHeight = nativeElement.getBoundingClientRect().height;
}, {phase: AfterRenderPhase.Read});
afterNextRender({
// Use the `Write` phase to write to a geometric property.
write: () => {
const padding = computePadding();
const changed = padding !== prevPadding;
if (changed) {
nativeElement.style.padding = padding;
}
return changed; // Communicate whether anything changed to the read phase.
},

// Use the `Read` phase to read geometric properties after all writes have occurred.
read: (didWrite) => {
if (didWrite) {
this.elementHeight = nativeElement.getBoundingClientRect().height;
}
}
});
}
}
```
Expand All @@ -271,10 +283,10 @@ There are four phases, run in the following order:

| Phase | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EarlyRead` | Use this phase to read any layout-affecting DOM properties and styles that are strictly necessary for subsequent calculation. Avoid this phase if possible, preferring the `Write` and `Read` phases. |
| `MixedReadWrite` | Default phase. Use for any operations need to both read and write layout-affecting properties and styles. Avoid this phase if possible, preferring the explicit `Write` and `Read` phases. |
| `Write` | Use this phase to write layout-affecting DOM properties and styles. |
| `Read` | Use this phase to read any layout-affecting DOM properties. |
| `earlyRead` | Use this phase to read any layout-affecting DOM properties and styles that are strictly necessary for subsequent calculation. Avoid this phase if possible, preferring the `write` and `read` phases. |
| `mixedReadWrite` | Default phase. Use for any operations need to both read and write layout-affecting properties and styles. Avoid this phase if possible, preferring the explicit `write` and `read` phases. |
| `write` | Use this phase to write layout-affecting DOM properties and styles. |
| `read` | Use this phase to read any layout-affecting DOM properties. |

## Lifecycle interfaces

Expand Down
25 changes: 16 additions & 9 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,31 @@ export interface AfterContentInit {
ngAfterContentInit(): void;
}

// @public
export function afterNextRender<E = never, W = never, M = never>(spec: {
earlyRead?: () => E;
write?: (...args: ɵFirstAvailable<[E]>) => W;
mixedReadWrite?: (...args: ɵFirstAvailable<[W, E]>) => M;
read?: (...args: ɵFirstAvailable<[M, W, E]>) => void;
}, opts?: AfterRenderOptions): AfterRenderRef;

// @public
export function afterNextRender(callback: VoidFunction, options?: AfterRenderOptions): AfterRenderRef;

// @public
export function afterRender<E = never, W = never, M = never>(spec: {
earlyRead?: () => E;
write?: (...args: ɵFirstAvailable<[E]>) => W;
mixedReadWrite?: (...args: ɵFirstAvailable<[W, E]>) => M;
read?: (...args: ɵFirstAvailable<[M, W, E]>) => void;
}, opts?: AfterRenderOptions): AfterRenderRef;

// @public
export function afterRender(callback: VoidFunction, options?: AfterRenderOptions): AfterRenderRef;

// @public
export interface AfterRenderOptions {
injector?: Injector;
phase?: AfterRenderPhase;
}

// @public
export enum AfterRenderPhase {
EarlyRead = 0,
MixedReadWrite = 2,
Read = 3,
Write = 1
}

// @public
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export {isStandalone} from './render3/definition';
export {
AfterRenderRef,
AfterRenderOptions,
AfterRenderPhase,
afterRender,
afterNextRender,
ɵFirstAvailable,
} from './render3/after_render_hooks';
export {ApplicationConfig, mergeApplicationConfig} from './application/application_config';
export {makeStateKey, StateKey, TransferState} from './transfer_state';
Expand Down
Loading

0 comments on commit a655e46

Please sign in to comment.