feat(ivy): separate attributes for directive matching purposes (#23991)

In ngIvy directives matching (determining which directives are active based
on a CSS seletor) happens at runtime. This means that runtime needs to have
enough context to match directives. This PR takes care of cases where a directive's
selector should match bindings (ex. [foo]="exp") and event handlers (ex. (out)="do()").
In the mentioned cases we need to have binding / output "attributes" for directive's
CSS selector matching purposes. At the same time those are not regular attributes and
as such should not  be reflected in the DOM.

Closes #23706

PR Close #23991
This commit is contained in:
Pawel Kozlowski
2018-05-04 15:58:42 +02:00
committed by Victor Berchet
parent b87d650da2
commit 90bf5d8961
9 changed files with 349 additions and 74 deletions

View File

@ -22,7 +22,7 @@ import {assertGreaterThan, assertLessThan, assertNotNull} from './assert';
import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, createTView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions';
import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition';
import {LInjector} from './interfaces/injector';
import {LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node';
import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node';
import {QueryReadType} from './interfaces/query';
import {Renderer3} from './interfaces/renderer';
import {LView, TView} from './interfaces/view';
@ -251,7 +251,7 @@ export function injectChangeDetectorRef(): viewEngine_ChangeDetectorRef {
*
* @experimental
*/
export function injectAttribute(attrName: string): string|undefined {
export function injectAttribute(attrNameToInject: string): string|undefined {
ngDevMode && assertPreviousIsParent();
const lElement = getPreviousOrParentNode() as LElementNode;
ngDevMode && assertNodeType(lElement, TNodeType.Element);
@ -260,8 +260,10 @@ export function injectAttribute(attrName: string): string|undefined {
const attrs = tElement.attrs;
if (attrs) {
for (let i = 0; i < attrs.length; i = i + 2) {
if (attrs[i] == attrName) {
return attrs[i + 1];
const attrName = attrs[i];
if (attrName === AttributeMarker.SELECT_ONLY) break;
if (attrName == attrNameToInject) {
return attrs[i + 1] as string;
}
}
}

View File

@ -73,6 +73,10 @@ export {
tick,
} from './instructions';
export {
AttributeMarker
} from './interfaces/node';
export {
pipe as Pp,
pipeBind1 as pb1,

View File

@ -15,7 +15,7 @@ import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interface
import {LQueries} from './interfaces/query';
import {CurrentMatchesList, LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view';
import {LContainerNode, LElementNode, LNode, TNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue, TElementNode,} from './interfaces/node';
import {AttributeMarker, TAttributes, LContainerNode, LElementNode, LNode, TNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue, TElementNode,} from './interfaces/node';
import {assertNodeType} from './node_assert';
import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -366,19 +366,19 @@ export function createLNodeObject(
*/
export function createLNode(
index: number | null, type: TNodeType.Element, native: RElement | RText | null,
name: string | null, attrs: string[] | null, lView?: LView | null): LElementNode;
name: string | null, attrs: TAttributes | null, lView?: LView | null): LElementNode;
export function createLNode(
index: number | null, type: TNodeType.View, native: null, name: null, attrs: null,
lView: LView): LViewNode;
export function createLNode(
index: number, type: TNodeType.Container, native: undefined, name: string | null,
attrs: string[] | null, lContainer: LContainer): LContainerNode;
attrs: TAttributes | null, lContainer: LContainer): LContainerNode;
export function createLNode(
index: number, type: TNodeType.Projection, native: null, name: null, attrs: string[] | null,
index: number, type: TNodeType.Projection, native: null, name: null, attrs: TAttributes | null,
lProjection: LProjection): LProjectionNode;
export function createLNode(
index: number | null, type: TNodeType, native: RText | RElement | null | undefined,
name: string | null, attrs: string[] | null, state?: null | LView | LContainer |
name: string | null, attrs: TAttributes | null, state?: null | LView | LContainer |
LProjection): LElementNode&LTextNode&LViewNode&LContainerNode&LProjectionNode {
const parent = isParent ? previousOrParentNode :
previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode;
@ -586,7 +586,8 @@ function getRenderFlags(view: LView): RenderFlags {
* ['id', 'warning5', 'class', 'alert']
*/
export function elementStart(
index: number, name: string, attrs?: string[] | null, localRefs?: string[] | null): RElement {
index: number, name: string, attrs?: TAttributes | null,
localRefs?: string[] | null): RElement {
ngDevMode &&
assertEqual(
currentView.bindingStartIndex, -1, 'elements should be created before any bindings');
@ -600,23 +601,18 @@ export function elementStart(
if (attrs) setUpAttributes(native, attrs);
appendChild(getParentLNode(node), native, currentView);
createDirectivesAndLocals(index, name, attrs, localRefs, false);
createDirectivesAndLocals(localRefs);
return native;
}
/**
* Creates directive instances and populates local refs.
*
* @param index Index of the current node (to create TNode)
* @param name Tag name of the current node
* @param attrs Attrs of the current node
* @param localRefs Local refs of the current node
* @param inlineViews Whether or not this node will create inline views
*/
function createDirectivesAndLocals(
index: number, name: string | null, attrs: string[] | null | undefined,
localRefs: string[] | null | undefined, inlineViews: boolean) {
function createDirectivesAndLocals(localRefs?: string[] | null) {
const node = previousOrParentNode;
if (firstTemplatePass) {
ngDevMode && ngDevMode.firstTemplatePass++;
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
@ -822,17 +818,18 @@ export function createTView(
};
}
function setUpAttributes(native: RElement, attrs: string[]): void {
ngDevMode && assertEqual(attrs.length % 2, 0, 'each attribute should have a key and a value');
function setUpAttributes(native: RElement, attrs: TAttributes): void {
const isProc = isProceduralRenderer(renderer);
for (let i = 0; i < attrs.length; i += 2) {
const attrName = attrs[i];
if (attrName === AttributeMarker.SELECT_ONLY) break;
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
const attrVal = attrs[i + 1];
ngDevMode && ngDevMode.rendererSetAttribute++;
isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) :
native.setAttribute(attrName, attrVal);
isProc ?
(renderer as ProceduralRenderer3)
.setAttribute(native, attrName as string, attrVal as string) :
native.setAttribute(attrName as string, attrVal as string);
}
}
}
@ -1046,7 +1043,7 @@ export function elementProperty<T>(
* @returns the TNode object
*/
export function createTNode(
type: TNodeType, index: number | null, tagName: string | null, attrs: string[] | null,
type: TNodeType, index: number | null, tagName: string | null, attrs: TAttributes | null,
parent: TElementNode | TContainerNode | null, tViews: TView[] | null): TNode {
ngDevMode && ngDevMode.tNode++;
return {
@ -1442,10 +1439,13 @@ function generateInitialInputs(
for (let i = 0; i < attrs.length; i += 2) {
const attrName = attrs[i];
const minifiedInputName = inputs[attrName];
const attrValue = attrs[i + 1];
if (attrName === AttributeMarker.SELECT_ONLY) break;
if (minifiedInputName !== undefined) {
const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(minifiedInputName, attrs[i + 1]);
inputsToStore.push(minifiedInputName, attrValue as string);
}
}
return initialInputData;
@ -1484,7 +1484,7 @@ export function createLContainer(
* @param localRefs A set of local reference bindings on the element.
*/
export function container(
index: number, template?: ComponentTemplate<any>, tagName?: string | null, attrs?: string[],
index: number, template?: ComponentTemplate<any>, tagName?: string | null, attrs?: TAttributes,
localRefs?: string[] | null): void {
ngDevMode && assertEqual(
currentView.bindingStartIndex, -1,
@ -1501,7 +1501,7 @@ export function container(
// Containers are added to the current view tree instead of their embedded views
// because views can be removed and re-inserted.
addToViewTree(currentView, node.data);
createDirectivesAndLocals(index, tagName || null, attrs, localRefs, template == null);
createDirectivesAndLocals(localRefs);
isParent = false;
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container);

View File

@ -158,6 +158,29 @@ export interface LProjectionNode extends LNode {
dynamicLContainerNode: null;
}
/**
* A set of marker values to be used in the attributes arrays. Those markers indicate that some
* items are not regular attributes and the processing should be adapted accordingly.
*/
export const enum AttributeMarker {
NS = 0, // namespace. Has to be repeated.
/**
* This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
* Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SELECT_ONLY, 'foo', 'bar']
*/
SELECT_ONLY = 1
}
/**
* A combination of:
* - attribute names and values
* - special markers acting as flags to alter attributes processing.
*/
export type TAttributes = (string | AttributeMarker)[];
/**
* LNode binding data (flyweight) for a particular node that is shared between all templates
* of a specific type.
@ -198,18 +221,20 @@ export interface TNode {
tagName: string|null;
/**
* Static attributes associated with an element. We need to store
* static attributes to support content projection with selectors.
* Attributes are stored statically because reading them from the DOM
* would be way too slow for content projection and queries.
* Attributes associated with an element. We need to store attributes to support various use-cases
* (attribute injection, content projection with selectors, directives matching).
* Attributes are stored statically because reading them from the DOM would be way too slow for
* content projection and queries.
*
* Since attrs will always be calculated first, they will never need
* to be marked undefined by other instructions.
* Since attrs will always be calculated first, they will never need to be marked undefined by
* other instructions.
*
* The name of the attribute and its value alternate in the array.
* For regular attributes a name of an attribute and its value alternate in the array.
* e.g. ['role', 'checkbox']
* This array can contain flags that will indicate "special attributes" (attributes with
* namespaces, attributes extracted from bindings and outputs).
*/
attrs: string[]|null;
attrs: TAttributes|null;
/**
* A set of local names under which a given element is exported in a template and

View File

@ -9,7 +9,7 @@
import './ng_dev_mode';
import {assertNotNull} from './assert';
import {TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
import {AttributeMarker, TAttributes, TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection';
const unusedValueToPlacateAjd = unused1 + unused2;
@ -40,6 +40,7 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
let mode: SelectorFlags = SelectorFlags.ELEMENT;
const nodeAttrs = tNode.attrs !;
const selectOnlyMarkerIdx = nodeAttrs ? nodeAttrs.indexOf(AttributeMarker.SELECT_ONLY) : -1;
// When processing ":not" selectors, we skip to the next ":not" if the
// current one doesn't match
@ -80,9 +81,11 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo
const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i];
if (selectorAttrValue !== '') {
const nodeAttrValue = nodeAttrs[attrIndexInNode + 1];
const nodeAttrValue = selectOnlyMarkerIdx > -1 && attrIndexInNode > selectOnlyMarkerIdx ?
'' :
nodeAttrs[attrIndexInNode + 1];
if (mode & SelectorFlags.CLASS &&
!isCssClassMatching(nodeAttrValue, selectorAttrValue as string) ||
!isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) ||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
if (isPositive(mode)) return false;
skipToNextSelector = true;
@ -98,10 +101,15 @@ function isPositive(mode: SelectorFlags): boolean {
return (mode & SelectorFlags.NOT) === 0;
}
function findAttrIndexInNode(name: string, attrs: string[] | null): number {
function findAttrIndexInNode(name: string, attrs: TAttributes | null): number {
let step = 2;
if (attrs === null) return -1;
for (let i = 0; i < attrs.length; i += 2) {
if (attrs[i] === name) return i;
for (let i = 0; i < attrs.length; i += step) {
const attrName = attrs[i];
if (attrName === name) return i;
if (attrName === AttributeMarker.SELECT_ONLY) {
step = 1;
}
}
return -1;
}
@ -123,7 +131,7 @@ export function getProjectAsAttrValue(tNode: TNode): string|null {
// 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 nodeAttrs[ngProjectAsAttrIdx + 1] as string;
}
}
return null;