Skip to content

Commit 9d5f0f3

Browse files
authored
fix(material/schematics): replace pre-existing attribute values (#25754)
Fixes that the MDC migration schematic was always adding new attributes, instead of replacing the values of the ones that are already there.
1 parent cd591b5 commit 9d5f0f3

File tree

2 files changed

+45
-10
lines changed

2 files changed

+45
-10
lines changed

src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ describe('#visitElements', () => {
118118
});
119119
expect(html).toBe('<a attr2="val2" attr1="val1"></a>');
120120
});
121+
122+
it('should replace value of existing attribute', async () => {
123+
runAddAttributeTest('<a attr="default"></a>', '<a attr="val"></a>');
124+
});
125+
126+
it('should add value to existing attribute that does not have a value', async () => {
127+
runAddAttributeTest('<a attr></a>', '<a attr="val"></a>');
128+
});
121129
});
122130

123131
it('should match indentation', async () => {

src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import * as compiler from '@angular/compiler';
9+
import {
10+
ParsedTemplate,
11+
TmplAstElement,
12+
TmplAstNode,
13+
parseTemplate as parseTemplateUsingCompiler,
14+
} from '@angular/compiler';
1015

1116
/**
1217
* Traverses the given tree of nodes and runs the given callbacks for each Element node encountered.
@@ -20,14 +25,14 @@ import * as compiler from '@angular/compiler';
2025
* @param postorderCallback A function that gets run for each Element node in a postorder traversal.
2126
*/
2227
export function visitElements(
23-
nodes: compiler.TmplAstNode[],
24-
preorderCallback: (node: compiler.TmplAstElement) => void = () => {},
25-
postorderCallback: (node: compiler.TmplAstElement) => void = () => {},
28+
nodes: TmplAstNode[],
29+
preorderCallback: (node: TmplAstElement) => void = () => {},
30+
postorderCallback: (node: TmplAstElement) => void = () => {},
2631
): void {
2732
nodes.reverse();
2833
for (let i = 0; i < nodes.length; i++) {
2934
const node = nodes[i];
30-
if (node instanceof compiler.TmplAstElement) {
35+
if (node instanceof TmplAstElement) {
3136
preorderCallback(node);
3237
visitElements(node.children, preorderCallback, postorderCallback);
3338
postorderCallback(node);
@@ -45,8 +50,8 @@ export function visitElements(
4550
* @param filePath URL to use for source mapping of the parsed template
4651
* @returns the updated template html.
4752
*/
48-
export function parseTemplate(template: string, templateUrl: string = ''): compiler.ParsedTemplate {
49-
return compiler.parseTemplate(template, templateUrl, {
53+
export function parseTemplate(template: string, templateUrl: string = ''): ParsedTemplate {
54+
return parseTemplateUsingCompiler(template, templateUrl, {
5055
preserveWhitespaces: true,
5156
preserveLineEndings: true,
5257
leadingTriviaChars: [],
@@ -61,7 +66,7 @@ export function parseTemplate(template: string, templateUrl: string = ''): compi
6166
* @param tag A new tag name.
6267
* @returns an updated html document.
6368
*/
64-
export function replaceStartTag(html: string, node: compiler.TmplAstElement, tag: string): string {
69+
export function replaceStartTag(html: string, node: TmplAstElement, tag: string): string {
6570
return replaceAt(html, node.startSourceSpan.start.offset + 1, node.name, tag);
6671
}
6772

@@ -73,7 +78,7 @@ export function replaceStartTag(html: string, node: compiler.TmplAstElement, tag
7378
* @param tag A new tag name.
7479
* @returns an updated html document.
7580
*/
76-
export function replaceEndTag(html: string, node: compiler.TmplAstElement, tag: string): string {
81+
export function replaceEndTag(html: string, node: TmplAstElement, tag: string): string {
7782
if (!node.endSourceSpan) {
7883
return html;
7984
}
@@ -91,10 +96,32 @@ export function replaceEndTag(html: string, node: compiler.TmplAstElement, tag:
9196
*/
9297
export function addAttribute(
9398
html: string,
94-
node: compiler.TmplAstElement,
99+
node: TmplAstElement,
95100
name: string,
96101
value: string,
97102
): string {
103+
const existingAttr = node.attributes.find(currentAttr => currentAttr.name === name);
104+
105+
if (existingAttr) {
106+
// If the attribute has a value already, replace it.
107+
if (existingAttr.valueSpan) {
108+
return (
109+
html.slice(0, existingAttr.valueSpan.start.offset) +
110+
value +
111+
html.slice(existingAttr.valueSpan.end.offset)
112+
);
113+
} else if (existingAttr.keySpan) {
114+
// Otherwise add a value to a value-less attribute. Note that the `keySpan` null check is
115+
// only necessary for the compiler. Technically an attribute should always have a key.
116+
return (
117+
html.slice(0, existingAttr.keySpan.end.offset) +
118+
`="${value}"` +
119+
html.slice(existingAttr.keySpan.end.offset)
120+
);
121+
}
122+
}
123+
124+
// Otherwise insert a new attribute.
98125
const index = node.startSourceSpan.start.offset + node.name.length + 1;
99126
const prefix = html.slice(0, index);
100127
const suffix = html.slice(index);

0 commit comments

Comments
 (0)