Skip to content

Commit

Permalink
feat(Compiler): Allow overriding the projection selector
Browse files Browse the repository at this point in the history
fixes #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 #7742
  • Loading branch information
vicb committed Mar 24, 2016
1 parent 3e593b8 commit aa966f5
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 13 deletions.
31 changes: 21 additions & 10 deletions modules/angular2/src/compiler/template_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,29 +264,40 @@ class TemplateParseVisitor implements HtmlAstVisitor {
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directives);
var children = htmlVisitAll(preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this,
element.children, Component.create(directives));
var elementNgContentIndex =
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);

// Override the actual selector when the `ngProjectAs` attribute is provided
var projectionSelector = isPresent(preparsedElement.projectAs) ?
CssSelector.parse(preparsedElement.projectAs)[0] :
elementCssSelector;
var ngContentIndex = component.findNgContentIndex(projectionSelector);
var parsedElement;

if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
if (isPresent(element.children) && element.children.length > 0) {
this._reportError(
`<ng-content> element cannot have content. <ng-content> must be immediately followed by </ng-content>`,
element.sourceSpan);
}
parsedElement =
new NgContentAst(this.ngContentCount++, elementNgContentIndex, element.sourceSpan);

parsedElement = new NgContentAst(
this.ngContentCount++, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} else if (isTemplateElement) {
this._assertAllEventsPublishedByDirectives(directives, events);
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps,
element.sourceSpan);
parsedElement = new EmbeddedTemplateAst(attrs, events, vars, directives, children,
elementNgContentIndex, element.sourceSpan);

parsedElement =
new EmbeddedTemplateAst(attrs, events, vars, directives, children,
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} else {
this._assertOnlyOneComponent(directives, element.sourceSpan);
var elementExportAsVars = vars.filter(varAst => varAst.value.length === 0);
let ngContentIndex =
hasInlineTemplates ? null : component.findNgContentIndex(projectionSelector);

parsedElement =
new ElementAst(nodeName, attrs, elementProps, events, elementExportAsVars, directives,
children, elementNgContentIndex, element.sourceSpan);
children, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
}
if (hasInlineTemplates) {
var templateCssSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
Expand All @@ -297,9 +308,9 @@ class TemplateParseVisitor implements HtmlAstVisitor {
element.name, templateElementOrDirectiveProps, templateDirectives);
this._assertNoComponentsNorElementBindingsOnTemplate(templateDirectives, templateElementProps,
element.sourceSpan);
parsedElement = new EmbeddedTemplateAst(
[], [], templateVars, templateDirectives, [parsedElement],
component.findNgContentIndex(templateCssSelector), element.sourceSpan);

parsedElement = new EmbeddedTemplateAst([], [], templateVars, templateDirectives,
[parsedElement], ngContentIndex, element.sourceSpan);
}
return parsedElement;
}
Expand Down
10 changes: 8 additions & 2 deletions modules/angular2/src/compiler/template_preparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ const LINK_STYLE_REL_VALUE = 'stylesheet';
const STYLE_ELEMENT = 'style';
const SCRIPT_ELEMENT = 'script';
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
const NG_PROJECT_AS = 'ngProjectAs';

export function preparseElement(ast: HtmlElementAst): PreparsedElement {
var selectAttr = null;
var hrefAttr = null;
var relAttr = null;
var nonBindable = false;
var projectAs: string = null;
ast.attrs.forEach(attr => {
let lcAttrName = attr.name.toLowerCase();
if (lcAttrName == NG_CONTENT_SELECT_ATTR) {
Expand All @@ -27,6 +29,10 @@ export function preparseElement(ast: HtmlElementAst): PreparsedElement {
relAttr = attr.value;
} else if (attr.name == NG_NON_BINDABLE_ATTR) {
nonBindable = true;
} else if (attr.name == NG_PROJECT_AS) {
if (attr.value.length > 0) {
projectAs = attr.value;
}
}
});
selectAttr = normalizeNgContentSelect(selectAttr);
Expand All @@ -41,7 +47,7 @@ export function preparseElement(ast: HtmlElementAst): PreparsedElement {
} else if (nodeName == LINK_ELEMENT && relAttr == LINK_STYLE_REL_VALUE) {
type = PreparsedElementType.STYLESHEET;
}
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable);
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable, projectAs);
}

export enum PreparsedElementType {
Expand All @@ -54,7 +60,7 @@ export enum PreparsedElementType {

export class PreparsedElement {
constructor(public type: PreparsedElementType, public selectAttr: string, public hrefAttr: string,
public nonBindable: boolean) {}
public nonBindable: boolean, public projectAs: string) {}
}


Expand Down
33 changes: 33 additions & 0 deletions modules/angular2/test/compiler/template_parser_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,39 @@ There is no directive with "exportAs" set to "dirA" ("<div [ERROR ->]#a="dirA"><
[createComp('div', ['*'])])))
.toEqual([['div', null], ['#text({{hello}})', 0], ['span', 0]]);
});

it('should match the element when there is an inline template', () => {
expect(humanizeContentProjection(
parse('<div><b *ngIf="cond"></b></div>', [createComp('div', ['a', 'b']), ngIf])))
.toEqual([['div', null], ['template', 1], ['b', null]]);
});

describe('ngProjectAs', () => {
it('should override elements', () => {
expect(humanizeContentProjection(
parse('<div><a ngProjectAs="b"></a></div>', [createComp('div', ['a', 'b'])])))
.toEqual([['div', null], ['a', 1]]);
});

it('should override <ng-content>', () => {
expect(humanizeContentProjection(
parse('<div><ng-content ngProjectAs="b"></ng-content></div>',
[createComp('div', ['ng-content', 'b'])])))
.toEqual([['div', null], ['ng-content', 1]]);
});

it('should override <template>', () => {
expect(humanizeContentProjection(parse('<div><template ngProjectAs="b"></template></div>',
[createComp('div', ['template', 'b'])])))
.toEqual([['div', null], ['template', 1]]);
});

it('should override inline templates', () => {
expect(humanizeContentProjection(parse('<div><a *ngIf="cond" ngProjectAs="b"></a></div>',
[createComp('div', ['a', 'b']), ngIf])))
.toEqual([['div', null], ['template', 1], ['a', null]]);
});
});
});

describe('splitClasses', () => {
Expand Down
5 changes: 4 additions & 1 deletion modules/angular2/test/compiler/template_preparser_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function main() {
beforeEach(inject([HtmlParser], (_htmlParser: HtmlParser) => { htmlParser = _htmlParser; }));

function preparse(html: string): PreparsedElement {
return preparseElement(htmlParser.parse(html, '').rootNodes[0]);
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0]);
}

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

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

0 comments on commit aa966f5

Please sign in to comment.