feat(ivy): add support for attributes on ng-content nodes (#21935)
By adding attributes on the <ng-content> element template authors can decide how content should be re-projected (or, in other words: which selectors should match re-projected content). PR Close #21935
This commit is contained in:
parent
61abba4bed
commit
1aa2947f70
|
@ -15,9 +15,9 @@ import {LQueries} from './interfaces/query';
|
||||||
import {LView, LifecycleStage, TData, TView} from './interfaces/view';
|
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 {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 {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 {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType} from './interfaces/definition';
|
||||||
import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, ObjectOrientedRenderer3, RendererStyleFlags3} from './interfaces/renderer';
|
import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, ObjectOrientedRenderer3, RendererStyleFlags3} from './interfaces/renderer';
|
||||||
import {isDifferent, stringify} from './util';
|
import {isDifferent, stringify} from './util';
|
||||||
|
@ -1222,33 +1222,16 @@ export function projectionDef(index: number, selectors?: CssSelector[]): void {
|
||||||
let componentChild = componentNode.child;
|
let componentChild = componentNode.child;
|
||||||
|
|
||||||
while (componentChild !== null) {
|
while (componentChild !== null) {
|
||||||
if (!selectors) {
|
// execute selector matching logic if and only if:
|
||||||
distributedNodes[0].push(componentChild);
|
// - there are selectors defined
|
||||||
} else if (
|
// - a node has a tag name / attributes that can be matched
|
||||||
(componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element ||
|
if (selectors && componentChild.tNode) {
|
||||||
(componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container) {
|
const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors !);
|
||||||
// Only trying to match selectors against:
|
distributedNodes[matchedIdx].push(componentChild);
|
||||||
// - 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 {
|
} else {
|
||||||
distributedNodes[0].push(componentChild);
|
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)
|
|
||||||
distributedNodes[0].push(componentChild);
|
|
||||||
}
|
|
||||||
componentChild = componentChild.next;
|
componentChild = componentChild.next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1291,9 +1274,16 @@ function appendToProjectionNode(
|
||||||
* @param nodeIndex
|
* @param nodeIndex
|
||||||
* @param localIndex - index under which distribution of projected nodes was memorized
|
* @param localIndex - index under which distribution of projected nodes was memorized
|
||||||
* @param selectorIndex - 0 means <ng-content> without any selector
|
* @param selectorIndex - 0 means <ng-content> 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});
|
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
|
isParent = false; // self closing
|
||||||
const currentParent = node.parent;
|
const currentParent = node.parent;
|
||||||
|
|
||||||
|
|
|
@ -114,3 +114,16 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
|
||||||
|
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -812,7 +812,7 @@ describe('content projection', () => {
|
||||||
* Descending into projected content for selector-matching purposes is not supported
|
* Descending into projected content for selector-matching purposes is not supported
|
||||||
* today: http://plnkr.co/edit/MYQcNfHSTKp9KvbzJWVQ?p=preview
|
* 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', () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <ng-content select="span"></ng-content>
|
* <ng-content select="span"></ng-content>
|
||||||
|
@ -878,6 +878,66 @@ describe('content projection', () => {
|
||||||
'<child><grand-child><span>in child template</span><hr><span>parent content</span></grand-child></child>');
|
'<child><grand-child><span>in child template</span><hr><span>parent content</span></grand-child></child>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should match selectors on ng-content nodes with attributes', () => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ng-content select="[card-title]"></ng-content>
|
||||||
|
* <hr>
|
||||||
|
* <ng-content select="[card-content]"></ng-content>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <card>
|
||||||
|
* <h1 card-title>Title</h1>
|
||||||
|
* <ng-content card-content></ng-content>
|
||||||
|
* </card>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <card-with-title>
|
||||||
|
* content
|
||||||
|
* </card-with-title>
|
||||||
|
*/
|
||||||
|
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(
|
||||||
|
'<card-with-title><card><h1 card-title="">Title</h1><hr>content</card></card-with-title>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should match selectors against projected containers', () => {
|
it('should match selectors against projected containers', () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue