Skip to content

Commit aa966f5

Browse files
committed
feat(Compiler): Allow overriding the projection selector
fixes angular#6303 BREAKING CHANGE: For static content projection, elements with *-directives are now matched against the element itself vs the template before. <p *ngIf="condition" foo></p> Before: // Use the implicit template for projection <ng-content select="template"></ng-content> After: // Use the actual element for projection <ng-content select="p[foo]"></ng-content> Closes angular#7742
1 parent 3e593b8 commit aa966f5

File tree

4 files changed

+66
-13
lines changed

4 files changed

+66
-13
lines changed

modules/angular2/src/compiler/template_parser.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,29 +264,40 @@ class TemplateParseVisitor implements HtmlAstVisitor {
264264
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directives);
265265
var children = htmlVisitAll(preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this,
266266
element.children, Component.create(directives));
267-
var elementNgContentIndex =
268-
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);
267+
268+
// Override the actual selector when the `ngProjectAs` attribute is provided
269+
var projectionSelector = isPresent(preparsedElement.projectAs) ?
270+
CssSelector.parse(preparsedElement.projectAs)[0] :
271+
elementCssSelector;
272+
var ngContentIndex = component.findNgContentIndex(projectionSelector);
269273
var parsedElement;
274+
270275
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
271276
if (isPresent(element.children) && element.children.length > 0) {
272277
this._reportError(
273278
`<ng-content> element cannot have content. <ng-content> must be immediately followed by </ng-content>`,
274279
element.sourceSpan);
275280
}
276-
parsedElement =
277-
new NgContentAst(this.ngContentCount++, elementNgContentIndex, element.sourceSpan);
281+
282+
parsedElement = new NgContentAst(
283+
this.ngContentCount++, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
278284
} else if (isTemplateElement) {
279285
this._assertAllEventsPublishedByDirectives(directives, events);
280286
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps,
281287
element.sourceSpan);
282-
parsedElement = new EmbeddedTemplateAst(attrs, events, vars, directives, children,
283-
elementNgContentIndex, element.sourceSpan);
288+
289+
parsedElement =
290+
new EmbeddedTemplateAst(attrs, events, vars, directives, children,
291+
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
284292
} else {
285293
this._assertOnlyOneComponent(directives, element.sourceSpan);
286294
var elementExportAsVars = vars.filter(varAst => varAst.value.length === 0);
295+
let ngContentIndex =
296+
hasInlineTemplates ? null : component.findNgContentIndex(projectionSelector);
297+
287298
parsedElement =
288299
new ElementAst(nodeName, attrs, elementProps, events, elementExportAsVars, directives,
289-
children, elementNgContentIndex, element.sourceSpan);
300+
children, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
290301
}
291302
if (hasInlineTemplates) {
292303
var templateCssSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
@@ -297,9 +308,9 @@ class TemplateParseVisitor implements HtmlAstVisitor {
297308
element.name, templateElementOrDirectiveProps, templateDirectives);
298309
this._assertNoComponentsNorElementBindingsOnTemplate(templateDirectives, templateElementProps,
299310
element.sourceSpan);
300-
parsedElement = new EmbeddedTemplateAst(
301-
[], [], templateVars, templateDirectives, [parsedElement],
302-
component.findNgContentIndex(templateCssSelector), element.sourceSpan);
311+
312+
parsedElement = new EmbeddedTemplateAst([], [], templateVars, templateDirectives,
313+
[parsedElement], ngContentIndex, element.sourceSpan);
303314
}
304315
return parsedElement;
305316
}

modules/angular2/src/compiler/template_preparser.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ const LINK_STYLE_REL_VALUE = 'stylesheet';
1111
const STYLE_ELEMENT = 'style';
1212
const SCRIPT_ELEMENT = 'script';
1313
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
14+
const NG_PROJECT_AS = 'ngProjectAs';
1415

