From c50faa97ca4cca567bf7affc5f314537a1deb8f5 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 2 Dec 2019 15:34:48 -0800 Subject: [PATCH] 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 --- .../compliance/r3_compiler_compliance_spec.ts | 45 ++++++++++++++++++ .../compiler/src/render3/view/template.ts | 12 +++-- packages/core/test/acceptance/content_spec.ts | 47 +++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index d16705e225..7ef43a01fb 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1510,6 +1510,51 @@ describe('compiler compliance', () => { 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: '
' + }) + 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', () => { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 897957aec9..4fd45693bb 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -841,6 +841,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver visitTemplate(template: t.Template) { const NG_TEMPLATE_TAG_NAME = 'ng-template'; const templateIndex = this.allocateDataSlot(); + let ngProjectAsAttr: t.TextAttribute|undefined; if (this.i18n) { this.i18n.appendTemplate(template.i18n !, templateIndex); @@ -864,10 +865,15 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // prepare attributes parameter (including attributes used for directive matching) const attrsExprs: o.Expression[] = []; - template.attributes.forEach( - (a: t.TextAttribute) => { attrsExprs.push(asLiteral(a.name), asLiteral(a.value)); }); + template.attributes.forEach((attr: t.TextAttribute) => { + if (attr.name === NG_PROJECT_AS_ATTR_NAME) { + ngProjectAsAttr = attr; + } + attrsExprs.push(asLiteral(attr.name), asLiteral(attr.value)); + }); 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)); // local refs (ex.: ) diff --git a/packages/core/test/acceptance/content_spec.ts b/packages/core/test/acceptance/content_spec.ts index 142a86fe9c..a064b2e00c 100644 --- a/packages/core/test/acceptance/content_spec.ts +++ b/packages/core/test/acceptance/content_spec.ts @@ -1165,6 +1165,53 @@ describe('projection', () => { expect(fixture.nativeElement).toHaveText('inline()ng-template(onetwothree)'); }); + it('should project template content with `ngProjectAs` defined', () => { + @Component({ + selector: 'projector-app', + template: ` + Projected + + + + `, + }) + class ProjectorApp { + } + + @Component({ + selector: 'root-comp', + template: ` + +
as element
+
as attribute
+
as class
+
+ `, + }) + 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', () => { it('should work when matching attributes', () => { let xDirectives = 0;