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:

committed by
Victor Berchet

parent
b87d650da2
commit
90bf5d8961
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,10 @@ export {
|
||||
tick,
|
||||
} from './instructions';
|
||||
|
||||
export {
|
||||
AttributeMarker
|
||||
} from './interfaces/node';
|
||||
|
||||
export {
|
||||
pipe as Pp,
|
||||
pipeBind1 as pb1,
|
||||
|
@ -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<extNode&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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user