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:
crisbeto 2019-07-07 16:35:58 +02:00 committed by Jason Aden
parent e92fb68f3c
commit 23e0d65471
10 changed files with 135 additions and 18 deletions

View File

@ -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();
} }
} }
`; `;

View File

@ -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', () => {

View File

@ -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};

View File

@ -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) { // so we exclude them while calculating whether current element has children
// we do not append text node instructions and ICUs inside i18n section, const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) :
// so we exclude them while calculating whether current element has children element.children.length > 0;
return !hasTextChildrenOnly(element.children);
}
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,

View File

@ -118,6 +118,7 @@ export {
ɵɵallocHostVars, ɵɵallocHostVars,
ɵɵelementContainerStart, ɵɵelementContainerStart,
ɵɵelementContainerEnd, ɵɵelementContainerEnd,
ɵɵelementContainer,
ɵɵstyling, ɵɵstyling,
ɵɵstyleMap, ɵɵstyleMap,
ɵɵclassMap, ɵɵclassMap,

View File

@ -56,6 +56,7 @@ export {
ɵɵdirectiveInject, ɵɵdirectiveInject,
ɵɵelement, ɵɵelement,
ɵɵelementContainer,
ɵɵelementContainerEnd, ɵɵelementContainerEnd,
ɵɵelementContainerStart, ɵɵelementContainerStart,

View File

@ -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();
}

View File

@ -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,

View File

@ -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(); }
}

View File

@ -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;