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…
Reference in New Issue