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 $_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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue