Skip to content

Commit

Permalink
feat(material/schematics): add chips template migrator (#24601)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wagnermaciel authored and mmalerba committed Jul 15, 2022
1 parent 4792672 commit 825688f
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 33 deletions.
Expand Up @@ -6,24 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/

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

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

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

return [
{
updates.push({
location: node.startSourceSpan.end,
updateFn: html => addAttribute(html, node, 'appearance', 'outlined'),
},
];
});
});

return updates;
}
}
@@ -0,0 +1,96 @@
import {createTestApp, patchDevkitTreeToExposeTypeScript} from '@angular/cdk/schematics/testing';
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import {createNewTestRunner, migrateComponents, TEMPLATE_FILE} from '../test-setup-helper';

describe('chips template migrator', () => {
let runner: SchematicTestRunner;
let cliAppTree: UnitTestTree;

async function runMigrationTest(oldFileContent: string, newFileContent: string) {
cliAppTree.overwrite(TEMPLATE_FILE, oldFileContent);
const tree = await migrateComponents(['chips'], runner, cliAppTree);
expect(tree.readContent(TEMPLATE_FILE)).toBe(newFileContent);
}

beforeEach(async () => {
runner = createNewTestRunner();
cliAppTree = patchDevkitTreeToExposeTypeScript(await createTestApp(runner));
});

it('should not update other elements', async () => {
await runMigrationTest('<mat-button></mat-button>', '<mat-button></mat-button>');
});

it('should update list to listbox', async () => {
await runMigrationTest(
'<mat-chip-list></mat-chip-list>',
'<mat-chip-listbox></mat-chip-listbox>',
);
});

it('should update list to grid if referenced by an input', async () => {
await runMigrationTest(
`
<mat-chip-list #chipList>
<input [matChipInputFor]="chipList">
</mat-chip-list>
`,
`
<mat-chip-grid #chipList>
<input [matChipInputFor]="chipList">
</mat-chip-grid>
`,
);
});

it('should update mat-chip inside a listbox to option', async () => {
await runMigrationTest(
`
<mat-chip-list>
<mat-chip>One</mat-chip>
<mat-chip>Two</mat-chip>
<mat-chip>Three</mat-chip>
</mat-chip-list>
`,
`
<mat-chip-listbox>
<mat-chip-option>One</mat-chip-option>
<mat-chip-option>Two</mat-chip-option>
<mat-chip-option>Three</mat-chip-option>
</mat-chip-listbox>
`,
);
});

it('should update mat-chip inside a grid to row', async () => {
await runMigrationTest(
`
<mat-chip-list #chipList>
<mat-chip>One</mat-chip>
<mat-chip>Two</mat-chip>
<mat-chip>Three</mat-chip>
<input [matChipInputFor]="chipList">
</mat-chip-list>
`,
`
<mat-chip-grid #chipList>
<mat-chip-row>One</mat-chip-row>
<mat-chip-row>Two</mat-chip-row>
<mat-chip-row>Three</mat-chip-row>
<input [matChipInputFor]="chipList">
</mat-chip-grid>
`,
);
});

it('should update list to listbox correctly even if it has a ref', async () => {
await runMigrationTest(
'<mat-chip-list #chipList></mat-chip-list>',
'<mat-chip-listbox #chipList></mat-chip-listbox>',
);
});

it('should update standalone chips', async () => {
await runMigrationTest('<mat-chip></mat-chip>', '<mat-chip-option></mat-chip-option>');
});
});
@@ -0,0 +1,121 @@
/**
* @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 * as compiler from '@angular/compiler';
import {TemplateMigrator, Update} from '../../template-migrator';
import {replaceStartTag, replaceEndTag, visitElements} from '../../tree-traversal';

/** Stores a mat-chip-list with the mat-chip elements nested within it. */
interface ChipMap {
chipList: compiler.TmplAstElement;
chips: compiler.TmplAstElement[];
}

