Skip to content

Commit 8f5124e

Browse files
fix(migrations): Add missing support for ngForOf (#52903)
This adds support to migrate ngForOf and ngForTrackBy when migrating control flow. PR Close #52903
1 parent ee892ee commit 8f5124e

File tree

6 files changed

+316
-14
lines changed

6 files changed

+316
-14
lines changed

packages/core/schematics/ng-generate/control-flow-migration/fors.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import {ElementCollector, ElementToMigrate, MigrateError, Result} from './types'
1212
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';
1313

1414
export const ngfor = '*ngFor';
15+
export const nakedngfor = 'ngFor';
16+
const fors = [
17+
ngfor,
18+
nakedngfor,
19+
];
20+
1521
export const commaSeparatedSyntax = new Map([
1622
['(', ')'],
1723
['{', '}'],
@@ -34,7 +40,7 @@ export function migrateFor(template: string): {migrated: string, errors: Migrate
3440
}
3541

3642
let result = template;
37-
const visitor = new ElementCollector([ngfor]);
43+
const visitor = new ElementCollector(fors);
3844
visitAll(visitor, parsed.rootNodes);
3945
calculateNesting(visitor, hasLineBreaks(template));
4046

@@ -65,8 +71,14 @@ export function migrateFor(template: string): {migrated: string, errors: Migrate
6571
return {migrated: result, errors};
6672
}
6773

68-
6974
function migrateNgFor(etm: ElementToMigrate, tmpl: string, offset: number): Result {
75+
if (etm.forAttrs !== undefined) {
76+
return migrateBoundNgFor(etm, tmpl, offset);
77+
}
78+
return migrateStandardNgFor(etm, tmpl, offset);
79+
}
80+
81+
function migrateStandardNgFor(etm: ElementToMigrate, tmpl: string, offset: number): Result {
7082
const aliasWithEqualRegexp = /=\s*(count|index|first|last|even|odd)/gm;
7183
const aliasWithAsRegexp = /(count|index|first|last|even|odd)\s+as/gm;
7284
const aliases = [];
@@ -136,6 +148,43 @@ function migrateNgFor(etm: ElementToMigrate, tmpl: string, offset: number): Resu
136148
return {tmpl: updatedTmpl, offsets: {pre, post}};
137149
}
138150

151+
function migrateBoundNgFor(etm: ElementToMigrate, tmpl: string, offset: number): Result {
152+
const forAttrs = etm.forAttrs!;
153+
const aliasMap = forAttrs.aliases;
154+
155+
const originals = getOriginals(etm, tmpl, offset);
156+
const condition = `${forAttrs.item} of ${forAttrs.forOf}`;
157+
158+
const aliases = [];
159+
let aliasedIndex = '$index';
160+
for (const [key, val] of aliasMap) {
161+
aliases.push(` let ${key.trim()} = $${val}`);
162+
if (val.trim() === 'index') {
163+
aliasedIndex = key;
164+
}
165+
}
166+
const aliasStr = (aliases.length > 0) ? `;${aliases.join(';')}` : '';
167+
168+
let trackBy = forAttrs.item;
169+
if (forAttrs.trackBy !== '') {
170+
// build trackby value
171+
trackBy = `${forAttrs.trackBy.trim()}(${aliasedIndex}, ${forAttrs.item})`;
172+
}
173+
174+
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
175+
const startBlock = `@for (${condition}; track ${trackBy}${aliasStr}) {\n ${start}`;
176+
177+
const endBlock = `${end}\n}`;
178+
const forBlock = startBlock + middle + endBlock;
179+
180+
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset));
181+
182+
const pre = originals.start.length - startBlock.length;
183+
const post = originals.end.length - endBlock.length;
184+
185+
return {tmpl: updatedTmpl, offsets: {pre, post}};
186+
}
187+
139188
function getNgForParts(expression: string): string[] {
140189
const parts: string[] = [];
141190
const commaSeparatedStack: string[] = [];

packages/core/schematics/ng-generate/control-flow-migration/ifs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const ngif = '*ngIf';
1515
export const boundngif = '[ngIf]';
1616
export const nakedngif = 'ngIf';
1717

18-
export const ifs = [
18+
const ifs = [
1919
ngif,
2020
nakedngif,
2121
boundngif,

packages/core/schematics/ng-generate/control-flow-migration/switches.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const nakedcase = 'ngSwitchCase';
1818
export const switchdefault = '*ngSwitchDefault';
1919
export const nakeddefault = 'ngSwitchDefault';
2020

21-
export const switches = [
21+
const switches = [
2222
ngswitch,
2323
boundcase,
2424
switchcase,

packages/core/schematics/ng-generate/control-flow-migration/types.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ts from 'typescript';
1212
export const ngtemplate = 'ng-template';
1313
export const boundngifelse = '[ngIfElse]';
1414
export const boundngifthenelse = '[ngIfThenElse]';
15+
export const nakedngfor = 'ngFor';
1516

1617
function allFormsOf(selector: string): string[] {
1718
return [
@@ -30,6 +31,10 @@ const commonModuleDirectives = new Set([
3031
...allFormsOf('ngStyle'),
3132
...allFormsOf('ngTemplateOutlet'),
3233
...allFormsOf('ngComponentOutlet'),
34+
'[NgForOf]',
35+
'[NgForTrackBy]',
36+
'[ngIfElse]',
37+
'[ngIfThenElse]',
3338
]);
3439

3540
function pipeMatchRegExpFor(name: string): RegExp {
@@ -72,6 +77,13 @@ export type Result = {
7277
offsets: Offsets,
7378
};
7479

80+
export interface ForAttributes {
81+
forOf: string;
82+
trackBy: string;
83+
item: string;
84+
aliases: Map<string, string>;
85+
}
86+
7587
/**
7688
* Represents an error that happened during migration
7789
*/
@@ -88,16 +100,18 @@ export class ElementToMigrate {
88100
attr: Attribute;
89101
elseAttr: Attribute|undefined;
90102
thenAttr: Attribute|undefined;
103+
forAttrs: ForAttributes|undefined;
91104
nestCount = 0;
92105
hasLineBreaks = false;
93106

94107
constructor(
95108
el: Element, attr: Attribute, elseAttr: Attribute|undefined = undefined,
96-
thenAttr: Attribute|undefined = undefined) {
109+
thenAttr: Attribute|undefined = undefined, forAttrs: ForAttributes|undefined = undefined) {
97110
this.el = el;
98111
this.attr = attr;
99112
this.elseAttr = elseAttr;
100113
this.thenAttr = thenAttr;
114+
this.forAttrs = forAttrs;
101115
}
102116

103117
getCondition(targetStr: string): string {
@@ -236,12 +250,38 @@ export class ElementCollector extends RecursiveVisitor {
236250
if (this._attributes.includes(attr.name)) {
237251
const elseAttr = el.attrs.find(x => x.name === boundngifelse);
238252
const thenAttr = el.attrs.find(x => x.name === boundngifthenelse);
239-
this.elements.push(new ElementToMigrate(el, attr, elseAttr, thenAttr));
253+
const forAttrs = attr.name === nakedngfor ? this.getForAttrs(el) : undefined;
254+
this.elements.push(new ElementToMigrate(el, attr, elseAttr, thenAttr, forAttrs));
240255
}
241256
}
242257
}
243258
super.visitElement(el, null);
244259
}
260+
261+
private getForAttrs(el: Element): ForAttributes {
262+
const aliases = new Map<string, string>();
263+
let item = '';
264+
let trackBy = '';
265+
let forOf = '';
266+
for (const attr of el.attrs) {
267+
if (attr.name === '[ngForTrackBy]') {
268+
trackBy = attr.value;
269+
}
270+
if (attr.name === '[ngForOf]') {
271+
forOf = attr.value;
272+
}
273+
if (attr.name.startsWith('let-')) {
274+
if (attr.value === '') {
275+
// item
276+
item = attr.name.replace('let-', '');
277+
} else {
278+
// alias
279+
aliases.set(attr.name.replace('let-', ''), attr.value);
280+
}
281+
}
282+
}
283+
return {forOf, trackBy, item, aliases};
284+
}
245285
}
246286

247287
/** Finds all elements with ngif structural directives. */

packages/core/schematics/ng-generate/control-flow-migration/util.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import ts from 'typescript';
1212

1313
import {AnalyzedFile, CommonCollector, ElementCollector, ElementToMigrate, Template, TemplateCollector} from './types';
1414

15-
const importRemovals = ['NgIf', 'NgFor', 'NgSwitch', 'NgSwitchCase', 'NgSwitchDefault'];
15+
const importRemovals = [
16+
'NgIf', 'NgIfElse', 'NgIfThenElse', 'NgFor', 'NgForOf', 'NgForTrackBy', 'NgSwitch',
17+
'NgSwitchCase', 'NgSwitchDefault'
18+
];
1619
const importWithCommonRemovals = [...importRemovals, 'CommonModule'];
1720

1821
/**
@@ -353,26 +356,34 @@ export function getOriginals(
353356
return {start, end: ''};
354357
}
355358

359+
function isI18nTemplate(etm: ElementToMigrate, i18nAttr: Attribute|undefined): boolean {
360+
return etm.el.name === 'ng-template' && i18nAttr !== undefined &&
361+
(etm.el.attrs.length === 2 || (etm.el.attrs.length === 3 && etm.elseAttr !== undefined));
362+
}
363+
364+
function isRemovableContainer(etm: ElementToMigrate): boolean {
365+
return (etm.el.name === 'ng-container' || etm.el.name === 'ng-template') &&
366+
(etm.el.attrs.length === 1 || etm.forAttrs !== undefined ||
367+
(etm.el.attrs.length === 2 && etm.elseAttr !== undefined) ||
368+
(etm.el.attrs.length === 3 && etm.elseAttr !== undefined && etm.thenAttr !== undefined));
369+
}
370+
356371
/**
357372
* builds the proper contents of what goes inside a given control flow block after migration
358373
*/
359374
export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
360375
{start: string, middle: string, end: string} {
361376
const i18nAttr = etm.el.attrs.find(x => x.name === 'i18n');
362-
if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') &&
363-
(etm.el.attrs.length === 1 || (etm.el.attrs.length === 2 && etm.elseAttr !== undefined) ||
364-
(etm.el.attrs.length === 3 && etm.elseAttr !== undefined && etm.thenAttr !== undefined))) {
377+
if (isRemovableContainer(etm)) {
365378
// this is the case where we're migrating and there's no need to keep the ng-container
366379
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
367380
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
368381
const middle = tmpl.slice(childStart, childEnd);
369382
return {start: '', middle, end: ''};
370-
} else if (
371-
etm.el.name === 'ng-template' && i18nAttr !== undefined &&
372-
(etm.el.attrs.length === 2 || (etm.el.attrs.length === 3 && etm.elseAttr !== undefined))) {
383+
} else if (isI18nTemplate(etm, i18nAttr)) {
373384
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
374385
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
375-
const middle = wrapIntoI18nContainer(i18nAttr, tmpl.slice(childStart, childEnd));
386+
const middle = wrapIntoI18nContainer(i18nAttr!, tmpl.slice(childStart, childEnd));
376387
return {start: '', middle, end: ''};
377388
}
378389

0 commit comments

Comments
 (0)