fix(compiler): support directive inputs with interpolations on `<ng-template>`s (#35984)
Prior to this commit, Ivy compiler didn't handle directive inputs with interpolations located on `<ng-template>` elements (e.g. `<ng-template dir="{{ field }}">`). That was the case for regular inputs as well as inputs that should be processed via i18n subsystem (e.g. `<ng-template i18n-dir dir="Hello {{ name }}">`). This commit adds support for such expressions for explicit `<ng-template>`s as well as a number of tests to confirm the behavior. Fixes #35752. PR Close #35984
This commit is contained in:
parent
3fa895298d
commit
79659ee5aa
|
@ -403,6 +403,86 @@ describe('i18n support in the template compiler', () => {
|
||||||
verify(input, output);
|
verify(input, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support i18n attributes with interpolations on explicit <ng-template> elements',
|
||||||
|
() => {
|
||||||
|
const input = `
|
||||||
|
<ng-template i18n-title title="Hello {{ name }}"></ng-template>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
var $I18N_0$;
|
||||||
|
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||||
|
const $MSG_EXTERNAL_3771704108176831903$$APP_SPEC_TS_1$ = goog.getMsg("Hello {$interpolation}", {
|
||||||
|
"interpolation": "\uFFFD0\uFFFD"
|
||||||
|
});
|
||||||
|
$I18N_0$ = $MSG_EXTERNAL_3771704108176831903$$APP_SPEC_TS_1$;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$I18N_0$ = $localize \`Hello $` +
|
||||||
|
String.raw `{"\uFFFD0\uFFFD"}:INTERPOLATION:\`;
|
||||||
|
}
|
||||||
|
const $_c2$ = ["title", $I18N_0$];
|
||||||
|
…
|
||||||
|
consts: [[${AttributeMarker.Bindings}, "title"]],
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵtemplate(0, MyComponent_ng_template_0_Template, 0, 0, "ng-template", 0);
|
||||||
|
$r3$.ɵɵi18nAttributes(1, $_c2$);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵɵi18nExp(ctx.name);
|
||||||
|
$r3$.ɵɵi18nApply(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support i18n attributes with interpolations on explicit <ng-template> elements with structural directives',
|
||||||
|
() => {
|
||||||
|
const input = `
|
||||||
|
<ng-template *ngIf="true" i18n-title title="Hello {{ name }}"></ng-template>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
var $I18N_0$;
|
||||||
|
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||||
|
const $MSG_EXTERNAL_3771704108176831903$$APP_SPEC_TS__1$ = goog.getMsg("Hello {$interpolation}", {
|
||||||
|
"interpolation": "\uFFFD0\uFFFD"
|
||||||
|
});
|
||||||
|
$I18N_0$ = $MSG_EXTERNAL_3771704108176831903$$APP_SPEC_TS__1$;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$I18N_0$ = $localize \`Hello $` +
|
||||||
|
String.raw `{"\uFFFD0\uFFFD"}:INTERPOLATION:\`;
|
||||||
|
}
|
||||||
|
const $_c2$ = ["title", $I18N_0$];
|
||||||
|
…
|
||||||
|
function MyComponent_0_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵtemplate(0, MyComponent_0_ng_template_0_Template, 0, 0, "ng-template", 1);
|
||||||
|
$r3$.ɵɵi18nAttributes(1, $_c2$);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
const $ctx_r2$ = $r3$.ɵɵnextContext();
|
||||||
|
$r3$.ɵɵi18nExp($ctx_r2$.name);
|
||||||
|
$r3$.ɵɵi18nApply(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
…
|
||||||
|
consts: [[${AttributeMarker.Template}, "ngIf"], [${AttributeMarker.Bindings}, "title"]],
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 2, 1, undefined, 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵɵproperty("ngIf", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`;
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not create translations for empty attributes', () => {
|
it('should not create translations for empty attributes', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div id="static" i18n-title="m|d" title></div>
|
<div id="static" i18n-title="m|d" title></div>
|
||||||
|
|
|
@ -548,6 +548,95 @@ describe('compiler compliance: template', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow directive inputs as an interpolated prop on <ng-template>', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, Directive, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template dir="{{ message }}"></ng-template>',
|
||||||
|
})
|
||||||
|
export class TestComp {
|
||||||
|
message = 'Hello';
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
const expectedTemplate = `
|
||||||
|
consts: [[${AttributeMarker.Bindings}, "dir"]],
|
||||||
|
template: function TestComp_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$i0$.ɵɵtemplate(0, $TestComp_ng_template_0_Template$, 0, 0, "ng-template", 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$i0$.ɵɵpropertyInterpolate("dir", ctx.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`;
|
||||||
|
expectEmit(result.source, expectedTemplate, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow directive inputs as an interpolated prop on <ng-template> (with structural directives)',
|
||||||
|
() => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, Directive, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template *ngIf="true" dir="{{ message }}"></ng-template>',
|
||||||
|
})
|
||||||
|
export class TestComp {
|
||||||
|
message = 'Hello';
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
|
||||||
|
// Expect that `ɵɵpropertyInterpolate` is generated in the inner template function.
|
||||||
|
const expectedInnerTemplate = `
|
||||||
|
function $TestComp_0_Template$(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$i0$.ɵɵtemplate(0, $TestComp_0_ng_template_0_Template$, 0, 0, "ng-template", 1);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
const $ctx_r0$ = i0.ɵɵnextContext();
|
||||||
|
$i0$.ɵɵpropertyInterpolate("dir", $ctx_r0$.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
expectEmit(result.source, expectedInnerTemplate, 'Incorrect template');
|
||||||
|
|
||||||
|
// Main template should just contain *ngIf property.
|
||||||
|
const expectedMainTemplate = `
|
||||||
|
consts: [[4, "ngIf"], [3, "dir"]],
|
||||||
|
template: function TestComp_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$i0$.ɵɵtemplate(0, $TestComp_0_Template$, 1, 1, undefined, 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$i0$.ɵɵproperty("ngIf", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`;
|
||||||
|
expectEmit(result.source, expectedMainTemplate, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
it('should create unique template function names even for similar nested template structures',
|
it('should create unique template function names even for similar nested template structures',
|
||||||
() => {
|
() => {
|
||||||
const files = {
|
const files = {
|
||||||
|
|
|
@ -587,11 +587,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
} else if (name === 'class') {
|
} else if (name === 'class') {
|
||||||
stylingBuilder.registerClassAttr(value);
|
stylingBuilder.registerClassAttr(value);
|
||||||
} else {
|
} else {
|
||||||
if (attr.i18n) {
|
(attr.i18n ? i18nAttrs : outputAttrs).push(attr);
|
||||||
i18nAttrs.push(attr);
|
|
||||||
} else {
|
|
||||||
outputAttrs.push(attr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -895,17 +891,26 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
// Only add normal input/output binding instructions on explicit <ng-template> elements.
|
// Only add normal input/output binding instructions on explicit <ng-template> elements.
|
||||||
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
|
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
|
||||||
|
const inputs: t.BoundAttribute[] = [];
|
||||||
|
const i18nAttrs: (t.TextAttribute | t.BoundAttribute)[] =
|
||||||
|
template.attributes.filter(attr => !!attr.i18n);
|
||||||
|
|
||||||
|
template.inputs.forEach(
|
||||||
|
(input: t.BoundAttribute) => (input.i18n ? i18nAttrs : inputs).push(input));
|
||||||
|
|
||||||
// Add i18n attributes that may act as inputs to directives. If such attributes are present,
|
// Add i18n attributes that may act as inputs to directives. If such attributes are present,
|
||||||
// generate `i18nAttributes` instruction. Note: we generate it only for explicit <ng-template>
|
// generate `i18nAttributes` instruction. Note: we generate it only for explicit <ng-template>
|
||||||
// elements, in case of inline templates, corresponding instructions will be generated in the
|
// elements, in case of inline templates, corresponding instructions will be generated in the
|
||||||
// nested template function.
|
// nested template function.
|
||||||
const i18nAttrs: t.TextAttribute[] = template.attributes.filter(attr => !!attr.i18n);
|
|
||||||
if (i18nAttrs.length > 0) {
|
if (i18nAttrs.length > 0) {
|
||||||
this.i18nAttributesInstruction(templateIndex, i18nAttrs, template.sourceSpan);
|
this.i18nAttributesInstruction(templateIndex, i18nAttrs, template.sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the input bindings
|
// Add the input bindings
|
||||||
this.templatePropertyBindings(templateIndex, template.inputs);
|
if (inputs.length > 0) {
|
||||||
|
this.templatePropertyBindings(templateIndex, inputs);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate listeners for directive output
|
// Generate listeners for directive output
|
||||||
if (template.outputs.length > 0) {
|
if (template.outputs.length > 0) {
|
||||||
const listeners = template.outputs.map(
|
const listeners = template.outputs.map(
|
||||||
|
@ -1036,11 +1041,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
this.allocateBindingSlots(value);
|
this.allocateBindingSlots(value);
|
||||||
propertyBindings.push({
|
if (value instanceof Interpolation) {
|
||||||
name: input.name,
|
// Params typically contain attribute namespace and value sanitizer, which is applicable
|
||||||
sourceSpan: input.sourceSpan,
|
// for regular HTML elements, but not applicable for <ng-template> (since props act as
|
||||||
value: () => this.convertPropertyBinding(value)
|
// inputs to directives), so keep params array empty.
|
||||||
});
|
const params: any[] = [];
|
||||||
|
|
||||||
|
// prop="{{value}}" case
|
||||||
|
this.interpolatedUpdateInstruction(
|
||||||
|
getPropertyInterpolationExpression(value), templateIndex, input.name, input, value,
|
||||||
|
params);
|
||||||
|
} else {
|
||||||
|
// [prop]="value" case
|
||||||
|
propertyBindings.push({
|
||||||
|
name: input.name,
|
||||||
|
sourceSpan: input.sourceSpan,
|
||||||
|
value: () => this.convertPropertyBinding(value)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -341,6 +341,78 @@ describe('directives', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('inputs', () => {
|
||||||
|
it('should allow directive inputs (as a prop binding) on <ng-template>', () => {
|
||||||
|
let dirInstance: WithInput;
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
constructor() { dirInstance = this; }
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template [dir]="message"></ng-template>',
|
||||||
|
})
|
||||||
|
class TestComp {
|
||||||
|
message = 'Hello';
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [TestComp, WithInput]});
|
||||||
|
const fixture = TestBed.createComponent(TestComp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(dirInstance !.dir).toBe('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow directive inputs (as an interpolated prop) on <ng-template>', () => {
|
||||||
|
let dirInstance: WithInput;
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
constructor() { dirInstance = this; }
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template dir="{{ message }}"></ng-template>',
|
||||||
|
})
|
||||||
|
class TestComp {
|
||||||
|
message = 'Hello';
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [TestComp, WithInput]});
|
||||||
|
const fixture = TestBed.createComponent(TestComp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(dirInstance !.dir).toBe('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow directive inputs (as an interpolated prop) on <ng-template> with structural directives',
|
||||||
|
() => {
|
||||||
|
let dirInstance: WithInput;
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
constructor() { dirInstance = this; }
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template *ngIf="true" dir="{{ message }}"></ng-template>',
|
||||||
|
})
|
||||||
|
class TestComp {
|
||||||
|
message = 'Hello';
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [TestComp, WithInput]});
|
||||||
|
const fixture = TestBed.createComponent(TestComp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(dirInstance !.dir).toBe('Hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('outputs', () => {
|
describe('outputs', () => {
|
||||||
@Directive({selector: '[out]'})
|
@Directive({selector: '[out]'})
|
||||||
class TestDir {
|
class TestDir {
|
||||||
|
|
|
@ -1423,6 +1423,58 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
expect(titleDirInstances[0].title).toBe('Bonjour');
|
expect(titleDirInstances[0].title).toBe('Bonjour');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow directive inputs (as an interpolated prop) on <ng-template>', () => {
|
||||||
|
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
|
||||||
|
|
||||||
|
let dirInstance: WithInput;
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
constructor() { dirInstance = this; }
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template i18n-dir dir="Hello {{ name }}"></ng-template>',
|
||||||
|
})
|
||||||
|
class TestComp {
|
||||||
|
name = 'Angular';
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [TestComp, WithInput]});
|
||||||
|
const fixture = TestBed.createComponent(TestComp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(dirInstance !.dir).toBe('Bonjour Angular');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow directive inputs (as interpolated props)' +
|
||||||
|
'on <ng-template> with structural directives present',
|
||||||
|
() => {
|
||||||
|
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
|
||||||
|
|
||||||
|
let dirInstance: WithInput;
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class WithInput {
|
||||||
|
constructor() { dirInstance = this; }
|
||||||
|
@Input() dir: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<ng-template *ngIf="true" i18n-dir dir="Hello {{ name }}"></ng-template>',
|
||||||
|
})
|
||||||
|
class TestComp {
|
||||||
|
name = 'Angular';
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [TestComp, WithInput]});
|
||||||
|
const fixture = TestBed.createComponent(TestComp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(dirInstance !.dir).toBe('Bonjour Angular');
|
||||||
|
});
|
||||||
|
|
||||||
it('should apply i18n attributes during second template pass', () => {
|
it('should apply i18n attributes during second template pass', () => {
|
||||||
loadTranslations({[computeMsgId('Set')]: 'Set'});
|
loadTranslations({[computeMsgId('Set')]: 'Set'});
|
||||||
@Directive({
|
@Directive({
|
||||||
|
|
Loading…
Reference in New Issue