From 966c2a326a6e403e8f7aa427a76b43cf97730ba6 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Thu, 19 Sep 2019 17:22:06 -0700 Subject: [PATCH] fix(ivy): include `ngProjectAs` into attributes array (#32784) Prior to this commit, the `ngProjectAs` attribute was only included with a special flag and in a parsed format. As a result, projected node was missing `ngProjectAs` attribute as well as other attributes added after `ngProjectAs` one. This is problematic since app code might rely on the presence of `ngProjectAs` attribute (for example in CSS). This commit fixes the problem by including `ngProjectAs` into attributes array as a regular attribute and also makes sure that the parsed version of the `ngProjectAs` attribute with a special marker is added after regular attributes (thus we set them correctly at runtime). This change also aligns View Engine and Ivy behavior. PR Close #32784 --- .../compliance/r3_compiler_compliance_spec.ts | 4 +-- .../compiler/src/render3/view/template.ts | 30 +++++++++++++------ packages/core/test/acceptance/content_spec.ts | 30 +++++++++++++++++++ 3 files changed, 53 insertions(+), 11 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 fc6dea5f75..0e14d8c6fd 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1376,7 +1376,7 @@ describe('compiler compliance', () => { const SimpleComponentDefinition = ` const $_c0$ = [[["", "title", ""]]]; const $_c1$ = ["[title]"]; - const $_c2$ = [5, ["", "title", ""]]; + const $_c2$ = ["ngProjectAs", "[title]", 5, ["", "title", ""]]; … MyApp.ngComponentDef = $r3$.ɵɵdefineComponent({ type: MyApp, @@ -1428,7 +1428,7 @@ describe('compiler compliance', () => { const SimpleComponentDefinition = ` const $_c0$ = [[["", "title", ""]]]; const $_c1$ = ["[title]"]; - const $_c2$ = [5, ["", "title", ""]]; + const $_c2$ = ["ngProjectAs", "[title],[header]", 5, ["", "title", ""]]; … MyApp.ngComponentDef = $r3$.ɵɵdefineComponent({ type: MyApp, diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index e00bdd9c2f..59ef9a94b5 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -497,18 +497,24 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length; const parameters: o.Expression[] = [o.literal(slot)]; const attributes: o.Expression[] = []; + let ngProjectAsAttr: t.TextAttribute|undefined; this._ngContentReservedSlots.push(ngContent.selector); ngContent.attributes.forEach((attribute) => { const {name, value} = attribute; if (name === NG_PROJECT_AS_ATTR_NAME) { - attributes.push(...getNgProjectAsLiteral(attribute)); - } else if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) { + ngProjectAsAttr = attribute; + } + if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) { attributes.push(o.literal(name), o.literal(value)); } }); + if (ngProjectAsAttr) { + attributes.push(...getNgProjectAsLiteral(ngProjectAsAttr)); + } + if (attributes.length > 0) { parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes)); } else if (projectionSlotIdx !== 0) { @@ -535,6 +541,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const i18nAttrs: (t.TextAttribute | t.BoundAttribute)[] = []; const outputAttrs: t.TextAttribute[] = []; + let ngProjectAsAttr: t.TextAttribute|undefined; const [namespaceKey, elementName] = splitNsName(element.name); const isNgContainer = checkIsNgContainer(element.name); @@ -549,6 +556,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } else if (name === 'class') { stylingBuilder.registerClassAttr(value); } else { + if (attr.name === NG_PROJECT_AS_ATTR_NAME) { + ngProjectAsAttr = attr; + } if (attr.i18n) { // Place attributes into a separate array for i18n processing, but also keep such // attributes in the main list to make them available for directive matching at runtime. @@ -590,16 +600,12 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); outputAttrs.forEach(attr => { - if (attr.name === NG_PROJECT_AS_ATTR_NAME) { - attributes.push(...getNgProjectAsLiteral(attr)); - } else { - attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value)); - } + attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value)); }); // add attributes for directive and projection matching purposes attributes.push(...this.prepareNonRenderAttrs( - allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs)); + allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs, ngProjectAsAttr)); parameters.push(this.toAttrsParam(attributes)); // local refs (ex.:
) @@ -1206,6 +1212,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver * STYLES, style1, value1, style2, value2, * BINDINGS, name1, name2, name3, * TEMPLATE, name4, name5, name6, + * PROJECT_AS, selector, * I18N, name7, name8, ...] * ``` * @@ -1215,7 +1222,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private prepareNonRenderAttrs( inputs: t.BoundAttribute[], outputs: t.BoundEvent[], styles?: StylingBuilder, templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [], - i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] { + i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = [], + ngProjectAsAttr?: t.TextAttribute): o.Expression[] { const alreadySeen = new Set(); const attrExprs: o.Expression[] = []; @@ -1271,6 +1279,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver templateAttrs.forEach(attr => addAttrExpr(attr.name)); } + if (ngProjectAsAttr) { + attrExprs.push(...getNgProjectAsLiteral(ngProjectAsAttr)); + } + if (i18nAttrs.length) { attrExprs.push(o.literal(core.AttributeMarker.I18n)); i18nAttrs.forEach(attr => addAttrExpr(attr.name)); diff --git a/packages/core/test/acceptance/content_spec.ts b/packages/core/test/acceptance/content_spec.ts index 24ca05122f..142a86fe9c 100644 --- a/packages/core/test/acceptance/content_spec.ts +++ b/packages/core/test/acceptance/content_spec.ts @@ -1025,6 +1025,36 @@ describe('projection', () => { expect(fixture.nativeElement.textContent).not.toContain('Title content'); }); + it('should preserve ngProjectAs and other attributes on projected element', () => { + @Component({ + selector: 'projector', + template: ``, + }) + class Projector { + } + + @Component({ + template: ` + +
+
+ ` + }) + class Root { + } + + TestBed.configureTestingModule({ + declarations: [Root, Projector], + }); + const fixture = TestBed.createComponent(Root); + fixture.detectChanges(); + + const projectedElement = fixture.debugElement.query(By.css('div')); + const {ngProjectAs, title} = projectedElement.attributes; + expect(ngProjectAs).toBe('projectMe'); + expect(title).toBe('some title'); + }); + describe('on inline templates (e.g. *ngIf)', () => { it('should work when matching the element name', () => { let divDirectives = 0;