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:
Andrew Scott 2020-01-02 09:49:18 -08:00 committed by Alex Rickabaugh
parent 852746e032
commit 4d7a9db44c
3 changed files with 72 additions and 48 deletions

View File

@ -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) {

View File

@ -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));

View File

@ -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',