Skip to content

Commit

Permalink
fixup! feat(material/schematics): add chips template migrator
Browse files Browse the repository at this point in the history
  • Loading branch information
wagnermaciel committed Mar 29, 2022
1 parent 6a726f3 commit 5a3f7a0
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/

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

export class CardTemplateMigrator extends TemplateMigrator {
override preorder(node: TmplAstElement): void {
if (node.name !== 'mat-card') {
return;
}
getUpdates(ast: compiler.ParsedTemplate): Update[] {
const updates: Update[] = [];

this.updates.push({
location: node.startSourceSpan.end,
updateFn: html => addAttribute(html, node, 'appearance', 'outlined'),
visitElements(ast.nodes, (node: compiler.TmplAstElement) => {
if (node.name !== 'mat-card') {
return;
}

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

return updates;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,8 @@ describe('chips template migrator', () => {
'<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>');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

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

/** Stores a mat-chip-list with the mat-chip elements nested within it. */
interface ChipMap {
Expand All @@ -17,144 +17,108 @@ interface ChipMap {
}

export class ChipsTemplateMigrator extends TemplateMigrator {
/** The matChipInputFor attributes found while traversing the template ast. */
private _chipInputAttrs: compiler.TmplAstBoundAttribute[] = [];
/** Stores the mat-chip-list elements with their nested mat-chip elements. */
chipMap?: ChipMap;

/** The current ChipMap being built. */
private _chipMap: ChipMap | null;
/** All of the ChipMaps found while parsing a template AST. */
chipMaps: ChipMap[] = [];

/** Contains all of the ChipMaps found while traversing the template ast. */
private _chipMaps: ChipMap[] = [];
/** Chips that are not nested within mat-chip elements. */
standaloneChips: compiler.TmplAstElement[] = [];

override postorder(node: compiler.TmplAstElement): void {
if (node.name === 'mat-chip-list') {
this._chipMaps.push(this._chipMap!);
this._chipMap = null;
}
}

override preorder(node: compiler.TmplAstElement): void {
switch (node.name) {
case 'mat-chip-list':
this._handleChipListNode(node);
break;
case 'mat-chip':
this._handleChipNode(node);
break;
case 'input':
this._storeChipRefs(node);
break;
}
}
/** Input elements that have matChipInputFor attributes. */
chipInputs: compiler.TmplAstBoundAttribute[] = [];

override getUpdates(): Update[] {
this._chipMaps.forEach(chipMap => {
chipMap.chips.forEach(chip => {
this.updates.push(
...this._buildUpdates(chip, chipMap.chipList, 'mat-chip-row', 'mat-chip-option'),
);
});
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'));
});

return this.updates;
}

override reset(): void {
super.reset();
this._chipInputAttrs = [];
this._chipMaps = [];
}

private _handleChipListNode(node: compiler.TmplAstElement): void {
this._chipMap = {
chipList: node,
chips: [],
};

this.updates.push(...this._buildUpdates(node, node, 'mat-chip-grid', 'mat-chip-listbox'));
this.standaloneChips.forEach(chip => {
updates.push(...this._buildTagUpdates(chip, 'mat-chip-option'));
});
this._clearDomData();
return updates;
}

private _handleChipNode(node: compiler.TmplAstElement): void {
if (this._chipMap) {
this._chipMap.chips.push(node);
return;
}

this.updates.push(
{
location: node.startSourceSpan.start,
updateFn: html => replaceStartTag(html, node, 'mat-chip-option'),
/** Traverses the AST and stores all relevant DOM data needed for building updates. */
private _gatherDomData(ast: compiler.ParsedTemplate): void {
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);
}
},
{
location: node.startSourceSpan.start,
updateFn: html => replaceEndTag(html, node, 'mat-chip-option'),
(node: compiler.TmplAstElement) => {
if (node.name === 'mat-chip-list') {
this.chipMaps.push(this.chipMap!);
this.chipMap = undefined;
}
},
);
}

/**
* Returns start and end tag updates for the given node.
*
* The updates returned are conditionally determined by whether the given chipListNode is going
* to become a mat-chip-grid or a mat-chip-listbox.
*
* @param node The node to update.
* @param chipListNode The node to evaluate as either a grid or listbox.
* @param gridTag The new tag name if the chipListNode is a grid.
* @param listboxTag The new tag name if the chipListNode is a listbox.
* @returns The start and end tag updates for the given node.
*/
private _buildUpdates(
node: compiler.TmplAstElement,
chipListNode: compiler.TmplAstElement,
gridTag: string,
listboxTag: string,
/** Clears the stored data to allow the same migrator to be used multiple times. */
private _clearDomData(): void {
this.chipMap = undefined;
this.chipMaps = [];
this.standaloneChips = [];
this.chipInputs = [];
}

/** 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 => {
return this._isChipGrid(chipListNode)
? replaceStartTag(html, node, gridTag)
: replaceStartTag(html, node, listboxTag);
},
updateFn: html => replaceStartTag(html, node, tagName),
},
{
location: node.endSourceSpan!.start,
updateFn: html => {
return this._isChipGrid(chipListNode)
? replaceEndTag(html, node, gridTag)
: replaceEndTag(html, node, listboxTag);
},
updateFn: html => replaceEndTag(html, node, tagName),
},
];
}

/** Stores the matChipInputFor references on given input. */
private _storeChipRefs(node: compiler.TmplAstElement): void {
for (let i = 0; i < node.inputs.length; i++) {
if (node.inputs[i].name === 'matChipInputFor') {
this._chipInputAttrs.push(node.inputs[i]);
return;
/** 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 whether the given node should be a mat-chip-grid or mat-chip-listbox.
*
* This is determined by whether the given mat-chip-list is referenced by any inputs. If it is,
* then the node is a mat-chip-grid. Otherwise, it is a mat-chip-listbox.
*
* IMPORTANT: This function should only be used in an updateFn callback. This function assumes
* the entire tree has already been traversed and all matChipInputFor attributes have been
* found and stored.
*/
/** 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._chipInputAttrs.some(attr => {
const value = attr.value as compiler.ASTWithSource;
return value.source === ref.name;
return this.chipInputs.some(attr => {
return ref.name === (attr.value as compiler.ASTWithSource).source;
});
});
}
Expand Down
Original file line number Diff line number Diff line change
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,21 +18,15 @@ 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 => migrators.forEach(m => m.preorder(node)),
node => migrators.forEach(m => m.postorder(node)),
);
const updates: Update[] = [];
migrators.forEach(m => updates.push(...m.getUpdates(ast)));

migrators.forEach(m => updates.push(...m.getUpdates()));
updates.sort((a, b) => b.location.offset - a.location.offset);
updates.forEach(update => {
template.content = update.updateFn(template.content);
});

migrators.forEach(m => m.reset());
this.fileSystem.overwrite(template.filePath, template.content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,6 @@ export interface Update {
}

export abstract class TemplateMigrator {
/** Stores template updates. By default gets returned by #getUpdates. */
protected updates: Update[] = [];

/** Preorder callback hook for the template AST traversal. */
preorder(node: compiler.TmplAstElement): void {}

/** Postorder callback hook for the template AST traversal. */
postorder(node: compiler.TmplAstElement): void {}

/** Returns the data needed to update the given node. */
getUpdates(): Update[] {
return this.updates;
}

/** Runs once a template has finished being updated. */
reset(): void {
this.updates = [];
}
abstract getUpdates(ast: compiler.ParsedTemplate): Update[];
}

0 comments on commit 5a3f7a0

Please sign in to comment.