diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 66a4dcd5dd..cb76bd2027 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -15,9 +15,9 @@ import {LQueries} from './interfaces/query'; import {LView, LifecycleStage, TData, TView} from './interfaces/view'; import {LContainerNode, LElementNode, LNode, LNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './interfaces/node'; -import {assertNodeType, assertNodeOfPossibleTypes} from './node_assert'; +import {assertNodeType} from './node_assert'; import {appendChild, insertChild, insertView, appendProjectedNode, removeView, canInsertNativeNode} from './node_manipulation'; -import {isNodeMatchingSelector} from './node_selector_matcher'; +import {matchingSelectorIndex} from './node_selector_matcher'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType} from './interfaces/definition'; import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, ObjectOrientedRenderer3, RendererStyleFlags3} from './interfaces/renderer'; import {isDifferent, stringify} from './util'; @@ -1222,33 +1222,16 @@ export function projectionDef(index: number, selectors?: CssSelector[]): void { let componentChild = componentNode.child; while (componentChild !== null) { - if (!selectors) { - distributedNodes[0].push(componentChild); - } else if ( - (componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element || - (componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container) { - // Only trying to match selectors against: - // - elements, excluding text nodes; - // - containers that have tagName and attributes associated. - - if (componentChild.tNode) { - for (let i = 0; i < selectors !.length; i++) { - if (isNodeMatchingSelector(componentChild.tNode, selectors ![i])) { - distributedNodes[i + 1].push(componentChild); - break; // first matching selector "captures" a given node - } else { - distributedNodes[0].push(componentChild); - } - } - } else { - distributedNodes[0].push(componentChild); - } - - } else if ((componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Projection) { - // we don't descend into nodes to re-project (not trying to match selectors against nodes to - // re-project) + // execute selector matching logic if and only if: + // - there are selectors defined + // - a node has a tag name / attributes that can be matched + if (selectors && componentChild.tNode) { + const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors !); + distributedNodes[matchedIdx].push(componentChild); + } else { distributedNodes[0].push(componentChild); } + componentChild = componentChild.next; } @@ -1291,9 +1274,16 @@ function appendToProjectionNode( * @param nodeIndex * @param localIndex - index under which distribution of projected nodes was memorized * @param selectorIndex - 0 means without any selector + * @param attrs - attributes attached to the ng-content node, if present */ -export function projection(nodeIndex: number, localIndex: number, selectorIndex: number = 0): void { +export function projection( + nodeIndex: number, localIndex: number, selectorIndex: number = 0, attrs?: string[]): void { const node = createLNode(nodeIndex, LNodeFlags.Projection, null, {head: null, tail: null}); + + if (node.tNode == null) { + node.tNode = createTNode(null, attrs || null, null, null); + } + isParent = false; // self closing const currentParent = node.parent; diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index 92af7f25f1..ce83368f69 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -114,3 +114,16 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo return false; } + +/** + * Checks a given node against matching selectors and returns + * selector index (or 0 if none matched); + */ +export function matchingSelectorIndex(tNode: TNode, selectors: CssSelector[]): number { + for (let i = 0; i < selectors.length; i++) { + if (isNodeMatchingSelector(tNode, selectors[i])) { + return i + 1; // first matching selector "captures" a given node + } + } + return 0; +} diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index b94b08713d..043365ac5e 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -812,7 +812,7 @@ describe('content projection', () => { * Descending into projected content for selector-matching purposes is not supported * today: http://plnkr.co/edit/MYQcNfHSTKp9KvbzJWVQ?p=preview */ - it('should not match selectors on re-projected content', () => { + it('should not descend into re-projected content', () => { /** * @@ -878,6 +878,66 @@ describe('content projection', () => { 'in child template
parent content
'); }); + it('should match selectors on ng-content nodes with attributes', () => { + + /** + * + *
+ * + */ + const Card = createComponent('card', function(ctx: any, cm: boolean) { + if (cm) { + pD(0, [[[['', 'card-title', ''], null]], [[['', 'card-content', ''], null]]]); + P(1, 0, 1); + E(2, 'hr'); + e(); + P(3, 0, 2); + } + }); + + /** + * + *

Title

+ * + *
+ */ + const CardWithTitle = createComponent('card-with-title', function(ctx: any, cm: boolean) { + if (cm) { + pD(0); + E(1, Card); + { + E(3, 'h1', ['card-title', '']); + { T(4, 'Title'); } + e(); + P(5, 0, 0, ['card-content', '']); + } + e(); + Card.ngComponentDef.h(2, 1); + r(2, 1); + } + }); + + /** + * + * content + * + */ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + E(0, CardWithTitle); + { T(2, 'content'); } + e(); + } + CardWithTitle.ngComponentDef.h(1, 0); + r(1, 0); + }); + + const app = renderComponent(App); + expect(toHtml(app)) + .toEqual( + '

Title


content
'); + }); + it('should match selectors against projected containers', () => { /**