fix(ivy): correctly support `ngProjectAs` on templates (#34200)
Prior to this commit, if a template (for example, generated using structural directive such as *ngIf) contains `ngProjectAs` attribute, it was not included into attributes array in generated code and as a result, these templates were not matched at runtime during content projection. This commit adds the logic to append `ngProjectAs` values into corresponding element's attribute arrays, so content projection works as expected. PR Close #34200
This commit is contained in:
parent
60b13d9948
commit
c50faa97ca
|
@ -1510,6 +1510,51 @@ describe('compiler compliance', () => {
|
||||||
result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
|
result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should include parsed ngProjectAs selectors into template attrs', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: '<div *ngIf="show" ngProjectAs=".someclass"></div>'
|
||||||
|
})
|
||||||
|
export class MyApp {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleComponentDefinition = `
|
||||||
|
MyApp.ɵcmp = i0.ɵɵdefineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
selectors: [
|
||||||
|
["my-app"]
|
||||||
|
],
|
||||||
|
decls: 1,
|
||||||
|
vars: 1,
|
||||||
|
consts: [
|
||||||
|
["ngProjectAs", ".someclass", ${AttributeMarker.Template}, "ngIf", ${AttributeMarker.ProjectAs}, ["", 8, "someclass"]],
|
||||||
|
["ngProjectAs", ".someclass", ${AttributeMarker.ProjectAs}, ["", 8, "someclass"]]
|
||||||
|
],
|
||||||
|
template: function MyApp_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
i0.ɵɵtemplate(0, MyApp_div_0_Template, 1, 0, "div", 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
i0.ɵɵproperty("ngIf", ctx.show);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encapsulation: 2
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, SimpleComponentDefinition, 'Incorrect MyApp definition');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('queries', () => {
|
describe('queries', () => {
|
||||||
|
|
|
@ -841,6 +841,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
visitTemplate(template: t.Template) {
|
visitTemplate(template: t.Template) {
|
||||||
const NG_TEMPLATE_TAG_NAME = 'ng-template';
|
const NG_TEMPLATE_TAG_NAME = 'ng-template';
|
||||||
const templateIndex = this.allocateDataSlot();
|
const templateIndex = this.allocateDataSlot();
|
||||||
|
let ngProjectAsAttr: t.TextAttribute|undefined;
|
||||||
|
|
||||||
if (this.i18n) {
|
if (this.i18n) {
|
||||||
this.i18n.appendTemplate(template.i18n !, templateIndex);
|
this.i18n.appendTemplate(template.i18n !, templateIndex);
|
||||||
|
@ -864,10 +865,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
// prepare attributes parameter (including attributes used for directive matching)
|
// prepare attributes parameter (including attributes used for directive matching)
|
||||||
const attrsExprs: o.Expression[] = [];
|
const attrsExprs: o.Expression[] = [];
|
||||||
template.attributes.forEach(
|
template.attributes.forEach((attr: t.TextAttribute) => {
|
||||||
(a: t.TextAttribute) => { attrsExprs.push(asLiteral(a.name), asLiteral(a.value)); });
|
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
|
||||||
|
ngProjectAsAttr = attr;
|
||||||
|
}
|
||||||
|
attrsExprs.push(asLiteral(attr.name), asLiteral(attr.value));
|
||||||
|
});
|
||||||
attrsExprs.push(...this.prepareNonRenderAttrs(
|
attrsExprs.push(...this.prepareNonRenderAttrs(
|
||||||
template.inputs, template.outputs, undefined, template.templateAttrs));
|
template.inputs, template.outputs, undefined, template.templateAttrs, undefined,
|
||||||
|
ngProjectAsAttr));
|
||||||
parameters.push(this.addAttrsToConsts(attrsExprs));
|
parameters.push(this.addAttrsToConsts(attrsExprs));
|
||||||
|
|
||||||
// local refs (ex.: <ng-template #foo>)
|
// local refs (ex.: <ng-template #foo>)
|
||||||
|
|
|
@ -1165,6 +1165,53 @@ describe('projection', () => {
|
||||||
expect(fixture.nativeElement).toHaveText('inline()ng-template(onetwothree)');
|
expect(fixture.nativeElement).toHaveText('inline()ng-template(onetwothree)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should project template content with `ngProjectAs` defined', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'projector-app',
|
||||||
|
template: `
|
||||||
|
Projected
|
||||||
|
<ng-content select="foo"></ng-content>
|
||||||
|
<ng-content select="[foo]"></ng-content>
|
||||||
|
<ng-content select=".foo"></ng-content>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class ProjectorApp {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'root-comp',
|
||||||
|
template: `
|
||||||
|
<projector-app>
|
||||||
|
<div *ngIf="show" ngProjectAs="foo">as element</div>
|
||||||
|
<div *ngIf="show" ngProjectAs="[foo]">as attribute</div>
|
||||||
|
<div *ngIf="show" ngProjectAs=".foo">as class</div>
|
||||||
|
</projector-app>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class RootComp {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProjectorApp, RootComp],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(RootComp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let content = fixture.nativeElement.textContent;
|
||||||
|
expect(content).toContain('as element');
|
||||||
|
expect(content).toContain('as attribute');
|
||||||
|
expect(content).toContain('as class');
|
||||||
|
|
||||||
|
fixture.componentInstance.show = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
content = fixture.nativeElement.textContent;
|
||||||
|
expect(content).not.toContain('as element');
|
||||||
|
expect(content).not.toContain('as attribute');
|
||||||
|
expect(content).not.toContain('as class');
|
||||||
|
});
|
||||||
|
|
||||||
describe('on containers', () => {
|
describe('on containers', () => {
|
||||||
it('should work when matching attributes', () => {
|
it('should work when matching attributes', () => {
|
||||||
let xDirectives = 0;
|
let xDirectives = 0;
|
||||||
|
|
Loading…
Reference in New Issue