Skip to content

Commit 6ddb250

Browse files
aparziAndrewKushnir
authored andcommitted
feat(migrations): add migration to convert ngClass to use class (#62983)
feat #61661 - add migration to convert ngClass to use class PR Close #62983
1 parent 7d6ae95 commit 6ddb250

File tree

14 files changed

+1728
-0
lines changed

14 files changed

+1728
-0
lines changed

adev/src/app/sub-navigation-data.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,12 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
14181418
path: 'reference/migrations/self-closing-tags',
14191419
contentPath: 'reference/migrations/self-closing-tags',
14201420
},
1421+
{
1422+
label: 'NgClass to Class',
1423+
path: 'reference/migrations/ngclass-to-class',
1424+
contentPath: 'reference/migrations/ngclass-to-class',
1425+
status: 'new',
1426+
},
14211427
],
14221428
},
14231429
];
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Migration from NgClass to class bindings
2+
3+
This schematic migrates NgClass directive usages to class bindings in your application.
4+
It will only migrate usages that are considered safe to migrate.
5+
6+
Run the schematic using the following command:
7+
8+
```bash
9+
ng generate @angular/core:ngclass-to-class
10+
```
11+
12+
13+
#### Before
14+
15+
```html
16+
<div [ngClass]="{admin: isAdmin, dense: density === 'high'}">
17+
```
18+
19+
20+
#### After
21+
22+
```html
23+
<div [class]="{admin: isAdmin, dense: density === 'high'}">
24+
```
25+
26+
## Configuration options
27+
28+
The migration supports a few options for fine tuning the migration to your specific needs.
29+
30+
### `--migrate-space-separated-key`
31+
32+
By default, the migration avoids migrating usages of `NgClass` in which object literal keys contain space-separated class names."
33+
When the --migrate-space-separated-key flag is enabled, a binding is created for each individual key.
34+
35+
36+
```html
37+
<div [ngClass]="{'class1 class2': condition}"></div>
38+
```
39+
40+
to
41+
42+
```html
43+
<div [class.class1]="condition" [class.class2]="condition"></div>
44+
```

adev/src/content/reference/migrations/overview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ Learn about how you can migrate your existing angular project to the latest feat
3030
<docs-card title="Self-closing tags" link="Migrate now" href="reference/migrations/self-closing-tags">
3131
Convert component templates to use self-closing tags where possible.
3232
</docs-card>
33+
<docs-card title="NgClass to Class Bindings" link="Migrate now" href="reference/migrations/ngclass-to-class">
34+
Convert component templates to prefer class bindings over the `NgClass`directives when possible.
35+
</docs-card>
3336
</docs-card-container>

