perf(ivy): add self-closing elementContainer instruction (#31444)
Adds a new `elementContainer` instruction that can be used to avoid two instruction (`elementContainerStart` and `elementContainerEnd`) for `ng-container` that has text-only content. This is particularly useful when we have `ng-container` inside i18n sections. This PR resolves FW-1105. PR Close #31444
This commit is contained in:
parent
e92fb68f3c
commit
23e0d65471
|
@ -253,7 +253,7 @@ describe('compiler compliance', () => {
|
||||||
expectEmit(result.source, template, 'Incorrect template');
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate elementContainerStart/End instructions for empty <ng-container>', () => {
|
it('should generate self-closing elementContainer instruction for empty <ng-container>', () => {
|
||||||
const files = {
|
const files = {
|
||||||
app: {
|
app: {
|
||||||
'spec.ts': `
|
'spec.ts': `
|
||||||
|
@ -276,8 +276,7 @@ describe('compiler compliance', () => {
|
||||||
…
|
…
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
i0.ɵɵelementContainerStart(0);
|
i0.ɵɵelementContainer(0);
|
||||||
i0.ɵɵelementContainerEnd();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1977,9 +1977,8 @@ describe('i18n support in the view compiler', () => {
|
||||||
$r3$.ɵɵelementStart(0, "div");
|
$r3$.ɵɵelementStart(0, "div");
|
||||||
$r3$.ɵɵi18nStart(1, $I18N_0$);
|
$r3$.ɵɵi18nStart(1, $I18N_0$);
|
||||||
$r3$.ɵɵtemplate(2, MyComponent_ng_template_2_Template, 2, 3, "ng-template");
|
$r3$.ɵɵtemplate(2, MyComponent_ng_template_2_Template, 2, 3, "ng-template");
|
||||||
$r3$.ɵɵelementContainerStart(3);
|
$r3$.ɵɵelementContainer(3);
|
||||||
$r3$.ɵɵpipe(4, "uppercase");
|
$r3$.ɵɵpipe(4, "uppercase");
|
||||||
$r3$.ɵɵelementContainerEnd();
|
|
||||||
$r3$.ɵɵi18nEnd();
|
$r3$.ɵɵi18nEnd();
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
}
|
}
|
||||||
|
@ -2324,6 +2323,76 @@ describe('i18n support in the view compiler', () => {
|
||||||
|
|
||||||
verify(input, output);
|
verify(input, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate a self-closing container instruction for ng-container inside i18n', () => {
|
||||||
|
const input = `
|
||||||
|
<div i18n>
|
||||||
|
Hello <ng-container>there</ng-container>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
var $I18N_0$;
|
||||||
|
if (ngI18nClosureMode) {
|
||||||
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg(" Hello {$startTagNgContainer}there{$closeTagNgContainer}", { "startTagNgContainer": "\uFFFD#2\uFFFD", "closeTagNgContainer": "\uFFFD/#2\uFFFD" });
|
||||||
|
$I18N_0$ = $MSG_APP_SPEC_TS_1$;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$I18N_0$ = $r3$.ɵɵi18nLocalize(" Hello {$startTagNgContainer}there{$closeTagNgContainer}", { "startTagNgContainer": "\uFFFD#2\uFFFD", "closeTagNgContainer": "\uFFFD/#2\uFFFD" });
|
||||||
|
}
|
||||||
|
…
|
||||||
|
consts: 3,
|
||||||
|
vars: 0,
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵelementStart(0, "div");
|
||||||
|
$r3$.ɵɵi18nStart(1, I18N_0);
|
||||||
|
$r3$.ɵɵelementContainer(2);
|
||||||
|
$r3$.ɵɵi18nEnd();
|
||||||
|
$r3$.ɵɵelementEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not generate a self-closing container instruction for ng-container with non-text content inside i18n',
|
||||||
|
() => {
|
||||||
|
const input = `
|
||||||
|
<div i18n>
|
||||||
|
Hello <ng-container>there <strong>!</strong></ng-container>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
var $I18N_0$;
|
||||||
|
if (ngI18nClosureMode) {
|
||||||
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg(" Hello {$startTagNgContainer}there {$startTagStrong}!{$closeTagStrong}{$closeTagNgContainer}", { "startTagNgContainer": "\uFFFD#2\uFFFD", "startTagStrong": "\uFFFD#3\uFFFD", "closeTagStrong": "\uFFFD/#3\uFFFD", "closeTagNgContainer": "\uFFFD/#2\uFFFD" });
|
||||||
|
$I18N_0$ = $MSG_APP_SPEC_TS_1$;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$I18N_0$ = $r3$.ɵɵi18nLocalize(" Hello {$startTagNgContainer}there {$startTagStrong}!{$closeTagStrong}{$closeTagNgContainer}", { "startTagNgContainer": "\uFFFD#2\uFFFD", "startTagStrong": "\uFFFD#3\uFFFD", "closeTagStrong": "\uFFFD/#3\uFFFD", "closeTagNgContainer": "\uFFFD/#2\uFFFD" });
|
||||||
|
}
|
||||||
|
…
|
||||||
|
consts: 4,
|
||||||
|
vars: 0,
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵelementStart(0, "div");
|
||||||
|
$r3$.ɵɵi18nStart(1, I18N_0);
|
||||||
|
$r3$.ɵɵelementContainerStart(2);
|
||||||
|
$r3$.ɵɵelement(3, "strong");
|
||||||
|
$r3$.ɵɵelementContainerEnd();
|
||||||
|
$r3$.ɵɵi18nEnd();
|
||||||
|
$r3$.ɵɵelementEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('whitespace preserving mode', () => {
|
describe('whitespace preserving mode', () => {
|
||||||
|
|
|
@ -66,6 +66,8 @@ export class Identifiers {
|
||||||
static elementContainerEnd:
|
static elementContainerEnd:
|
||||||
o.ExternalReference = {name: 'ɵɵelementContainerEnd', moduleName: CORE};
|
o.ExternalReference = {name: 'ɵɵelementContainerEnd', moduleName: CORE};
|
||||||
|
|
||||||
|
static elementContainer: o.ExternalReference = {name: 'ɵɵelementContainer', moduleName: CORE};
|
||||||
|
|
||||||
static styling: o.ExternalReference = {name: 'ɵɵstyling', moduleName: CORE};
|
static styling: o.ExternalReference = {name: 'ɵɵstyling', moduleName: CORE};
|
||||||
|
|
||||||
static styleMap: o.ExternalReference = {name: 'ɵɵstyleMap', moduleName: CORE};
|
static styleMap: o.ExternalReference = {name: 'ɵɵstyleMap', moduleName: CORE};
|
||||||
|
|
|
@ -613,23 +613,21 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this.i18n.appendElement(element.i18n !, elementIndex);
|
this.i18n.appendElement(element.i18n !, elementIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChildren = () => {
|
// Note that we do not append text node instructions and ICUs inside i18n section,
|
||||||
if (!isI18nRootElement && this.i18n) {
|
|
||||||
// we do not append text node instructions and ICUs inside i18n section,
|
|
||||||
// so we exclude them while calculating whether current element has children
|
// so we exclude them while calculating whether current element has children
|
||||||
return !hasTextChildrenOnly(element.children);
|
const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) :
|
||||||
}
|
element.children.length > 0;
|
||||||
return element.children.length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSelfClosingInstruction = !stylingBuilder.hasBindings && !isNgContainer &&
|
const createSelfClosingInstruction = !stylingBuilder.hasBindings &&
|
||||||
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
|
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren;
|
||||||
|
|
||||||
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
|
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
|
||||||
!stylingBuilder.hasBindings && hasTextChildrenOnly(element.children);
|
!stylingBuilder.hasBindings && hasTextChildrenOnly(element.children);
|
||||||
|
|
||||||
if (createSelfClosingInstruction) {
|
if (createSelfClosingInstruction) {
|
||||||
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
this.creationInstruction(
|
||||||
|
element.sourceSpan, isNgContainer ? R3.elementContainer : R3.element,
|
||||||
|
trimTrailingNulls(parameters));
|
||||||
} else {
|
} else {
|
||||||
this.creationInstruction(
|
this.creationInstruction(
|
||||||
element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
|
element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
|
||||||
|
|
|
@ -118,6 +118,7 @@ export {
|
||||||
ɵɵallocHostVars,
|
ɵɵallocHostVars,
|
||||||
ɵɵelementContainerStart,
|
ɵɵelementContainerStart,
|
||||||
ɵɵelementContainerEnd,
|
ɵɵelementContainerEnd,
|
||||||
|
ɵɵelementContainer,
|
||||||
ɵɵstyling,
|
ɵɵstyling,
|
||||||
ɵɵstyleMap,
|
ɵɵstyleMap,
|
||||||
ɵɵclassMap,
|
ɵɵclassMap,
|
||||||
|
|
|
@ -56,6 +56,7 @@ export {
|
||||||
ɵɵdirectiveInject,
|
ɵɵdirectiveInject,
|
||||||
|
|
||||||
ɵɵelement,
|
ɵɵelement,
|
||||||
|
ɵɵelementContainer,
|
||||||
ɵɵelementContainerEnd,
|
ɵɵelementContainerEnd,
|
||||||
|
|
||||||
ɵɵelementContainerStart,
|
ɵɵelementContainerStart,
|
||||||
|
|
|
@ -100,3 +100,19 @@ export function ɵɵelementContainerEnd(): void {
|
||||||
|
|
||||||
registerPostOrderHooks(tView, previousOrParentTNode);
|
registerPostOrderHooks(tView, previousOrParentTNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty logical container using {@link elementContainerStart}
|
||||||
|
* and {@link elementContainerEnd}
|
||||||
|
*
|
||||||
|
* @param index Index of the element in the LView array
|
||||||
|
* @param attrs Set of attributes to be used when matching directives.
|
||||||
|
* @param localRefs A set of local reference bindings on the element.
|
||||||
|
*
|
||||||
|
* @codeGenApi
|
||||||
|
*/
|
||||||
|
export function ɵɵelementContainer(
|
||||||
|
index: number, attrs?: TAttributes | null, localRefs?: string[] | null): void {
|
||||||
|
ɵɵelementContainerStart(index, attrs, localRefs);
|
||||||
|
ɵɵelementContainerEnd();
|
||||||
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ export const angularCoreEnv: {[name: string]: Function} =
|
||||||
'ɵɵelement': r3.ɵɵelement,
|
'ɵɵelement': r3.ɵɵelement,
|
||||||
'ɵɵelementContainerStart': r3.ɵɵelementContainerStart,
|
'ɵɵelementContainerStart': r3.ɵɵelementContainerStart,
|
||||||
'ɵɵelementContainerEnd': r3.ɵɵelementContainerEnd,
|
'ɵɵelementContainerEnd': r3.ɵɵelementContainerEnd,
|
||||||
|
'ɵɵelementContainer': r3.ɵɵelementContainer,
|
||||||
'ɵɵpureFunction0': r3.ɵɵpureFunction0,
|
'ɵɵpureFunction0': r3.ɵɵpureFunction0,
|
||||||
'ɵɵpureFunction1': r3.ɵɵpureFunction1,
|
'ɵɵpureFunction1': r3.ɵɵpureFunction1,
|
||||||
'ɵɵpureFunction2': r3.ɵɵpureFunction2,
|
'ɵɵpureFunction2': r3.ɵɵpureFunction2,
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import {registerLocaleData} from '@angular/common';
|
import {registerLocaleData} from '@angular/common';
|
||||||
import localeRo from '@angular/common/locales/ro';
|
import localeRo from '@angular/common/locales/ro';
|
||||||
import {Component, ContentChild, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core';
|
import {Component, ContentChild, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {setDelayProjection} from '@angular/core/src/render3/instructions/projection';
|
import {setDelayProjection} from '@angular/core/src/render3/instructions/projection';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {By} from '@angular/platform-browser';
|
import {By} from '@angular/platform-browser';
|
||||||
|
@ -18,7 +18,7 @@ import {onlyInIvy} from '@angular/private/testing';
|
||||||
|
|
||||||
onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef]});
|
TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef, UppercasePipe]});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => { setDelayProjection(false); });
|
afterEach(() => { setDelayProjection(false); });
|
||||||
|
@ -315,6 +315,29 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to act as child elements inside i18n block (text + pipes)', () => {
|
||||||
|
// Note: for some reason keeping this key inline causes clang to reformat the entire file
|
||||||
|
// in a very weird way. Keeping it separated like this seems to make it happy.
|
||||||
|
const key = '{$startTagNgTemplate}Hello {$interpolation}{$closeTagNgTemplate}' +
|
||||||
|
'{$startTagNgContainer}Bye {$interpolation}{$closeTagNgContainer}';
|
||||||
|
|
||||||
|
ɵi18nConfigureLocalize({
|
||||||
|
translations: {
|
||||||
|
[key]:
|
||||||
|
'{$startTagNgTemplate}Hej {$interpolation}{$closeTagNgTemplate}{$startTagNgContainer}Vi ses {$interpolation}{$closeTagNgContainer}'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const fixture = initWithTemplate(AppComp, `
|
||||||
|
<div i18n>
|
||||||
|
<ng-template tplRef>Hello {{name | uppercase}}</ng-template>
|
||||||
|
<ng-container>Bye {{name | uppercase}}</ng-container>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const element = fixture.nativeElement.firstChild;
|
||||||
|
expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Hej ANGULARVi ses ANGULAR');
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to handle deep nested levels with templates', () => {
|
it('should be able to handle deep nested levels with templates', () => {
|
||||||
ɵi18nConfigureLocalize({
|
ɵi18nConfigureLocalize({
|
||||||
translations: {
|
translations: {
|
||||||
|
@ -1463,3 +1486,8 @@ class DirectiveWithTplRef {
|
||||||
constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {}
|
constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {}
|
||||||
ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); }
|
ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Pipe({name: 'uppercase'})
|
||||||
|
class UppercasePipe implements PipeTransform {
|
||||||
|
transform(value: string) { return value.toUpperCase(); }
|
||||||
|
}
|
||||||
|
|
|
@ -853,6 +853,8 @@ export declare function ɵɵdisableBindings(): void;
|
||||||
|
|
||||||
export declare function ɵɵelement(index: number, name: string, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
export declare function ɵɵelement(index: number, name: string, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
||||||
|
|
||||||
|
export declare function ɵɵelementContainer(index: number, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
||||||
|
|
||||||
export declare function ɵɵelementContainerEnd(): void;
|
export declare function ɵɵelementContainerEnd(): void;
|
||||||
|
|
||||||
export declare function ɵɵelementContainerStart(index: number, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
export declare function ɵɵelementContainerStart(index: number, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
||||||
|
|
Loading…
Reference in New Issue