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
This commit is contained in:
		
							parent
							
								
									278d634723
								
							
						
					
					
						commit
						966c2a326a
					
				| @ -1376,7 +1376,7 @@ describe('compiler compliance', () => { | |||||||
|         const SimpleComponentDefinition = ` |         const SimpleComponentDefinition = ` | ||||||
|           const $_c0$ = [[["", "title", ""]]]; |           const $_c0$ = [[["", "title", ""]]]; | ||||||
|           const $_c1$ = ["[title]"]; |           const $_c1$ = ["[title]"]; | ||||||
|           const $_c2$ = [5, ["", "title", ""]]; |           const $_c2$ = ["ngProjectAs", "[title]", 5, ["", "title", ""]]; | ||||||
|           … |           … | ||||||
|           MyApp.ngComponentDef = $r3$.ɵɵdefineComponent({ |           MyApp.ngComponentDef = $r3$.ɵɵdefineComponent({ | ||||||
|             type: MyApp, |             type: MyApp, | ||||||
| @ -1428,7 +1428,7 @@ describe('compiler compliance', () => { | |||||||
|         const SimpleComponentDefinition = ` |         const SimpleComponentDefinition = ` | ||||||
|           const $_c0$ = [[["", "title", ""]]]; |           const $_c0$ = [[["", "title", ""]]]; | ||||||
|           const $_c1$ = ["[title]"]; |           const $_c1$ = ["[title]"]; | ||||||
|           const $_c2$ = [5, ["", "title", ""]]; |           const $_c2$ = ["ngProjectAs", "[title],[header]", 5, ["", "title", ""]]; | ||||||
|           … |           … | ||||||
|           MyApp.ngComponentDef = $r3$.ɵɵdefineComponent({ |           MyApp.ngComponentDef = $r3$.ɵɵdefineComponent({ | ||||||
|             type: MyApp, |             type: MyApp, | ||||||
|  | |||||||
| @ -497,18 +497,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver | |||||||
|     const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length; |     const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length; | ||||||
|     const parameters: o.Expression[] = [o.literal(slot)]; |     const parameters: o.Expression[] = [o.literal(slot)]; | ||||||
|     const attributes: o.Expression[] = []; |     const attributes: o.Expression[] = []; | ||||||
|  |     let ngProjectAsAttr: t.TextAttribute|undefined; | ||||||
| 
 | 
 | ||||||
|     this._ngContentReservedSlots.push(ngContent.selector); |     this._ngContentReservedSlots.push(ngContent.selector); | ||||||
| 
 | 
 | ||||||
|     ngContent.attributes.forEach((attribute) => { |     ngContent.attributes.forEach((attribute) => { | ||||||
|       const {name, value} = attribute; |       const {name, value} = attribute; | ||||||
|       if (name === NG_PROJECT_AS_ATTR_NAME) { |       if (name === NG_PROJECT_AS_ATTR_NAME) { | ||||||
|         attributes.push(...getNgProjectAsLiteral(attribute)); |         ngProjectAsAttr = attribute; | ||||||
|       } else if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) { |       } | ||||||
|  |       if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) { | ||||||
|         attributes.push(o.literal(name), o.literal(value)); |         attributes.push(o.literal(name), o.literal(value)); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     if (ngProjectAsAttr) { | ||||||
|  |       attributes.push(...getNgProjectAsLiteral(ngProjectAsAttr)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (attributes.length > 0) { |     if (attributes.length > 0) { | ||||||
|       parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes)); |       parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes)); | ||||||
|     } else if (projectionSlotIdx !== 0) { |     } else if (projectionSlotIdx !== 0) { | ||||||
| @ -535,6 +541,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver | |||||||
| 
 | 
 | ||||||
|     const i18nAttrs: (t.TextAttribute | t.BoundAttribute)[] = []; |     const i18nAttrs: (t.TextAttribute | t.BoundAttribute)[] = []; | ||||||
|     const outputAttrs: t.TextAttribute[] = []; |     const outputAttrs: t.TextAttribute[] = []; | ||||||
|  |     let ngProjectAsAttr: t.TextAttribute|undefined; | ||||||
| 
 | 
 | ||||||
|     const [namespaceKey, elementName] = splitNsName(element.name); |     const [namespaceKey, elementName] = splitNsName(element.name); | ||||||
|     const isNgContainer = checkIsNgContainer(element.name); |     const isNgContainer = checkIsNgContainer(element.name); | ||||||
| @ -549,6 +556,9 @@ 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.name === NG_PROJECT_AS_ATTR_NAME) { | ||||||
|  |           ngProjectAsAttr = attr; | ||||||
|  |         } | ||||||
|         if (attr.i18n) { |         if (attr.i18n) { | ||||||
|           // Place attributes into a separate array for i18n processing, but also keep such
 |           // 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.
 |           // attributes in the main list to make them available for directive matching at runtime.
 | ||||||
| @ -590,16 +600,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     outputAttrs.forEach(attr => { |     outputAttrs.forEach(attr => { | ||||||
|       if (attr.name === NG_PROJECT_AS_ATTR_NAME) { |       attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value)); | ||||||
|         attributes.push(...getNgProjectAsLiteral(attr)); |  | ||||||
|       } else { |  | ||||||
|         attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value)); |  | ||||||
|       } |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // add attributes for directive and projection matching purposes
 |     // add attributes for directive and projection matching purposes
 | ||||||
|     attributes.push(...this.prepareNonRenderAttrs( |     attributes.push(...this.prepareNonRenderAttrs( | ||||||
|         allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs)); |         allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs, ngProjectAsAttr)); | ||||||
|     parameters.push(this.toAttrsParam(attributes)); |     parameters.push(this.toAttrsParam(attributes)); | ||||||
| 
 | 
 | ||||||
|     // local refs (ex.: <div #foo #bar="baz">)
 |     // local refs (ex.: <div #foo #bar="baz">)
 | ||||||
| @ -1206,6 +1212,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver | |||||||
|    *   STYLES, style1, value1, style2, value2, |    *   STYLES, style1, value1, style2, value2, | ||||||
|    *   BINDINGS, name1, name2, name3, |    *   BINDINGS, name1, name2, name3, | ||||||
|    *   TEMPLATE, name4, name5, name6, |    *   TEMPLATE, name4, name5, name6, | ||||||
|  |    *   PROJECT_AS, selector, | ||||||
|    *   I18N, name7, name8, ...] |    *   I18N, name7, name8, ...] | ||||||
|    * ``` |    * ``` | ||||||
|    * |    * | ||||||
| @ -1215,7 +1222,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver | |||||||
|   private prepareNonRenderAttrs( |   private prepareNonRenderAttrs( | ||||||
|       inputs: t.BoundAttribute[], outputs: t.BoundEvent[], styles?: StylingBuilder, |       inputs: t.BoundAttribute[], outputs: t.BoundEvent[], styles?: StylingBuilder, | ||||||
|       templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [], |       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<string>(); |     const alreadySeen = new Set<string>(); | ||||||
|     const attrExprs: o.Expression[] = []; |     const attrExprs: o.Expression[] = []; | ||||||
| 
 | 
 | ||||||
| @ -1271,6 +1279,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver | |||||||
|       templateAttrs.forEach(attr => addAttrExpr(attr.name)); |       templateAttrs.forEach(attr => addAttrExpr(attr.name)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (ngProjectAsAttr) { | ||||||
|  |       attrExprs.push(...getNgProjectAsLiteral(ngProjectAsAttr)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (i18nAttrs.length) { |     if (i18nAttrs.length) { | ||||||
|       attrExprs.push(o.literal(core.AttributeMarker.I18n)); |       attrExprs.push(o.literal(core.AttributeMarker.I18n)); | ||||||
|       i18nAttrs.forEach(attr => addAttrExpr(attr.name)); |       i18nAttrs.forEach(attr => addAttrExpr(attr.name)); | ||||||
|  | |||||||
| @ -1025,6 +1025,36 @@ describe('projection', () => { | |||||||
|     expect(fixture.nativeElement.textContent).not.toContain('Title content'); |     expect(fixture.nativeElement.textContent).not.toContain('Title content'); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   it('should preserve ngProjectAs and other attributes on projected element', () => { | ||||||
|  |     @Component({ | ||||||
|  |       selector: 'projector', | ||||||
|  |       template: `<ng-content select="projectMe"></ng-content>`, | ||||||
|  |     }) | ||||||
|  |     class Projector { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Component({ | ||||||
|  |       template: ` | ||||||
|  |         <projector> | ||||||
|  |           <div ngProjectAs="projectMe" title="some title"></div> | ||||||
|  |         </projector> | ||||||
|  |       ` | ||||||
|  |     }) | ||||||
|  |     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)', () => { |   describe('on inline templates (e.g.  *ngIf)', () => { | ||||||
|     it('should work when matching the element name', () => { |     it('should work when matching the element name', () => { | ||||||
|       let divDirectives = 0; |       let divDirectives = 0; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user