packages/core/schematics/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ npm_package(
4343
"migrations.json",
4444
":bundles",
4545
"//packages/core/schematics/migrations/control-flow-migration:static_files",
46+
"//packages/core/schematics/migrations/ngclass-to-class-migration:static_files",
4647
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
4748
"//packages/core/schematics/ng-generate/inject-migration:static_files",
4849
"//packages/core/schematics/ng-generate/output-migration:static_files",
@@ -97,6 +98,10 @@ bundle_entrypoints = [
9798
"control-flow-migration",
9899
"packages/core/schematics/migrations/control-flow-migration/index.js",
99100
],
101+
[
102+
"ngclass-to-class-migration",
103+
"packages/core/schematics/migrations/ngclass-to-class-migration/index.js",
104+
],
100105
[
101106
"router-current-navigation",
102107
"packages/core/schematics/migrations/router-current-navigation/index.js",
@@ -117,6 +122,7 @@ rollup.rollup(
117122
"//:node_modules/semver",
118123
"//packages/core/schematics:tsconfig_build",
119124
"//packages/core/schematics/migrations/control-flow-migration",
125+
"//packages/core/schematics/migrations/ngclass-to-class-migration",
120126
"//packages/core/schematics/migrations/router-current-navigation",
121127
"//packages/core/schematics/migrations/router-last-successful-navigation",
122128
"//packages/core/schematics/ng-generate/cleanup-unused-imports",

packages/core/schematics/collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
"factory": "./bundles/control-flow-migration.cjs#migrate",
5858
"schema": "./migrations/control-flow-migration/schema.json",
5959
"aliases": ["control-flow"]
60+
},
61+
"ngclass-to-class-migration": {
62+
"description": "Updates usages of `ngClass` directives to the `class` bindings where possible",
63+
"factory": "./bundles/ngclass-to-class-migration.cjs#migrate",
64+
"schema": "./migrations/ngclass-to-class-migration/schema.json",
65+
"aliases": ["ngclass-to-class"]
6066
}
6167
}
6268
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
load("//tools:defaults2.bzl", "copy_to_bin", "ts_project")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/test:__pkg__",
7+
],
8+
)
9+
10+
copy_to_bin(
11+
name = "static_files",
12+
srcs = ["schema.json"],
13+
)
14+
15+
ts_project(
16+
name = "ngclass-to-class-migration",
17+
srcs = glob(["**/*.ts"]),
18+
data = ["schema.json"],
19+
deps = [
20+
"//:node_modules/@types/node",
21+
"//:node_modules/typescript",
22+
"//packages/compiler",
23+
"//packages/compiler-cli",
24+
"//packages/compiler-cli/private",
25+
"//packages/compiler-cli/src/ngtsc/annotations",
26+
"//packages/compiler-cli/src/ngtsc/annotations/directive",
27+
"//packages/compiler-cli/src/ngtsc/file_system",
28+
"//packages/compiler-cli/src/ngtsc/imports",
29+
"//packages/compiler-cli/src/ngtsc/metadata",
30+
"//packages/compiler-cli/src/ngtsc/reflection",
31+
"//packages/core/schematics/utils",
32+
"//packages/core/schematics/utils/tsurge",
33+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
34+
],
35+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
# ngClass to class migration
3+
This schematic helps developers to convert ngClass directive usages to class bindings where possible.
4+
5+
## How to run this migration?
6+
The migration can be run using the following command:
7+
8+
```bash
9+
ng generate @angular/core:ngclass-to-class
10+
```
11+
12+
By default, the migration will go over the entire application. If you want to apply this migration to a subset of the files, you can pass the path argument as shown below:
13+
14+
```bash
15+
ng generate @angular/core:ngclass-to-class --path src/app/sub-component
16+
```
17+
18+
### How does it work?
19+
The schematic will attempt to find all the places in the templates where the component selectors are used. And check if they can be converted to self-closing tags.
20+
21+
Example:
22+
23+
```html
24+
<!-- Before -->
25+
<div [ngClass]="{admin: isAdmin, dense: density === 'high'}">
26+
27+
<!-- After -->
28+
<div [class]="{admin: isAdmin, dense: density === 'high'}">
29+
```
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.dev/license
7+
*/
8+
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {NgClassMigration} from './ngclass-to-class-migration';
11+
import {MigrationStage, runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
12+
13+
interface Options {
14+
path: string;
15+
analysisDir: string;
16+
migrateSpaceSeparatedKey?: boolean;
17+
}
18+
19+
export function migrate(options: Options): Rule {
20+
return async (tree, context) => {
21+
await runMigrationInDevkit({
22+
tree,
23+
getMigration: (fs) =>
24+
new NgClassMigration({
25+
migrateSpaceSeparatedKey: options.migrateSpaceSeparatedKey,
26+
shouldMigrate: (file) => {
27+
return (
28+
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
29+
!/(^|\/)node_modules\//.test(file.rootRelativePath)
30+
);
31+
},
32+
}),
33+
beforeProgramCreation: (tsconfigPath: string, stage: MigrationStage) => {
34+
if (stage === MigrationStage.Analysis) {
35+
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
36+
} else {
37+
context.logger.info(`Running migration for: ${tsconfigPath}...`);
38+
}
39+
},
40+
beforeUnitAnalysis: (tsconfigPath: string) => {
41+
context.logger.info(`Scanning for component tags: ${tsconfigPath}...`);
42+
},
43+
afterAllAnalyzed: () => {
44+
context.logger.info(``);
45+
context.logger.info(`Processing analysis data between targets...`);
46+
context.logger.info(``);
47+
},
48+
afterAnalysisFailure: () => {
49+
context.logger.error('Migration failed unexpectedly with no analysis data');
50+
},
51+
whenDone: ({
52+
touchedFilesCount,
53+
replacementCount,
54+
}: {
55+
touchedFilesCount: number;
56+
replacementCount: number;
57+
}) => {
58+
context.logger.info('');
59+
context.logger.info(`Successfully migrated to class from ngClass 🎉`);
60+
context.logger.info(
61+
` -> Migrated ${replacementCount} ngClass to class in ${touchedFilesCount} files.`,
62+
);
63+
},
64+
});
65+
};
66+
}

0 commit comments

Comments
 (0)