Skip to content

Commit 3ebaecc

Browse files
aparzithePunderWoman
authored andcommitted
fix(migrations): handle reused templates in control flow migration (#63996)
The control flow migration was incorrectly removing `ng-template` elements in scenarios where they were referenced by multiple `*ngIf` directives' `else` clauses and also used independently via `ngTemplateOutlet`. PR Close #63996
1 parent 3c14d05 commit 3ebaecc

File tree

2 files changed

+128
-40
lines changed

2 files changed

+128
-40
lines changed

packages/core/schematics/migrations/control-flow-migration/util.ts

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ const endMarkerRegex = new RegExp(endMarker, 'gm');
3333
const startI18nMarkerRegex = new RegExp(startI18nMarker, 'gm');
3434
const endI18nMarkerRegex = new RegExp(endI18nMarker, 'gm');
3535
const replaceMarkerRegex = new RegExp(`${startMarker}|${endMarker}`, 'gm');
36+
const PRIORITY_WEIGHT_TEMPLATE_REFERENCE_BY_OUTLET = 2;
37+
38+
interface TemplateUsageResult {
39+
isReferencedInTemplateOutlet: boolean;
40+
totalCount: number;
41+
}
3642

3743
/**
3844
* Analyzes a source file to find file that need to be migrated and the text ranges within them.
@@ -430,39 +436,6 @@ export function getTemplates(template: string): Map<string, Template> {
430436
return new Map<string, Template>();
431437
}
432438

433-
function countTemplateUsage(nodes: any[], templateName: string): number {
434-
let count = 0;
435-
let isReferencedInTemplateOutlet = false;
436-
437-
for (const node of nodes) {
438-
if (node.attrs) {
439-
for (const attr of node.attrs) {
440-
if (attr.name === '*ngTemplateOutlet' && attr.value === templateName.slice(1)) {
441-
isReferencedInTemplateOutlet = true;
442-
break;
443-
}
444-
445-
if (attr.name.trim() === templateName) {
446-
count++;
447-
}
448-
}
449-
}
450-
451-
if (node.children) {
452-
if (node.name === 'for') {
453-
for (const child of node.children) {
454-
if (child.value?.includes(templateName.slice(1))) {
455-
count++;
456-
}
457-
}
458-
}
459-
count += countTemplateUsage(node.children, templateName);
460-
}
461-
}
462-
463-
return isReferencedInTemplateOutlet ? count + 2 : count;
464-
}
465-
466439
export function updateTemplates(
467440
template: string,
468441
templates: Map<string, Template>,
@@ -499,9 +472,13 @@ export function processNgTemplates(
499472
const templates = getTemplates(template);
500473

501474
// swap placeholders and remove
502-
for (const [name, t] of templates) {
503-
const replaceRegex = new RegExp(getPlaceholder(name.slice(1)), 'g');
504-
const forRegex = new RegExp(getPlaceholder(name.slice(1), PlaceholderKind.Alternate), 'g');
475+
for (const [nameWithHash, t] of templates) {
476+
const name = nameWithHash.slice(1);
477+
const replaceRegex = new RegExp(getPlaceholder(name), 'g');
478+
const forRegex = new RegExp(
479+
getPlaceholder(nameWithHash.slice(1), PlaceholderKind.Alternate),
480+
'g',
481+
);
505482
const forMatches = [...template.matchAll(forRegex)];
506483
const matches = [...forMatches, ...template.matchAll(replaceRegex)];
507484
let safeToRemove = true;
@@ -526,7 +503,15 @@ export function processNgTemplates(
526503
(obj, index, self) => index === self.findIndex((t) => t.input === obj.input),
527504
);
528505

529-
if ((t.count === dist.length || t.count - matches.length === 1) && safeToRemove) {
506+
// Check if template is used by ngTemplateOutlet in addition to control flow
507+
const hasTemplateOutletUsage = checkForTemplateOutletUsage(template, nameWithHash.slice(1));
508+
509+
// Only remove template if it's safe to do so AND not used by ngTemplateOutlet
510+
if (
511+
(t.count === dist.length || t.count - matches.length === 1) &&
512+
safeToRemove &&
513+
!hasTemplateOutletUsage
514+
) {
530515
const refsInComponentFile = getViewChildOrViewChildrenNames(sourceFile);
531516
if (refsInComponentFile?.length > 0) {
532517
const templateRefs = getTemplateReferences(template);
@@ -555,6 +540,71 @@ export function processNgTemplates(
555540
}
556541
}
557542

543+
function analyzeTemplateUsage(nodes: any[], templateName: string): TemplateUsageResult {
544+
let count = 0;
545+
let isReferencedInTemplateOutlet = false;
546+
const templateNameWithHash = `#${templateName}`;
547+
548+
function traverseNodes(nodeList: any[]): void {
549+
for (const node of nodeList) {
550+
if (node.attrs) {
551+
for (const attr of node.attrs) {
552+
if (
553+
(attr.name === '*ngTemplateOutlet' || attr.name === '[ngTemplateOutlet]') &&
554+
attr.value === templateName
555+
) {
556+
isReferencedInTemplateOutlet = true;
557+
}
558+
559+
if (attr.name.trim() === templateNameWithHash) {
560+
count++;
561+
}
562+
}
563+
}
564+
565+
if (node.children) {
566+
if (node.name === 'for') {
567+
for (const child of node.children) {
568+
if (child.value?.includes(templateName)) {
569+
count++;
570+
}
571+
}
572+
}
573+
574+
traverseNodes(node.children);
575+
}
576+
}
577+
}
578+
579+
traverseNodes(nodes);
580+
581+
return {
582+
isReferencedInTemplateOutlet,
583+
totalCount: isReferencedInTemplateOutlet
584+
? count + PRIORITY_WEIGHT_TEMPLATE_REFERENCE_BY_OUTLET
585+
: count,
586+
};
587+
}
588+
589+
/**
590+
* Checks if a template is used by ngTemplateOutlet directive
591+
*/
592+
function checkForTemplateOutletUsage(template: string, templateName: string): boolean {
593+
const parsed = parseTemplate(template);
594+
if (parsed.tree === undefined) {
595+
return false;
596+
}
597+
598+
const result = analyzeTemplateUsage(parsed.tree.rootNodes, templateName);
599+
return result.isReferencedInTemplateOutlet;
600+
}
601+
602+
function countTemplateUsage(nodes: any[], templateNameWithHash: string): number {
603+
const templateName = templateNameWithHash.slice(1);
604+
const result = analyzeTemplateUsage(nodes, templateName);
605+
return result.totalCount;
606+
}
607+
558608
function getViewChildOrViewChildrenNames(sourceFile: ts.SourceFile): Array<string> {
559609
const names: Array<string> = [];
560610

@@ -585,12 +635,12 @@ function getTemplateReferences(template: string): string[] {
585635
return [];
586636
}
587637

588-
const references: string[] = [];
638+
const templateNameRefWithoutHash: string[] = [];
589639

590640
function visitNodes(nodes: any) {
591641
for (const node of nodes) {
592642
if (node?.name === 'ng-template') {
593-
references.push(...node.attrs?.map((ref: any) => ref?.name?.slice(1)));
643+
templateNameRefWithoutHash.push(...node.attrs?.map((ref: any) => ref?.name?.slice(1)));
594644
}
595645
if (node.children) {
596646
visitNodes(node.children);
@@ -599,7 +649,7 @@ function getTemplateReferences(template: string): string[] {
599649
}
600650

601651
visitNodes(parsed.tree.rootNodes);
602-
return references;
652+
return templateNameRefWithoutHash;
603653
}
604654

605655
function replaceRemainingPlaceholders(template: string): string {

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5074,6 +5074,44 @@ describe('control flow migration (ng update)', () => {
50745074
'@if (show) {<div>Some greek characters: θδ!</div>}',
50755075
);
50765076
});
5077+
5078+
it('should migrate multiple ngIf directives with same else template and preserve template outlet', async () => {
5079+
writeFile(
5080+
'/comp.ts',
5081+
`
5082+
import {Component} from '@angular/core';
5083+
import {NgIf} from '@angular/common';
5084+
5085+
@Component({
5086+
imports: [NgIf],
5087+
template: \`
5088+
<div *ngIf="1 == 1; else elseTemplate">
5089+
<h1>TEST</h1>
5090+
</div>
5091+
<div *ngIf="1 == 1; else elseTemplate">
5092+
<h1>TEST</h1>
5093+
</div>
5094+
5095+
<ng-container [ngTemplateOutlet]="elseTemplate"></ng-container>
5096+
<ng-template #elseTemplate>
5097+
<h1>Test</h1>
5098+
<div>Test</div>
5099+
</ng-template>
5100+
\`
5101+
})
5102+
class Comp {
5103+
}
5104+
`,
5105+
);
5106+
5107+
await runMigration();
5108+
const content = tree.readContent('/comp.ts');
5109+
5110+
expect(content.replace(/\s+/g, ' ')).toContain(
5111+
`<ng-container [ngTemplateOutlet]="elseTemplate"></ng-container>`,
5112+
);
5113+
expect(content.replace(/\s+/g, ' ')).toContain(`<ng-template #elseTemplate>`);
5114+
});
50775115
});
50785116

50795117
describe('formatting', () => {

0 commit comments

Comments
 (0)