feat(Compiler): Allow overriding the projection selector
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
This commit is contained in:
parent
3e593b8221
commit
aa966f5de2
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) => {
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue