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');
|
||||
});
|
||||
|
||||
it('should generate elementContainerStart/End instructions for empty <ng-container>', () => {
|
||||
it('should generate self-closing elementContainer instruction for empty <ng-container>', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
|
@ -276,8 +276,7 @@ describe('compiler compliance', () => {
|
|||
…
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementContainerStart(0);
|
||||
i0.ɵɵelementContainerEnd();
|
||||
i0.ɵɵelementContainer(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1977,9 +1977,8 @@ describe('i18n support in the view compiler', () => {
|
|||
$r3$.ɵɵelementStart(0, "div");
|
||||
$r3$.ɵɵi18nStart(1, $I18N_0$);
|
||||
$r3$.ɵɵtemplate(2, MyComponent_ng_template_2_Template, 2, 3, "ng-template");
|
||||
$r3$.ɵɵelementContainerStart(3);
|
||||
$r3$.ɵɵelementContainer(3);
|
||||
$r3$.ɵɵpipe(4, "uppercase");
|
||||
$r3$.ɵɵelementContainerEnd();
|
||||
$r3$.ɵɵi18nEnd();
|
||||
$r3$.ɵɵelementEnd();
|
||||
}
|
||||
|
@ -2324,6 +2323,76 @@ describe('i18n support in the view compiler', () => {
|
|||
|
||||
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', () => {
|
||||
|
|
|
@ -66,6 +66,8 @@ export class Identifiers {
|
|||
static elementContainerEnd:
|
||||
o.ExternalReference = {name: 'ɵɵelementContainerEnd', moduleName: CORE};
|
||||
|
||||
static elementContainer: o.ExternalReference = {name: 'ɵɵelementContainer', moduleName: CORE};
|
||||
|
||||
static styling: o.ExternalReference = {name: 'ɵɵstyling', 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);
|
||||
}
|
||||
|
||||
const hasChildren = () => {
|
||||
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
|
||||
return !hasTextChildrenOnly(element.children);
|
||||
}
|
||||
return element.children.length > 0;
|
||||
};
|
||||
// Note that we do not append text node instructions and ICUs inside i18n section,
|
||||
// so we exclude them while calculating whether current element has children
|
||||
const hasChildren = (!isI18nRootElement && this.i18n) ? !hasTextChildrenOnly(element.children) :
|
||||
element.children.length > 0;
|
||||
|
||||
const createSelfClosingInstruction = !stylingBuilder.hasBindings && !isNgContainer &&
|
||||
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
|
||||
const createSelfClosingInstruction = !stylingBuilder.hasBindings &&
|
||||
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren;
|
||||
|
||||
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
|
||||
!stylingBuilder.hasBindings && hasTextChildrenOnly(element.children);
|
||||
|
||||
if (createSelfClosingInstruction) {
|
||||
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
||||
this.creationInstruction(
|
||||
element.sourceSpan, isNgContainer ? R3.elementContainer : R3.element,
|
||||
trimTrailingNulls(parameters));
|
||||
} else {
|
||||
this.creationInstruction(
|
||||
element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
|
||||
|
|
|
@ -118,6 +118,7 @@ export {
|
|||
ɵɵallocHostVars,
|
||||
ɵɵelementContainerStart,
|
||||
ɵɵelementContainerEnd,
|
||||
ɵɵelementContainer,
|
||||
ɵɵstyling,
|
||||
ɵɵstyleMap,
|
||||
ɵɵclassMap,
|
||||
|
|
|
@ -56,6 +56,7 @@ export {
|
|||
ɵɵdirectiveInject,
|
||||
|
||||
ɵɵelement,
|
||||
ɵɵelementContainer,
|
||||
ɵɵelementContainerEnd,
|
||||
|
||||
ɵɵelementContainerStart,
|
||||
|
|
|
@ -100,3 +100,19 @@ export function ɵɵelementContainerEnd(): void {
|
|||
|
||||
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,
|
||||
'ɵɵelementContainerStart': r3.ɵɵelementContainerStart,
|
||||
'ɵɵelementContainerEnd': r3.ɵɵelementContainerEnd,
|
||||
'ɵɵelementContainer': r3.ɵɵelementContainer,
|
||||
'ɵɵpureFunction0': r3.ɵɵpureFunction0,
|
||||
'ɵɵpureFunction1': r3.ɵɵpureFunction1,
|
||||
'ɵɵpureFunction2': r3.ɵɵpureFunction2,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {registerLocaleData} from '@angular/common';
|
||||
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 {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
@ -18,7 +18,7 @@ import {onlyInIvy} from '@angular/private/testing';
|
|||
|
||||
onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef]});
|
||||
TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef, UppercasePipe]});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
ɵi18nConfigureLocalize({
|
||||
translations: {
|
||||
|
@ -1463,3 +1486,8 @@ class DirectiveWithTplRef {
|
|||
constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {}
|
||||
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 ɵɵelementContainer(index: number, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
||||
|
||||
export declare function ɵɵelementContainerEnd(): void;
|
||||
|
||||
export declare function ɵɵelementContainerStart(index: number, attrs?: TAttributes | null, localRefs?: string[] | null): void;
|
||||
|
|
Loading…
Reference in New Issue