Skip to content

Commit de2bfc0

Browse files
aparziAndrewKushnir
authored andcommitted
fix(core): fix removal of a container reference used in the component file (angular#60210)
During migration a container reference was deleted even though it was used in the component file. PR Close angular#60210
1 parent a980ac9 commit de2bfc0

File tree

3 files changed

+159
-3
lines changed

3 files changed

+159
-3
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function migrateTemplate(
4949
return {migrated: template, errors: switchResult.errors};
5050
}
5151
const caseResult = migrateCase(switchResult.migrated);
52-
const templateResult = processNgTemplates(caseResult.migrated);
52+
const templateResult = processNgTemplates(caseResult.migrated, file.sourceFile);
5353
if (templateResult.err !== undefined) {
5454
return {migrated: template, errors: [{type: 'template', error: templateResult.err}]};
5555
}

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

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,10 @@ function generatei18nContainer(
466466
/**
467467
* Counts, replaces, and removes any necessary ng-templates post control flow migration
468468
*/
469-
export function processNgTemplates(template: string): {migrated: string; err: Error | undefined} {
469+
export function processNgTemplates(
470+
template: string,
471+
sourceFile: ts.SourceFile,
472+
): {migrated: string; err: Error | undefined} {
470473
// count usage
471474
try {
472475
const templates = getTemplates(template);
@@ -496,7 +499,17 @@ export function processNgTemplates(template: string): {migrated: string; err: Er
496499
}
497500
// the +1 accounts for the t.count's counting of the original template
498501
if (t.count === matches.length + 1 && safeToRemove) {
499-
template = template.replace(t.contents, `${startMarker}${endMarker}`);
502+
const refsInComponentFile = getViewChildOrViewChildrenNames(sourceFile);
503+
if (refsInComponentFile?.length > 0) {
504+
const templateRefs = getTemplateReferences(template);
505+
for (const ref of refsInComponentFile) {
506+
if (!templateRefs.includes(ref)) {
507+
template = template.replace(t.contents, `${startMarker}${endMarker}`);
508+
}
509+
}
510+
} else {
511+
template = template.replace(t.contents, `${startMarker}${endMarker}`);
512+
}
500513
}
501514
// templates may have changed structure from nested replaced templates
502515
// so we need to reprocess them before the next loop.
@@ -514,6 +527,53 @@ export function processNgTemplates(template: string): {migrated: string; err: Er
514527
}
515528
}
516529

530+
function getViewChildOrViewChildrenNames(sourceFile: ts.SourceFile): Array<string> {
531+
const names: Array<string> = [];
532+
533+
function visit(node: ts.Node) {
534+
if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
535+
const expr = node.expression;
536+
if (
537+
ts.isIdentifier(expr.expression) &&
538+
(expr.expression.text === 'ViewChild' || expr.expression.text === 'ViewChildren')
539+
) {
540+
const firstArg = expr.arguments[0];
541+
if (firstArg && ts.isStringLiteral(firstArg)) {
542+
names.push(firstArg.text);
543+
}
544+
return;
545+
}
546+
}
547+
ts.forEachChild(node, visit);
548+
}
549+
550+
visit(sourceFile);
551+
return names;
552+
}
553+
554+
function getTemplateReferences(template: string): string[] {
555+
const parsed = parseTemplate(template);
556+
if (parsed.tree === undefined) {
557+
return [];
558+
}
559+
560+
const references: string[] = [];
561+
562+
function visitNodes(nodes: any) {
563+
for (const node of nodes) {
564+
if (node?.name === 'ng-template') {
565+
references.push(...node.attrs?.map((ref: any) => ref?.name?.slice(1)));
566+
}
567+
if (node.children) {
568+
visitNodes(node.children);
569+
}
570+
}
571+
}
572+
573+
visitNodes(parsed.tree.rootNodes);
574+
return references;
575+
}
576+
517577
function replaceRemainingPlaceholders(template: string): string {
518578
const pattern = '.*';
519579
const placeholderPattern = getPlaceholder(pattern);

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6836,5 +6836,101 @@ describe('control flow migration', () => {
68366836
`"ng-container". Please fix and re-run the migration.`,
68376837
);
68386838
});
6839+
6840+
it('should not remove component reference it is used in component file with viewChild', async () => {
6841+
writeFile(
6842+
'/comp.ts',
6843+
`
6844+
import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
6845+
import { NgIf } from '@angular/common';
6846+
6847+
@Component({
6848+
standalone: true
6849+
imports: [NgIf],
6850+
template: \`<h1>Hello from {{ name }}!</h1>
6851+
<div *ngIf="showContent; then contentTemplate"></div>
6852+
<ng-template #contentTemplate><div>test content</div></ng-template>\`
6853+
})
6854+
class Comp {
6855+
@ViewChild('contentTemplate') testContainer!: TemplateRef<unknown>;
6856+
name = 'Angular';
6857+
showContent = true;
6858+
options: { value: string; html: any }[] = [];
6859+
6860+
constructor(private viewContainerRef: ViewContainerRef) {}
6861+
6862+
ngAfterViewInit(): void {
6863+
this.viewContainerRef.createEmbeddedView(this.testContainer);
6864+
}
6865+
}
6866+
`,
6867+
);
6868+
6869+
await runMigration();
6870+
const content = tree.readContent('/comp.ts');
6871+
expect(content).toContain('<ng-template #contentTemplate>');
6872+
});
6873+
6874+
it('should not remove component reference it is used in component file with viewChildren', async () => {
6875+
writeFile(
6876+
'/comp.ts',
6877+
`
6878+
import {Component, TemplateRef, ViewChildren, ViewContainerRef, QueryList} from '@angular/core';
6879+
import { NgIf } from '@angular/common';
6880+
6881+
@Component({
6882+
standalone: true
6883+
imports: [NgIf],
6884+
template: \`<h1>Hello from {{ name }}!</h1>
6885+
<div *ngIf="showContent; then contentTemplate"></div>
6886+
<ng-template #contentTemplate><div>test content</div></ng-template>\`
6887+
})
6888+
class Comp {
6889+
@ViewChildren('contentTemplate') testContainer!: QueryList<TemplateRef<unknown>>;
6890+
name = 'Angular';
6891+
showContent = true;
6892+
options: { value: string; html: any }[] = [];
6893+
6894+
constructor(private viewContainerRef: ViewContainerRef) {}
6895+
6896+
ngAfterViewInit(): void {
6897+
this.viewContainerRef.createEmbeddedView(this.testContainer.last);
6898+
}
6899+
}
6900+
`,
6901+
);
6902+
6903+
await runMigration();
6904+
const content = tree.readContent('/comp.ts');
6905+
expect(content).toContain('<ng-template #contentTemplate>');
6906+
});
6907+
6908+
it('should remove component reference when viewChild is commented in component file', async () => {
6909+
writeFile(
6910+
'/comp.ts',
6911+
`
6912+
import {Component, TemplateRef, ViewChild} from '@angular/core';
6913+
import { NgIf } from '@angular/common';
6914+
6915+
@Component({
6916+
standalone: true
6917+
imports: [NgIf],
6918+
template: \`<h1>Hello from {{ name }}!</h1>
6919+
<div *ngIf="showContent; then contentTemplate"></div>
6920+
<ng-template #contentTemplate><div>test content</div></ng-template>\`
6921+
})
6922+
class Comp {
6923+
// @ViewChild('contentTemplate') testContainer!: TemplateRef<unknown>;
6924+
name = 'Angular';
6925+
showContent = true;
6926+
options: { value: string; html: any }[] = [];
6927+
}
6928+
`,
6929+
);
6930+
6931+
await runMigration();
6932+
const content = tree.readContent('/comp.ts');
6933+
expect(content).not.toContain('<ng-template #contentTemplate>');
6934+
});
68396935
});
68406936
});

0 commit comments

Comments
 (0)