1516
export function preparseElement(ast: HtmlElementAst): PreparsedElement {
1617
var selectAttr = null;
1718
var hrefAttr = null;
1819
var relAttr = null;
1920
var nonBindable = false;
21+
var projectAs: string = null;
2022
ast.attrs.forEach(attr => {
2123
let lcAttrName = attr.name.toLowerCase();
2224
if (lcAttrName == NG_CONTENT_SELECT_ATTR) {
@@ -27,6 +29,10 @@ export function preparseElement(ast: HtmlElementAst): PreparsedElement {
2729
relAttr = attr.value;
2830
} else if (attr.name == NG_NON_BINDABLE_ATTR) {
2931
nonBindable = true;
32+
} else if (attr.name == NG_PROJECT_AS) {
33+
if (attr.value.length > 0) {
34+
projectAs = attr.value;
35+
}
3036
}
3137
});
3238
selectAttr = normalizeNgContentSelect(selectAttr);
@@ -41,7 +47,7 @@ export function preparseElement(ast: HtmlElementAst): PreparsedElement {
4147
} else if (nodeName == LINK_ELEMENT && relAttr == LINK_STYLE_REL_VALUE) {
4248
type = PreparsedElementType.STYLESHEET;
4349
}
44-
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable);
50+
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable, projectAs);
4551
}
4652

4753
export enum PreparsedElementType {
@@ -54,7 +60,7 @@ export enum PreparsedElementType {
5460

5561
export class PreparsedElement {
5662
constructor(public type: PreparsedElementType, public selectAttr: string, public hrefAttr: string,
57-
public nonBindable: boolean) {}
63+
public nonBindable: boolean, public projectAs: string) {}
5864
}
5965

6066

modules/angular2/test/compiler/template_parser_spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,39 @@ There is no directive with "exportAs" set to "dirA" ("<div [ERROR ->]#a="dirA"><
686686
[createComp('div', ['*'])])))
687687
.toEqual([['div', null], ['#text({{hello}})', 0], ['span', 0]]);
688688
});
689+
690+
it('should match the element when there is an inline template', () => {
691+
expect(humanizeContentProjection(
692+
parse('<div><b *ngIf="cond"></b></div>', [createComp('div', ['a', 'b']), ngIf])))
693+
.toEqual([['div', null], ['template', 1], ['b', null]]);
694+
});
695+
696+
describe('ngProjectAs', () => {
697+
it('should override elements', () => {
698+
expect(humanizeContentProjection(
699+
parse('<div><a ngProjectAs="b"></a></div>', [createComp('div', ['a', 'b'])])))
700+
.toEqual([['div', null], ['a', 1]]);
701+
});
702+
703+
it('should override <ng-content>', () => {
704+
expect(humanizeContentProjection(
705+
parse('<div><ng-content ngProjectAs="b"></ng-content></div>',
706+
[createComp('div', ['ng-content', 'b'])])))
707+
.toEqual([['div', null], ['ng-content', 1]]);
708+
});
709+
710+
it('should override <template>', () => {
711+
expect(humanizeContentProjection(parse('<div><template ngProjectAs="b"></template></div>',
712+
[createComp('div', ['template', 'b'])])))
713+
.toEqual([['div', null], ['template', 1]]);
714+
});
715+
716+
it('should override inline templates', () => {
717+
expect(humanizeContentProjection(parse('<div><a *ngIf="cond" ngProjectAs="b"></a></div>',
718+
[createComp('div', ['a', 'b']), ngIf])))
719+
.toEqual([['div', null], ['template', 1], ['a', null]]);
720+
});
721+
});
689722
});
690723

691724
describe('splitClasses', () => {

modules/angular2/test/compiler/template_preparser_spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function main() {
2626
beforeEach(inject([HtmlParser], (_htmlParser: HtmlParser) => { htmlParser = _htmlParser; }));
2727

2828
function preparse(html: string): PreparsedElement {
29-
return preparseElement(htmlParser.parse(html, '').rootNodes[0]);
29+
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0]);
3030
}
3131

3232
it('should detect script elements', inject([HtmlParser], (htmlParser: HtmlParser) => {
@@ -54,5 +54,8 @@ export function main() {
5454
expect(preparse('<ng-content select="*">').selectAttr).toEqual('*');
5555
}));
5656

57+
it('should extract ngProjectAs value', () => {
58+
expect(preparse('<p ngProjectAs="el[attr].class"></p>').projectAs).toEqual('el[attr].class');
59+
});
5760
});
5861
}

0 commit comments

Comments
 (0)