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:
Andrew Kushnir 2019-09-19 17:22:06 -07:00 committed by atscott
parent 278d634723
commit 966c2a326a
3 changed files with 53 additions and 11 deletions

View File

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

View File

@ -497,18 +497,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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<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);
@ -549,6 +556,9 @@ 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.
@ -590,16 +600,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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));
}
});
// 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.: <div #foo #bar="baz">)
@ -1206,6 +1212,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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<void>, 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<string>();
const attrExprs: o.Expression[] = [];
@ -1271,6 +1279,10 @@ 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

@ -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: `<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)', () => {
it('should work when matching the element name', () => {
let divDirectives = 0;