Skip to content

Commit 825688f

Browse files
wagnermacielmmalerba
authored andcommittedJul 15, 2022
feat(material/schematics): add chips template migrator (#24601)
* feat(material/schematics): add chips template migrator * refactor some of the template migrator logic * impl ChipsTemplateMigrator and added it to list of migrators * remove unnecessary class vars from TemplateMigrator * unit tested
1 parent 4792672 commit 825688f

File tree

6 files changed

+237
-33
lines changed

6 files changed

+237
-33
lines changed
 

‎src/material/schematics/ng-generate/mdc-migration/rules/components/card/card-template.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,25 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {TmplAstElement} from '@angular/compiler';
9+
import * as compiler from '@angular/compiler';
1010
import {TemplateMigrator, Update} from '../../template-migrator';
11-
import {addAttribute} from '../../tree-traversal';
11+
import {addAttribute, visitElements} from '../../tree-traversal';
1212

1313
export class CardTemplateMigrator extends TemplateMigrator {
14-
component = 'card';
15-
tagName = 'mat-card';
14+
getUpdates(ast: compiler.ParsedTemplate): Update[] {
15+
const updates: Update[] = [];
1616

17-
getUpdates(node: TmplAstElement): Update[] {
18-
if (node.name !== this.tagName) {
19-
return [];
20-
}
17+
visitElements(ast.nodes, (node: compiler.TmplAstElement) => {
18+
if (node.name !== 'mat-card') {
19+
return;
20+
}
2121

22-
return [
23-
{
22+
updates.push({
2423
location: node.startSourceSpan.end,
2524
updateFn: html => addAttribute(html, node, 'appearance', 'outlined'),
26-
},
27-
];
25+
});
26+
});
27+
28+
return updates;
2829
}
2930
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {createTestApp, patchDevkitTreeToExposeTypeScript} from '@angular/cdk/schematics/testing';
2+
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
3+
import {createNewTestRunner, migrateComponents, TEMPLATE_FILE} from '../test-setup-helper';
4+
5+
describe('chips template migrator', () => {
6+
let runner: SchematicTestRunner;
7+
let cliAppTree: UnitTestTree;
8+
9+
async function runMigrationTest(oldFileContent: string, newFileContent: string) {
10+
cliAppTree.overwrite(TEMPLATE_FILE, oldFileContent);
11+
const tree = await migrateComponents(['chips'], runner, cliAppTree);
12+
expect(tree.readContent(TEMPLATE_FILE)).toBe(newFileContent);
13+
}
14+
15+
beforeEach(async () => {
16+
runner = createNewTestRunner();
17+
cliAppTree = patchDevkitTreeToExposeTypeScript(await createTestApp(runner));
18+
});
19+
20+
it('should not update other elements', async () => {
21+
await runMigrationTest('<mat-button></mat-button>', '<mat-button></mat-button>');
22+
});
23+
24+
it('should update list to listbox', async () => {
25+
await runMigrationTest(
26+
'<mat-chip-list></mat-chip-list>',
27+
'<mat-chip-listbox></mat-chip-listbox>',
28+
);
29+
});
30+
31+
it('should update list to grid if referenced by an input', async () => {
32+
await runMigrationTest(
33+
`
34+
<mat-chip-list #chipList>
35+
<input [matChipInputFor]="chipList">
36+
</mat-chip-list>
37+
`,
38+
`
39+
<mat-chip-grid #chipList>
40+
<input [matChipInputFor]="chipList">
41+
</mat-chip-grid>
42+
`,
43+
);
44+
});
45+
46+
it('should update mat-chip inside a listbox to option', async () => {
47+
await runMigrationTest(
48+
`
49+
<mat-chip-list>
50+
<mat-chip>One</mat-chip>
51+
<mat-chip>Two</mat-chip>
52+
<mat-chip>Three</mat-chip>
53+
</mat-chip-list>
54+
`,
55+
`
56+
<mat-chip-listbox>
57+
<mat-chip-option>One</mat-chip-option>
58+
<mat-chip-option>Two</mat-chip-option>
59+
<mat-chip-option>Three</mat-chip-option>
60+
</mat-chip-listbox>
61+
`,
62+
);
63+
});
64+
65+
it('should update mat-chip inside a grid to row', async () => {
66+
await runMigrationTest(
67+
`
68+
<mat-chip-list #chipList>
69+
<mat-chip>One</mat-chip>
70+
<mat-chip>Two</mat-chip>
71+
<mat-chip>Three</mat-chip>
72+
<input [matChipInputFor]="chipList">
73+
</mat-chip-list>
74+
`,
75+
`
76+
<mat-chip-grid #chipList>
77+
<mat-chip-row>One</mat-chip-row>
78+
<mat-chip-row>Two</mat-chip-row>
79+
<mat-chip-row>Three</mat-chip-row>
80+
<input [matChipInputFor]="chipList">
81+
</mat-chip-grid>
82+
`,
83+
);
84+
});
85+
86+
it('should update list to listbox correctly even if it has a ref', async () => {
87+
await runMigrationTest(
88+
'<mat-chip-list #chipList></mat-chip-list>',
89+
'<mat-chip-listbox #chipList></mat-chip-listbox>',
90+
);
91+
});
92+
93+
it('should update standalone chips', async () => {
94+
await runMigrationTest('<mat-chip></mat-chip>', '<mat-chip-option></mat-chip-option>');
95+
});
96+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 * as compiler from '@angular/compiler';
10+
import {TemplateMigrator, Update} from '../../template-migrator';
11+
import {replaceStartTag, replaceEndTag, visitElements} from '../../tree-traversal';
12+
13+
/** Stores a mat-chip-list with the mat-chip elements nested within it. */
14+
interface ChipMap {
15+
chipList: compiler.TmplAstElement;
16+
chips: compiler.TmplAstElement[];
17+
}
18+
19+
export class ChipsTemplateMigrator extends TemplateMigrator {
20+
/** Stores the mat-chip-list elements with their nested mat-chip elements. */
21+
chipMap?: ChipMap;
22+
23+
/** All of the ChipMaps found while parsing a template AST. */
24+
chipMaps: ChipMap[] = [];
25+
26+
/** Chips that are not nested within mat-chip elements. */
27+
standaloneChips: compiler.TmplAstElement[] = [];
28+
29+
/** Input elements that have matChipInputFor attributes. */
30+
chipInputs: compiler.TmplAstBoundAttribute[] = [];
31+
32+
getUpdates(ast: compiler.ParsedTemplate): Update[] {
33+
this._gatherDomData(ast);
34+
const updates: Update[] = [];
35+
this.chipMaps.forEach(chipMap => {
36+
if (this._isChipGrid(chipMap.chipList)) {
37+
updates.push(...this._buildUpdatesForChipMap(chipMap, 'mat-chip-grid', 'mat-chip-row'));
38+
return;
39+
}
40+
updates.push(...this._buildUpdatesForChipMap(chipMap, 'mat-chip-listbox', 'mat-chip-option'));
41+
});
42+
this.standaloneChips.forEach(chip => {
43+
updates.push(...this._buildTagUpdates(chip, 'mat-chip-option'));
44+
});
45+
return updates;
46+
}
47+
48+
/** Traverses the AST and stores all relevant DOM data needed for building updates. */
49+
private _gatherDomData(ast: compiler.ParsedTemplate): void {
50+
this.chipMap = undefined;
51+
this.chipMaps = [];
52+
this.standaloneChips = [];
53+
this.chipInputs = [];
54+
55+
visitElements(
56+
ast.nodes,
57+
(node: compiler.TmplAstElement) => {
58+
switch (node.name) {
59+
case 'input':
60+
this._handleInputNode(node);
61+
break;
62+
case 'mat-chip-list':
63+
this.chipMap = {chipList: node, chips: []};
64+
break;
65+
case 'mat-chip':
66+
this.chipMap ? this.chipMap.chips.push(node) : this.standaloneChips.push(node);
67+
}
68+
},
69+
(node: compiler.TmplAstElement) => {
70+
if (node.name === 'mat-chip-list') {
71+
this.chipMaps.push(this.chipMap!);
72+
this.chipMap = undefined;
73+
}
74+
},
75+
);
76+
}
77+
78+
/** Returns the mat-chip-list and mat-chip updates for the given ChipMap. */
79+
private _buildUpdatesForChipMap(
80+
chipMap: ChipMap,
81+
chipListTagName: string,
82+
chipTagName: string,
83+
): Update[] {
84+
const updates: Update[] = [];
85+
updates.push(...this._buildTagUpdates(chipMap.chipList, chipListTagName));
86+
chipMap.chips.forEach(chip => updates.push(...this._buildTagUpdates(chip, chipTagName)));
87+
return updates;
88+
}
89+
90+
/** Creates and returns the start and end tag updates for the given node. */
91+
private _buildTagUpdates(node: compiler.TmplAstElement, tagName: string): Update[] {
92+
return [
93+
{
94+
location: node.startSourceSpan.start,
95+
updateFn: html => replaceStartTag(html, node, tagName),
96+
},
97+
{
98+
location: node.endSourceSpan!.start,
99+
updateFn: html => replaceEndTag(html, node, tagName),
100+
},
101+
];
102+
}
103+
104+
/** Stores the given input node if it has a matChipInputFor attribute. */
105+
private _handleInputNode(node: compiler.TmplAstElement): void {
106+
node.inputs.forEach(attr => {
107+
if (attr.name === 'matChipInputFor') {
108+
this.chipInputs.push(attr);
109+
}
110+
});
111+
}
112+
113+
/** Returns true if the given mat-chip-list is referenced by any inputs. */
114+
private _isChipGrid(node: compiler.TmplAstElement): boolean {
115+
return node.references.some(ref => {
116+
return this.chipInputs.some(attr => {
117+
return ref.name === (attr.value as compiler.ASTWithSource).source;
118+
});
119+
});
120+
}
121+
}

‎src/material/schematics/ng-generate/mdc-migration/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {CardStylesMigrator} from './components/card/card-styles';
1616
import {CardTemplateMigrator} from './components/card/card-template';
1717
import {CheckboxStylesMigrator} from './components/checkbox/checkbox-styles';
1818
import {ChipsStylesMigrator} from './components/chips/chips-styles';
19+
import {ChipsTemplateMigrator} from './components/chips/chips-template';
1920
import {DialogStylesMigrator} from './components/dialog/dialog-styles';
2021
import {FormFieldStylesMigrator} from './components/form-field/form-field-styles';
2122
import {InputStylesMigrator} from './components/input/input-styles';
@@ -63,6 +64,7 @@ export const MIGRATORS: ComponentMigrator[] = [
6364
{
6465
component: 'chips',
6566
styles: new ChipsStylesMigrator(),
67+
template: new ChipsTemplateMigrator(),
6668
},
6769
{
6870
component: 'dialog',

‎src/material/schematics/ng-generate/mdc-migration/rules/template-migration.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Migration, ResolvedResource} from '@angular/cdk/schematics';
1010
import {SchematicContext} from '@angular-devkit/schematics';
11-
import {visitElements, parseTemplate} from './tree-traversal';
11+
import {parseTemplate} from './tree-traversal';
1212
import {ComponentMigrator} from '.';
1313
import {Update} from './template-migrator';
1414

@@ -18,16 +18,11 @@ export class TemplateMigration extends Migration<ComponentMigrator[], SchematicC
1818
override visitTemplate(template: ResolvedResource) {
1919
const ast = parseTemplate(template.content, template.filePath);
2020
const migrators = this.upgradeData.filter(m => m.template).map(m => m.template!);
21-
const updates: Update[] = [];
2221

23-
visitElements(ast.nodes, node => {
24-
for (let i = 0; i < migrators.length; i++) {
25-
updates.push(...migrators[i].getUpdates(node));
26-
}
27-
});
22+
const updates: Update[] = [];
23+
migrators.forEach(m => updates.push(...m.getUpdates(ast)));
2824

2925
updates.sort((a, b) => b.location.offset - a.location.offset);
30-
3126
updates.forEach(update => {
3227
template.content = update.updateFn(template.content);
3328
});

‎src/material/schematics/ng-generate/mdc-migration/rules/template-migrator.ts

+2-13
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@ export interface Update {
1818
}
1919

2020
export abstract class TemplateMigrator {
21-
/** The name of the component that this migration handles. */
22-
abstract component: string;
23-
24-
/** The tag name to be updated in the template. */
25-
abstract tagName: string;
26-
27-
/**
28-
* Returns the data needed to update the given node.
29-
*
30-
* @param node A template ast element.
31-
* @returns The data needed to update this node.
32-
*/
33-
abstract getUpdates(node: compiler.TmplAstElement): Update[];
21+
/** Returns the data needed to update the given node. */
22+
abstract getUpdates(ast: compiler.ParsedTemplate): Update[];
3423
}

0 commit comments

Comments
 (0)