fix(ivy): Ensure ngProjectAs marker name appears at even attribute index (#34617)
The `getProjectAsAttrValue` in `node_selector_matcher` finds the ProjectAs marker and then additionally checks that the marker appears in an even index of the node attributes because "attribute names are stored at even indexes". This is true for "regular" attribute bindings but classes, styles, bindings, templates, and i18n do not necessarily follow this rule because there can be an uneven number of them, causing the next "special" attribute "name" to appear at an odd index. To address this issue, ensure ngProjectAs is placed right after "regular" attributes. PR Close #34617
This commit is contained in:
parent
852746e032
commit
4d7a9db44c
|
@ -1536,7 +1536,7 @@ describe('compiler compliance', () => {
|
|||
decls: 1,
|
||||
vars: 1,
|
||||
consts: [
|
||||
["ngProjectAs", ".someclass", ${AttributeMarker.Template}, "ngIf", ${AttributeMarker.ProjectAs}, ["", 8, "someclass"]],
|
||||
["ngProjectAs", ".someclass", ${AttributeMarker.ProjectAs}, ["", 8, "someclass"], ${AttributeMarker.Template}, "ngIf"],
|
||||
["ngProjectAs", ".someclass", ${AttributeMarker.ProjectAs}, ["", 8, "someclass"]]
|
||||
],
|
||||
template: function MyApp_Template(rf, ctx) {
|
||||
|
|
|
@ -499,24 +499,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const slot = this.allocateDataSlot();
|
||||
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) {
|
||||
ngProjectAsAttr = attribute;
|
||||
}
|
||||
if (name.toLowerCase() !== NG_CONTENT_SELECT_ATTR) {
|
||||
attributes.push(o.literal(name), o.literal(value));
|
||||
}
|
||||
});
|
||||
|
||||
if (ngProjectAsAttr) {
|
||||
attributes.push(...getNgProjectAsLiteral(ngProjectAsAttr));
|
||||
}
|
||||
const nonContentSelectAttributes =
|
||||
ngContent.attributes.filter(attr => attr.name.toLowerCase() !== NG_CONTENT_SELECT_ATTR);
|
||||
const attributes = this.getAttributeExpressions(nonContentSelectAttributes, [], []);
|
||||
|
||||
if (attributes.length > 0) {
|
||||
parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes));
|
||||
|
@ -540,7 +528,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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);
|
||||
|
@ -555,9 +542,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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.
|
||||
|
@ -580,7 +564,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
}
|
||||
|
||||
// Add the attributes
|
||||
const attributes: o.Expression[] = [];
|
||||
const allOtherInputs: t.BoundAttribute[] = [];
|
||||
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
|
@ -598,13 +581,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
}
|
||||
});
|
||||
|
||||
outputAttrs.forEach(attr => {
|
||||
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, ngProjectAsAttr));
|
||||
const attributes: o.Expression[] = this.getAttributeExpressions(
|
||||
outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs);
|
||||
parameters.push(this.addAttrsToConsts(attributes));
|
||||
|
||||
// local refs (ex.: <div #foo #bar="baz">)
|
||||
|
@ -844,7 +823,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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);
|
||||
|
@ -867,16 +845,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
this.matchDirectives(NG_TEMPLATE_TAG_NAME, template);
|
||||
|
||||
// prepare attributes parameter (including attributes used for directive matching)
|
||||
const attrsExprs: o.Expression[] = [];
|
||||
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, undefined,
|
||||
ngProjectAsAttr));
|
||||
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
|
||||
template.attributes, template.inputs, template.outputs, undefined, template.templateAttrs,
|
||||
undefined);
|
||||
parameters.push(this.addAttrsToConsts(attrsExprs));
|
||||
|
||||
// local refs (ex.: <ng-template #foo>)
|
||||
|
@ -1243,24 +1214,37 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
*
|
||||
* ```
|
||||
* attrs = [prop, value, prop2, value2,
|
||||
* PROJECT_AS, selector,
|
||||
* CLASSES, class1, class2,
|
||||
* STYLES, style1, value1, style2, value2,
|
||||
* BINDINGS, name1, name2, name3,
|
||||
* TEMPLATE, name4, name5, name6,
|
||||
* PROJECT_AS, selector,
|
||||
* I18N, name7, name8, ...]
|
||||
* ```
|
||||
*
|
||||
* Note that this function will fully ignore all synthetic (@foo) attribute values
|
||||
* because those values are intended to always be generated as property instructions.
|
||||
*/
|
||||
private prepareNonRenderAttrs(
|
||||
inputs: t.BoundAttribute[], outputs: t.BoundEvent[], styles?: StylingBuilder,
|
||||
templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
|
||||
i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
|
||||
ngProjectAsAttr?: t.TextAttribute): o.Expression[] {
|
||||
private getAttributeExpressions(
|
||||
renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
|
||||
styles?: StylingBuilder, templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
|
||||
i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] {
|
||||
const alreadySeen = new Set<string>();
|
||||
const attrExprs: o.Expression[] = [];
|
||||
let ngProjectAsAttr: t.TextAttribute|undefined;
|
||||
|
||||
renderAttributes.forEach((attr: t.TextAttribute) => {
|
||||
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
|
||||
ngProjectAsAttr = attr;
|
||||
}
|
||||
attrExprs.push(...getAttributeNameLiterals(attr.name), asLiteral(attr.value));
|
||||
});
|
||||
|
||||
// Keep ngProjectAs next to the other name, value pairs so we can verify that we match
|
||||
// ngProjectAs marker in the attribute name slot.
|
||||
if (ngProjectAsAttr) {
|
||||
attrExprs.push(...getNgProjectAsLiteral(ngProjectAsAttr));
|
||||
}
|
||||
|
||||
function addAttrExpr(key: string | number, value?: o.Expression): void {
|
||||
if (typeof key === 'string') {
|
||||
|
@ -1314,10 +1298,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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));
|
||||
|
|
|
@ -955,6 +955,50 @@ describe('projection', () => {
|
|||
expect(fixture.nativeElement).toHaveText('hello');
|
||||
});
|
||||
|
||||
it('should support ngProjectAs with a various number of other bindings and attributes', () => {
|
||||
@Directive({selector: '[color],[margin]'})
|
||||
class ElDecorator {
|
||||
@Input() color?: string;
|
||||
@Input() margin?: number;
|
||||
}
|
||||
@Component({
|
||||
selector: 'card',
|
||||
template: `
|
||||
<ng-content select="[card-title]"></ng-content>
|
||||
---
|
||||
<ng-content select="[card-subtitle]"></ng-content>
|
||||
---
|
||||
<ng-content select="[card-content]"></ng-content>
|
||||
---
|
||||
<ng-content select="[card-footer]"></ng-content>
|
||||
`
|
||||
})
|
||||
class Card {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'card-with-title',
|
||||
template: `
|
||||
<card>
|
||||
<h1 [color]="'red'" [margin]="10" ngProjectAs="[card-title]">Title</h1>
|
||||
<h2 xlink:href="google.com" ngProjectAs="[card-subtitle]">Subtitle</h2>
|
||||
<div style="font-color: blue;" ngProjectAs="[card-content]">content</div>
|
||||
<div [color]="'blue'" ngProjectAs="[card-footer]">footer</div>
|
||||
</card>
|
||||
`
|
||||
})
|
||||
class CardWithTitle {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Card, CardWithTitle, ElDecorator]});
|
||||
const fixture = TestBed.createComponent(CardWithTitle);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
|
||||
expect(fixture.nativeElement.textContent)
|
||||
.toContain('Title --- Subtitle --- content --- footer');
|
||||
});
|
||||
|
||||
it('should support ngProjectAs on elements (including <ng-content>)', () => {
|
||||
@Component({
|
||||
selector: 'card',
|
||||
|
|
Loading…
Reference in New Issue