mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(migrations): Add support for ng-templates with i18n attributes (#52597)
This makes sure that i18n attributes are preserved on ng-templates being removed during the migration. fixes: #52517 PR Close #52597
This commit is contained in:
committed by
Andrew Scott
parent
6240613477
commit
70d30c28e0
@@ -80,9 +80,11 @@ export class Template {
|
||||
count: number = 0;
|
||||
contents: string = '';
|
||||
children: string = '';
|
||||
i18n: Attribute|null = null;
|
||||
|
||||
constructor(el: Element) {
|
||||
constructor(el: Element, i18n: Attribute|null) {
|
||||
this.el = el;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
generateContents(tmpl: string) {
|
||||
@@ -155,11 +157,19 @@ export class TemplateCollector extends RecursiveVisitor {
|
||||
|
||||
override visitElement(el: Element): void {
|
||||
if (el.name === ngtemplate) {
|
||||
let i18n = null;
|
||||
let templateAttr = null;
|
||||
for (const attr of el.attrs) {
|
||||
if (attr.name.startsWith('#')) {
|
||||
this.elements.push(new ElementToMigrate(el, attr));
|
||||
this.templates.set(attr.name, new Template(el));
|
||||
if (attr.name === 'i18n') {
|
||||
i18n = attr;
|
||||
}
|
||||
if (attr.name.startsWith('#')) {
|
||||
templateAttr = attr;
|
||||
}
|
||||
}
|
||||
if (templateAttr !== null) {
|
||||
this.elements.push(new ElementToMigrate(el, templateAttr));
|
||||
this.templates.set(templateAttr.name, new Template(el, i18n));
|
||||
}
|
||||
}
|
||||
super.visitElement(el, null);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {HtmlParser, ParseTreeResult, visitAll} from '@angular/compiler';
|
||||
import {Attribute, HtmlParser, ParseTreeResult, visitAll} from '@angular/compiler';
|
||||
import {dirname, join} from 'path';
|
||||
import ts from 'typescript';
|
||||
|
||||
@@ -173,6 +173,11 @@ export function countTemplateUsage(template: string): Map<string, Template> {
|
||||
return new Map<string, Template>();
|
||||
}
|
||||
|
||||
function wrapIntoI18nContainer(i18nAttr: Attribute, content: string) {
|
||||
const i18n = i18nAttr.value === '' ? 'i18n' : `i18n="${i18nAttr.value}"`;
|
||||
return `<ng-container ${i18n}>${content}</ng-container>`;
|
||||
}
|
||||
|
||||
export function processNgTemplates(template: string): string {
|
||||
// count usage
|
||||
const templates = countTemplateUsage(template);
|
||||
@@ -182,7 +187,12 @@ export function processNgTemplates(template: string): string {
|
||||
const placeholder = `${name}|`;
|
||||
|
||||
if (template.indexOf(placeholder) > -1) {
|
||||
template = template.replace(placeholder, t.children);
|
||||
if (t.i18n !== null) {
|
||||
const container = wrapIntoI18nContainer(t.i18n, t.children);
|
||||
template = template.replace(placeholder, container);
|
||||
} else {
|
||||
template = template.replace(placeholder, t.children);
|
||||
}
|
||||
if (t.count <= 2) {
|
||||
template = template.replace(t.contents, '');
|
||||
}
|
||||
@@ -213,6 +223,7 @@ export function getOriginals(
|
||||
|
||||
export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
|
||||
{start: string, middle: string, end: string} {
|
||||
const i18nAttr = etm.el.attrs.find(x => x.name === 'i18n');
|
||||
if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') &&
|
||||
etm.el.attrs.length === 1) {
|
||||
// this is the case where we're migrating and there's no need to keep the ng-container
|
||||
@@ -220,6 +231,11 @@ export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number
|
||||
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
|
||||
const middle = tmpl.slice(childStart, childEnd);
|
||||
return {start: '', middle, end: ''};
|
||||
} else if (etm.el.name === 'ng-template' && etm.el.attrs.length === 2 && i18nAttr !== undefined) {
|
||||
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
|
||||
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
|
||||
const middle = wrapIntoI18nContainer(i18nAttr, tmpl.slice(childStart, childEnd));
|
||||
return {start: '', middle, end: ''};
|
||||
}
|
||||
|
||||
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset;
|
||||
|
||||
@@ -357,6 +357,130 @@ describe('control flow migration', () => {
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an if case with an ng-template with i18n', async () => {
|
||||
writeFile('/comp.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
templateUrl: './comp.html'
|
||||
})
|
||||
class Comp {
|
||||
show = false;
|
||||
}
|
||||
`);
|
||||
|
||||
writeFile('/comp.html', [
|
||||
`<div>`,
|
||||
`<ng-template *ngIf="show" i18n="@@something"><span>Content here</span></ng-template>`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe([
|
||||
`<div>`,
|
||||
`@if (show) {`,
|
||||
`<ng-container i18n="@@something"><span>Content here</span></ng-container>`,
|
||||
`}`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an if case with an ng-template with empty i18n', async () => {
|
||||
writeFile('/comp.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
templateUrl: './comp.html'
|
||||
})
|
||||
class Comp {
|
||||
show = false;
|
||||
}
|
||||
`);
|
||||
|
||||
writeFile('/comp.html', [
|
||||
`<div>`,
|
||||
`<ng-template *ngIf="show" i18n><span>Content here</span></ng-template>`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe([
|
||||
`<div>`,
|
||||
`@if (show) {`,
|
||||
`<ng-container i18n><span>Content here</span></ng-container>`,
|
||||
`}`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an if case with an ng-container with i18n', async () => {
|
||||
writeFile('/comp.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
templateUrl: './comp.html'
|
||||
})
|
||||
class Comp {
|
||||
show = false;
|
||||
}
|
||||
`);
|
||||
|
||||
writeFile('/comp.html', [
|
||||
`<div>`,
|
||||
`<ng-container *ngIf="show" i18n="@@something"><span>Content here</span></ng-container>`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe([
|
||||
`<div>`,
|
||||
`@if (show) {`,
|
||||
`<ng-container i18n="@@something"><span>Content here</span></ng-container>`,
|
||||
`}`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an if case with an ng-container with empty i18n', async () => {
|
||||
writeFile('/comp.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
templateUrl: './comp.html'
|
||||
})
|
||||
class Comp {
|
||||
show = false;
|
||||
}
|
||||
`);
|
||||
|
||||
writeFile('/comp.html', [
|
||||
`<div>`,
|
||||
`<ng-container *ngIf="show" i18n><span>Content here</span></ng-container>`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe([
|
||||
`<div>`,
|
||||
`@if (show) {`,
|
||||
`<ng-container i18n><span>Content here</span></ng-container>`,
|
||||
`}`,
|
||||
`</div>`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an if else case', async () => {
|
||||
writeFile('/comp.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
@@ -2560,6 +2684,50 @@ describe('control flow migration', () => {
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should preserve i18n attribute on ng-templates in an if/else', async () => {
|
||||
writeFile('/comp.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'declare-comp',
|
||||
templateUrl: 'comp.html',
|
||||
})
|
||||
class DeclareComp {}
|
||||
`);
|
||||
|
||||
writeFile('/comp.html', [
|
||||
`<div>`,
|
||||
` <ng-container *ngIf="cond; else testTpl">`,
|
||||
` bla bla`,
|
||||
` </ng-container>`,
|
||||
`</div>`,
|
||||
`<ng-template #testTpl i18n="@@test_key">`,
|
||||
` <div class="test" *ngFor="let item of items"></div>`,
|
||||
`</ng-template>`,
|
||||
].join('\n'));
|
||||
|
||||
await runMigration();
|
||||
const actual = tree.readContent('/comp.html');
|
||||
|
||||
const expected = [
|
||||
`<div>`,
|
||||
` @if (cond) {\n`,
|
||||
` bla bla`,
|
||||
` `,
|
||||
`} @else {`,
|
||||
`<ng-container i18n="@@test_key">`,
|
||||
` @for (item of items; track item) {`,
|
||||
` <div class="test"></div>`,
|
||||
`}`,
|
||||
`</ng-container>`,
|
||||
`}`,
|
||||
`</div>\n`,
|
||||
].join('\n');
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no migration needed', () => {
|
||||
|
||||
Reference in New Issue
Block a user