feat(ivy): add support for the ngProjectAs attribute (#22498)

PR Close #22498
This commit is contained in:
Pawel Kozlowski
2018-02-28 15:00:58 +01:00
committed by Alex Eagle
parent f86d8ae0fd
commit 2c75acc5b3
6 changed files with 197 additions and 24 deletions

View File

@ -11,7 +11,7 @@ import './ng_dev_mode';
import {assertEqual, assertLessThan, assertNotEqual, assertNotNull, assertNull, assertSame} from './assert';
import {LContainer, TContainer} from './interfaces/container';
import {LInjector} from './interfaces/injector';
import {CssSelector, LProjection} from './interfaces/projection';
import {CssSelector, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query';
import {LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view';
@ -560,8 +560,12 @@ function setUpAttributes(native: RElement, attrs: string[]): void {
const isProc = isProceduralRenderer(renderer);
for (let i = 0; i < attrs.length; i += 2) {
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrs[i], attrs[i | 1]) :
native.setAttribute(attrs[i], attrs[i | 1]);
const attrName = attrs[i];
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
const attrVal = attrs[i + 1];
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) :
native.setAttribute(attrName, attrVal);
}
}
}
@ -1279,9 +1283,23 @@ export function directiveRefresh<T>(directiveIndex: number, elementIndex: number
* each projected node belongs (it re-distributes nodes among "buckets" where each "bucket" is
* backed by a selector).
*
* @param selectors
* This function requires CSS selectors to be provided in 2 forms: parsed (by a compiler) and text,
* un-parsed form.
*
* The parsed form is needed for efficient matching of a node against a given CSS selector.
* The un-parsed, textual form is needed for support of the ngProjectAs attribute.
*
* Having a CSS selector in 2 different formats is not ideal, but alternatives have even more
* drawbacks:
* - having only a textual form would require runtime parsing of CSS selectors;
* - we can't have only a parsed as we can't re-construct textual form from it (as entered by a
* template author).
*
* @param selectors A collection of parsed CSS selectors
* @param rawSelectors A collection of CSS selectors in the raw, un-parsed form
*/
export function projectionDef(index: number, selectors?: CssSelector[]): void {
export function projectionDef(
index: number, selectors?: CssSelector[], textSelectors?: string[]): void {
const noOfNodeBuckets = selectors ? selectors.length + 1 : 1;
const distributedNodes = new Array<LNode[]>(noOfNodeBuckets);
for (let i = 0; i < noOfNodeBuckets; i++) {
@ -1296,7 +1314,7 @@ export function projectionDef(index: number, selectors?: CssSelector[]): void {
// - 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 !);
const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !);
distributedNodes[matchedIdx].push(componentChild);
} else {
distributedNodes[0].push(componentChild);

View File

@ -45,6 +45,8 @@ export type CssSelectorWithNegations = [SimpleCssSelector | null, SimpleCssSelec
*/
export type CssSelector = CssSelectorWithNegations[];
export const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs';
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
export const unusedValueExportToPlacateAjd = 1;

View File

@ -10,7 +10,7 @@ import './ng_dev_mode';
import {assertNotNull} from './assert';
import {TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
import {CssSelector, CssSelectorWithNegations, SimpleCssSelector, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
import {CssSelector, CssSelectorWithNegations, NG_PROJECT_AS_ATTR_NAME, SimpleCssSelector, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
const unusedValueToPlacateAjd = unused1 + unused2;
@ -115,13 +115,34 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
return false;
}
export function getProjectAsAttrValue(tNode: TNode): string|null {
const nodeAttrs = tNode.attrs;
if (nodeAttrs != null) {
const ngProjectAsAttrIdx = nodeAttrs.indexOf(NG_PROJECT_AS_ATTR_NAME);
// only check for ngProjectAs in attribute names, don't accidentally match attribute's value
// (attribute names are stored at even indexes)
if ((ngProjectAsAttrIdx & 1) === 0) {
return nodeAttrs[ngProjectAsAttrIdx + 1];
}
}
return null;
}
/**
* Checks a given node against matching selectors and returns
* selector index (or 0 if none matched);
* selector index (or 0 if none matched).
*
* This function takes into account the ngProjectAs attribute: if present its value will be compared
* to the raw (un-parsed) CSS selector instead of using standard selector matching logic.
*/
export function matchingSelectorIndex(tNode: TNode, selectors: CssSelector[]): number {
export function matchingSelectorIndex(
tNode: TNode, selectors: CssSelector[], textSelectors: string[]): number {
const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
for (let i = 0; i < selectors.length; i++) {
if (isNodeMatchingSelector(tNode, selectors[i])) {
// if a node has the ngProjectAs attribute match it against unparsed selector
// match a node against a parsed selector only if ngProjectAs attribute is not present
if (ngProjectAsAttrVal === textSelectors[i] ||
ngProjectAsAttrVal === null && isNodeMatchingSelector(tNode, selectors[i])) {
return i + 1; // first matching selector "captures" a given node
}
}