export class ChipsTemplateMigrator extends TemplateMigrator {
/** Stores the mat-chip-list elements with their nested mat-chip elements. */
chipMap?: ChipMap;

/** All of the ChipMaps found while parsing a template AST. */
chipMaps: ChipMap[] = [];

/** Chips that are not nested within mat-chip elements. */
standaloneChips: compiler.TmplAstElement[] = [];

/** Input elements that have matChipInputFor attributes. */
chipInputs: compiler.TmplAstBoundAttribute[] = [];

getUpdates(ast: compiler.ParsedTemplate): Update[] {
this._gatherDomData(ast);
const updates: Update[] = [];
this.chipMaps.forEach(chipMap => {
if (this._isChipGrid(chipMap.chipList)) {
updates.push(...this._buildUpdatesForChipMap(chipMap, 'mat-chip-grid', 'mat-chip-row'));
return;
}
updates.push(...this._buildUpdatesForChipMap(chipMap, 'mat-chip-listbox', 'mat-chip-option'));
});
this.standaloneChips.forEach(chip => {
updates.push(...this._buildTagUpdates(chip, 'mat-chip-option'));
});
return updates;
}

/** Traverses the AST and stores all relevant DOM data needed for building updates. */
private _gatherDomData(ast: compiler.ParsedTemplate): void {
this.chipMap = undefined;
this.chipMaps = [];
this.standaloneChips = [];
this.chipInputs = [];

visitElements(
ast.nodes,
(node: compiler.TmplAstElement) => {
switch (node.name) {
case 'input':
this._handleInputNode(node);
break;
case 'mat-chip-list':
this.chipMap = {chipList: node, chips: []};
break;
case 'mat-chip':
this.chipMap ? this.chipMap.chips.push(node) : this.standaloneChips.push(node);
}
},
(node: compiler.TmplAstElement) => {
if (node.name === 'mat-chip-list') {
this.chipMaps.push(this.chipMap!);
this.chipMap = undefined;
}
},
);
}

/** Returns the mat-chip-list and mat-chip updates for the given ChipMap. */
private _buildUpdatesForChipMap(
chipMap: ChipMap,
chipListTagName: string,
chipTagName: string,
): Update[] {
const updates: Update[] = [];
updates.push(...this._buildTagUpdates(chipMap.chipList, chipListTagName));
chipMap.chips.forEach(chip => updates.push(...this._buildTagUpdates(chip, chipTagName)));
return updates;
}

/** Creates and returns the start and end tag updates for the given node. */
private _buildTagUpdates(node: compiler.TmplAstElement, tagName: string): Update[] {
return [
{
location: node.startSourceSpan.start,
updateFn: html => replaceStartTag(html, node, tagName),
},
{
location: node.endSourceSpan!.start,
updateFn: html => replaceEndTag(html, node, tagName),
},
];
}

/** Stores the given input node if it has a matChipInputFor attribute. */
private _handleInputNode(node: compiler.TmplAstElement): void {
node.inputs.forEach(attr => {
if (attr.name === 'matChipInputFor') {
this.chipInputs.push(attr);
}
});
}

/** Returns true if the given mat-chip-list is referenced by any inputs. */
private _isChipGrid(node: compiler.TmplAstElement): boolean {
return node.references.some(ref => {
return this.chipInputs.some(attr => {
return ref.name === (attr.value as compiler.ASTWithSource).source;
});
});
}
}
Expand Up @@ -16,6 +16,7 @@ import {CardStylesMigrator} from './components/card/card-styles';
import {CardTemplateMigrator} from './components/card/card-template';
import {CheckboxStylesMigrator} from './components/checkbox/checkbox-styles';
import {ChipsStylesMigrator} from './components/chips/chips-styles';
import {ChipsTemplateMigrator} from './components/chips/chips-template';
import {DialogStylesMigrator} from './components/dialog/dialog-styles';
import {FormFieldStylesMigrator} from './components/form-field/form-field-styles';
import {InputStylesMigrator} from './components/input/input-styles';
Expand Down Expand Up @@ -63,6 +64,7 @@ export const MIGRATORS: ComponentMigrator[] = [
{
component: 'chips',
styles: new ChipsStylesMigrator(),
template: new ChipsTemplateMigrator(),
},
{
component: 'dialog',
Expand Down
Expand Up @@ -8,7 +8,7 @@

import {Migration, ResolvedResource} from '@angular/cdk/schematics';
import {SchematicContext} from '@angular-devkit/schematics';
import {visitElements, parseTemplate} from './tree-traversal';
import {parseTemplate} from './tree-traversal';
import {ComponentMigrator} from '.';
import {Update} from './template-migrator';

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

visitElements(ast.nodes, node => {
for (let i = 0; i < migrators.length; i++) {
updates.push(...migrators[i].getUpdates(node));
}
});
const updates: Update[] = [];
migrators.forEach(m => updates.push(...m.getUpdates(ast)));

updates.sort((a, b) => b.location.offset - a.location.offset);

updates.forEach(update => {
template.content = update.updateFn(template.content);
});
Expand Down
Expand Up @@ -18,17 +18,6 @@ export interface Update {
}

export abstract class TemplateMigrator {
/** The name of the component that this migration handles. */
abstract component: string;

/** The tag name to be updated in the template. */
abstract tagName: string;

/**
* Returns the data needed to update the given node.
*
* @param node A template ast element.
* @returns The data needed to update this node.
*/
abstract getUpdates(node: compiler.TmplAstElement): Update[];
/** Returns the data needed to update the given node. */
abstract getUpdates(ast: compiler.ParsedTemplate): Update[];
}

0 comments on commit 825688f

Please sign in to comment.