8000 feat(core): Redesign the afterRender & afterNextRender phases API by mmalerba · Pull Request #55648 · angular/angular · GitHub
[go: up one dir, main page]

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
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
19 changes: 18 additions & 1 deletion goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,36 @@ 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?: Omit<AfterRenderOptions, 'phase'>): 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?: Omit<AfterRenderOptions, 'phase'>): AfterRenderRef;

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

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

// @public
// @public @deprecated
export enum AfterRenderPhase {
EarlyRead = 0,
MixedReadWrite = 2,
Expand Down
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pkg_npm(
validate = False,
visibility = ["//packages/core:__pkg__"],
deps = [
"//packages/core/schematics/migrations/after-render-phase:bundle",
"//packages/core/schematics/migrations/http-providers:bundle",
"//packages/core/schematics/migrations/invalid-two-way-bindings:bundle",
"//packages/core/schematics/ng-generate/control-flow-migration:bundle",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"version": "18.0.0",
"description": "Replace deprecated HTTP related modules with provider functions",
"factory": "./migrations/http-providers/bundle"
},
"migration-after-render-phase": {
"version": "18.1.0",
"description": "Updates calls to afterRender with an explicit phase to the new API",
"factory": "./migrations/after-render-phase/bundle"
}
}
}
33 changes: 33 additions & 0 deletions packages/core/schematics/migrations/after-render-phase/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
load("//tools:defaults.bzl", "esbuild", "ts_library")

package(
default_visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
)

ts_library(
name = "after-render-phase",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
deps = [
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)

esbuild(
name = "bundle",
entry_point = ":index.ts",
external = [
"@angular-devkit/*",
"typescript",
],
format = "cjs",
platform = "node",
deps = [":after-render-phase"],
)
58 changes: 58 additions & 0 deletions packages/core/schematics/migrations/after-render-phase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @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 {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics';
import {relative} from 'path';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
import {migrateFile} from './migration';

export default function (): Rule {
return async (tree: Tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];

if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the afterRender phase migration.',
);
}

for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}

function runMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const program = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));

for (const sourceFile of sourceFiles) {
let update: UpdateRecorder | null = null;

const rewriter = (startPos: number, width: number, text: string | null) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, program.getTypeChecker(), rewriter);

if (update !== null) {
tree.commitUpdate(update);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @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 ts from 'typescript';
import {ChangeTracker} from '../../utils/change_tracker';
import {getImportOfIdentifier, getImportSpecifier} from '../../utils/typescript/imports';

const CORE = '@angular/core';
const AFTER_RENDER_PHASE_ENUM = 'AfterRenderPhase';
const AFTER_RENDER_FNS = new Set(['afterRender', 'afterNextRender']);

type RewriteFn = (startPos: number, width: number, text: string) => void;

export function migrateFile(
sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker,
rewriteFn: RewriteFn,
) {
const changeTracker = new ChangeTracker(ts.createPrinter());
const phaseEnum = getImportSpecifier(sourceFile, CORE, AFTER_RENDER_PHASE_ENUM);

// Check if there are any imports of the `AfterRenderPhase` enum.
if (phaseEnum) {
// Remove the `AfterRenderPhase` enum import.
changeTracker.removeNode(phaseEnum);
ts.forEachChild(sourceFile, function visit(node: ts.Node) {
ts.forEachChild(node, visit);

// Check if this is a function call of `afterRender` or `afterNextRender`.
if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
AFTER_RENDER_FNS.has(getImportOfIdentifier(typeChecker, node.expression)?.name || '')
) {
let phase: string | undefined;
const [callback, options] = node.arguments;
// Check if any `AfterRenderOptions` options were specified.
if (ts.isObjectLiteralExpression(options)) {
const phaseProp = options.properties.find((p) => p.name?.getText() === 'phase');
// Check if the `phase` options is set.
if (
phaseProp &&
ts.isPropertyAssignment(phaseProp) &&
ts.isPropertyAccessExpression(phaseProp.initializer) &&
phaseProp.initializer.expression.getText() === AFTER_REN B94A DER_PHASE_ENUM
) {
phaseProp.initializer.expression;
phase = phaseProp.initializer.name.getText();
// Remove the `phase` option.
if (options.properties.length === 1) {
changeTracker.removeNode(options);
} else {
const newOptions = ts.factory.createObjectLiteralExpression(
options.properties.filter((p) => p !== phaseProp),
);
changeTracker.replaceNode(options, newOptions);
}
}
}
// If we found a phase, update the callback.
if (phase) {
phase = phase.substring(0, 1).toLocaleLowerCase() + phase.substring(1);
const spec = ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(ts.factory.createIdentifier(phase), callback),
]);
changeTracker.replaceNode(callback, spec);
}
}
});
}

// Write the changes.
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
}
2 changes: 2 additions & 0 deletions packages/core/schematics/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jasmine_node_test(
data = [
"//packages/core/schematics:collection.json",
"//packages/core/schematics:migrations.json",
"//packages/core/schematics/migrations/after-render-phase",
"//packages/core/schematics/migrations/after-render-phase:bundle",
"//packages/core/schematics/migrations/http-providers",
"//packages/core/schematics/migrations/http-providers:bundle",
"//packages/core/schematics/migrations/invalid-two-way-bindings",
Expand Down
Loading
0