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:
		
							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 {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, createTView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; | ||||||
| import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition'; | import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition'; | ||||||
| import {LInjector} from './interfaces/injector'; | 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 {QueryReadType} from './interfaces/query'; | ||||||
| import {Renderer3} from './interfaces/renderer'; | import {Renderer3} from './interfaces/renderer'; | ||||||
| import {LView, TView} from './interfaces/view'; | import {LView, TView} from './interfaces/view'; | ||||||
| @ -251,7 +251,7 @@ export function injectChangeDetectorRef(): viewEngine_ChangeDetectorRef { | |||||||
|  * |  * | ||||||
|  * @experimental |  * @experimental | ||||||
|  */ |  */ | ||||||
| export function injectAttribute(attrName: string): string|undefined { | export function injectAttribute(attrNameToInject: string): string|undefined { | ||||||
|   ngDevMode && assertPreviousIsParent(); |   ngDevMode && assertPreviousIsParent(); | ||||||
|   const lElement = getPreviousOrParentNode() as LElementNode; |   const lElement = getPreviousOrParentNode() as LElementNode; | ||||||
|   ngDevMode && assertNodeType(lElement, TNodeType.Element); |   ngDevMode && assertNodeType(lElement, TNodeType.Element); | ||||||
| @ -260,8 +260,10 @@ export function injectAttribute(attrName: string): string|undefined { | |||||||
|   const attrs = tElement.attrs; |   const attrs = tElement.attrs; | ||||||
|   if (attrs) { |   if (attrs) { | ||||||
|     for (let i = 0; i < attrs.length; i = i + 2) { |     for (let i = 0; i < attrs.length; i = i + 2) { | ||||||
|       if (attrs[i] == attrName) { |       const attrName = attrs[i]; | ||||||
|         return attrs[i + 1]; |       if (attrName === AttributeMarker.SELECT_ONLY) break; | ||||||
|  |       if (attrName == attrNameToInject) { | ||||||
|  |         return attrs[i + 1] as string; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -73,6 +73,10 @@ export { | |||||||
|   tick, |   tick, | ||||||
| } from './instructions'; | } from './instructions'; | ||||||
| 
 | 
 | ||||||
|  | export { | ||||||
|  |     AttributeMarker | ||||||
|  | } from './interfaces/node'; | ||||||
|  | 
 | ||||||
| export { | export { | ||||||
|   pipe as Pp, |   pipe as Pp, | ||||||
|   pipeBind1 as pb1, |   pipeBind1 as pb1, | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interface | |||||||
| import {LQueries} from './interfaces/query'; | import {LQueries} from './interfaces/query'; | ||||||
| import {CurrentMatchesList, LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view'; | 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 {assertNodeType} from './node_assert'; | ||||||
| import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode} from './node_manipulation'; | import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode} from './node_manipulation'; | ||||||
| import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; | import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; | ||||||
| @ -366,19 +366,19 @@ export function createLNodeObject( | |||||||
|  */ |  */ | ||||||
| export function createLNode( | export function createLNode( | ||||||
|     index: number | null, type: TNodeType.Element, native: RElement | RText | null, |     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( | export function createLNode( | ||||||
|     index: number | null, type: TNodeType.View, native: null, name: null, attrs: null, |     index: number | null, type: TNodeType.View, native: null, name: null, attrs: null, | ||||||
|     lView: LView): LViewNode; |     lView: LView): LViewNode; | ||||||
| export function createLNode( | export function createLNode( | ||||||
|     index: number, type: TNodeType.Container, native: undefined, name: string | null, |     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( | 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; |     lProjection: LProjection): LProjectionNode; | ||||||
| export function createLNode( | export function createLNode( | ||||||
|     index: number | null, type: TNodeType, native: RText | RElement | null | undefined, |     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 { |         LProjection): LElementNode<extNode&LViewNode&LContainerNode&LProjectionNode { | ||||||
|   const parent = isParent ? previousOrParentNode : |   const parent = isParent ? previousOrParentNode : | ||||||
|                             previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode; |                             previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode; | ||||||
| @ -586,7 +586,8 @@ function getRenderFlags(view: LView): RenderFlags { | |||||||
|  * ['id', 'warning5', 'class', 'alert'] |  * ['id', 'warning5', 'class', 'alert'] | ||||||
|  */ |  */ | ||||||
| export function elementStart( | 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 && |   ngDevMode && | ||||||
|       assertEqual( |       assertEqual( | ||||||
|           currentView.bindingStartIndex, -1, 'elements should be created before any bindings'); |           currentView.bindingStartIndex, -1, 'elements should be created before any bindings'); | ||||||
| @ -600,23 +601,18 @@ export function elementStart( | |||||||
| 
 | 
 | ||||||
|   if (attrs) setUpAttributes(native, attrs); |   if (attrs) setUpAttributes(native, attrs); | ||||||
|   appendChild(getParentLNode(node), native, currentView); |   appendChild(getParentLNode(node), native, currentView); | ||||||
|   createDirectivesAndLocals(index, name, attrs, localRefs, false); |   createDirectivesAndLocals(localRefs); | ||||||
|   return native; |   return native; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Creates directive instances and populates local refs. |  * 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 localRefs Local refs of the current node | ||||||
|  * @param inlineViews Whether or not this node will create inline views |  | ||||||
|  */ |  */ | ||||||
| function createDirectivesAndLocals( | function createDirectivesAndLocals(localRefs?: string[] | null) { | ||||||
|     index: number, name: string | null, attrs: string[] | null | undefined, |  | ||||||
|     localRefs: string[] | null | undefined, inlineViews: boolean) { |  | ||||||
|   const node = previousOrParentNode; |   const node = previousOrParentNode; | ||||||
|  | 
 | ||||||
|   if (firstTemplatePass) { |   if (firstTemplatePass) { | ||||||
|     ngDevMode && ngDevMode.firstTemplatePass++; |     ngDevMode && ngDevMode.firstTemplatePass++; | ||||||
|     cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null); |     cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null); | ||||||
| @ -822,17 +818,18 @@ export function createTView( | |||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function setUpAttributes(native: RElement, attrs: string[]): void { | function setUpAttributes(native: RElement, attrs: TAttributes): void { | ||||||
|   ngDevMode && assertEqual(attrs.length % 2, 0, 'each attribute should have a key and a value'); |  | ||||||
| 
 |  | ||||||
|   const isProc = isProceduralRenderer(renderer); |   const isProc = isProceduralRenderer(renderer); | ||||||
|   for (let i = 0; i < attrs.length; i += 2) { |   for (let i = 0; i < attrs.length; i += 2) { | ||||||
|     const attrName = attrs[i]; |     const attrName = attrs[i]; | ||||||
|  |     if (attrName === AttributeMarker.SELECT_ONLY) break; | ||||||
|     if (attrName !== NG_PROJECT_AS_ATTR_NAME) { |     if (attrName !== NG_PROJECT_AS_ATTR_NAME) { | ||||||
|       const attrVal = attrs[i + 1]; |       const attrVal = attrs[i + 1]; | ||||||
|       ngDevMode && ngDevMode.rendererSetAttribute++; |       ngDevMode && ngDevMode.rendererSetAttribute++; | ||||||
|       isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) : |       isProc ? | ||||||
|                native.setAttribute(attrName, attrVal); |           (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 |  * @returns the TNode object | ||||||
|  */ |  */ | ||||||
| export function createTNode( | 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 { |     parent: TElementNode | TContainerNode | null, tViews: TView[] | null): TNode { | ||||||
|   ngDevMode && ngDevMode.tNode++; |   ngDevMode && ngDevMode.tNode++; | ||||||
|   return { |   return { | ||||||
| @ -1442,10 +1439,13 @@ function generateInitialInputs( | |||||||
|   for (let i = 0; i < attrs.length; i += 2) { |   for (let i = 0; i < attrs.length; i += 2) { | ||||||
|     const attrName = attrs[i]; |     const attrName = attrs[i]; | ||||||
|     const minifiedInputName = inputs[attrName]; |     const minifiedInputName = inputs[attrName]; | ||||||
|  |     const attrValue = attrs[i + 1]; | ||||||
|  | 
 | ||||||
|  |     if (attrName === AttributeMarker.SELECT_ONLY) break; | ||||||
|     if (minifiedInputName !== undefined) { |     if (minifiedInputName !== undefined) { | ||||||
|       const inputsToStore: InitialInputs = |       const inputsToStore: InitialInputs = | ||||||
|           initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); |           initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); | ||||||
|       inputsToStore.push(minifiedInputName, attrs[i + 1]); |       inputsToStore.push(minifiedInputName, attrValue as string); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   return initialInputData; |   return initialInputData; | ||||||
| @ -1484,7 +1484,7 @@ export function createLContainer( | |||||||
|  * @param localRefs A set of local reference bindings on the element. |  * @param localRefs A set of local reference bindings on the element. | ||||||
|  */ |  */ | ||||||
| export function container( | 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 { |     localRefs?: string[] | null): void { | ||||||
|   ngDevMode && assertEqual( |   ngDevMode && assertEqual( | ||||||
|                    currentView.bindingStartIndex, -1, |                    currentView.bindingStartIndex, -1, | ||||||
| @ -1501,7 +1501,7 @@ export function container( | |||||||
|   // Containers are added to the current view tree instead of their embedded views
 |   // Containers are added to the current view tree instead of their embedded views
 | ||||||
|   // because views can be removed and re-inserted.
 |   // because views can be removed and re-inserted.
 | ||||||
|   addToViewTree(currentView, node.data); |   addToViewTree(currentView, node.data); | ||||||
|   createDirectivesAndLocals(index, tagName || null, attrs, localRefs, template == null); |   createDirectivesAndLocals(localRefs); | ||||||
| 
 | 
 | ||||||
|   isParent = false; |   isParent = false; | ||||||
|   ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container); |   ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container); | ||||||
|  | |||||||
| @ -158,6 +158,29 @@ export interface LProjectionNode extends LNode { | |||||||
|   dynamicLContainerNode: null; |   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 |  * LNode binding data (flyweight) for a particular node that is shared between all templates | ||||||
|  * of a specific type. |  * of a specific type. | ||||||
| @ -198,18 +221,20 @@ export interface TNode { | |||||||
|   tagName: string|null; |   tagName: string|null; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Static attributes associated with an element. We need to store |    * Attributes associated with an element. We need to store attributes to support various use-cases | ||||||
|    * static attributes to support content projection with selectors. |    * (attribute injection, content projection with selectors, directives matching). | ||||||
|    * Attributes are stored statically because reading them from the DOM |    * Attributes are stored statically because reading them from the DOM would be way too slow for | ||||||
|    * would be way too slow for content projection and queries. |    * content projection and queries. | ||||||
|    * |    * | ||||||
|    * Since attrs will always be calculated first, they will never need |    * Since attrs will always be calculated first, they will never need to be marked undefined by | ||||||
|    * to be marked undefined by other instructions. |    * 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'] |    * 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 |    * 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 './ng_dev_mode'; | ||||||
| 
 | 
 | ||||||
| import {assertNotNull} from './assert'; | 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'; | import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection'; | ||||||
| 
 | 
 | ||||||
| const unusedValueToPlacateAjd = unused1 + unused2; | const unusedValueToPlacateAjd = unused1 + unused2; | ||||||
| @ -40,6 +40,7 @@ export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boo | |||||||
| 
 | 
 | ||||||
|   let mode: SelectorFlags = SelectorFlags.ELEMENT; |   let mode: SelectorFlags = SelectorFlags.ELEMENT; | ||||||
|   const nodeAttrs = tNode.attrs !; |   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
 |   // When processing ":not" selectors, we skip to the next ":not" if the
 | ||||||
|   // current one doesn't match
 |   // 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]; |       const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i]; | ||||||
|       if (selectorAttrValue !== '') { |       if (selectorAttrValue !== '') { | ||||||
|         const nodeAttrValue = nodeAttrs[attrIndexInNode + 1]; |         const nodeAttrValue = selectOnlyMarkerIdx > -1 && attrIndexInNode > selectOnlyMarkerIdx ? | ||||||
|  |             '' : | ||||||
|  |             nodeAttrs[attrIndexInNode + 1]; | ||||||
|         if (mode & SelectorFlags.CLASS && |         if (mode & SelectorFlags.CLASS && | ||||||
|                 !isCssClassMatching(nodeAttrValue, selectorAttrValue as string) || |                 !isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) || | ||||||
|             mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) { |             mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) { | ||||||
|           if (isPositive(mode)) return false; |           if (isPositive(mode)) return false; | ||||||
|           skipToNextSelector = true; |           skipToNextSelector = true; | ||||||
| @ -98,10 +101,15 @@ function isPositive(mode: SelectorFlags): boolean { | |||||||
|   return (mode & SelectorFlags.NOT) === 0; |   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; |   if (attrs === null) return -1; | ||||||
|   for (let i = 0; i < attrs.length; i += 2) { |   for (let i = 0; i < attrs.length; i += step) { | ||||||
|     if (attrs[i] === name) return i; |     const attrName = attrs[i]; | ||||||
|  |     if (attrName === name) return i; | ||||||
|  |     if (attrName === AttributeMarker.SELECT_ONLY) { | ||||||
|  |       step = 1; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   return -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
 |     // only check for ngProjectAs in attribute names, don't accidentally match attribute's value
 | ||||||
|     // (attribute names are stored at even indexes)
 |     // (attribute names are stored at even indexes)
 | ||||||
|     if ((ngProjectAsAttrIdx & 1) === 0) { |     if ((ngProjectAsAttrIdx & 1) === 0) { | ||||||
|       return nodeAttrs[ngProjectAsAttrIdx + 1]; |       return nodeAttrs[ngProjectAsAttrIdx + 1] as string; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   return null; |   return null; | ||||||
|  | |||||||
| @ -8,10 +8,11 @@ | |||||||
| 
 | 
 | ||||||
| import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection'; | import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection'; | ||||||
| 
 | 
 | ||||||
| import {detectChanges} from '../../src/render3/index'; | import {AttributeMarker, detectChanges} from '../../src/render3/index'; | ||||||
| import {container, containerRefreshEnd, containerRefreshStart, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, load, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions'; | import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions'; | ||||||
| import {RenderFlags} from '../../src/render3/interfaces/definition'; | import {RenderFlags} from '../../src/render3/interfaces/definition'; | ||||||
| import {createComponent, renderComponent, toHtml} from './render_util'; | 
 | ||||||
|  | import {ComponentFixture, createComponent, renderComponent, toHtml} from './render_util'; | ||||||
| 
 | 
 | ||||||
| describe('content projection', () => { | describe('content projection', () => { | ||||||
|   it('should project content', () => { |   it('should project content', () => { | ||||||
| @ -583,6 +584,43 @@ describe('content projection', () => { | |||||||
|               '<child><div id="first"><span title="toFirst">1</span></div><div id="second"><span title="toSecond">2</span></div></child>'); |               '<child><div id="first"><span title="toFirst">1</span></div><div id="second"><span title="toSecond">2</span></div></child>'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // https://stackblitz.com/edit/angular-psokum?file=src%2Fapp%2Fapp.module.ts
 | ||||||
|  |     it('should project nodes where attribute selector matches a binding', () => { | ||||||
|  |       /** | ||||||
|  |        *  <ng-content select="[title]"></ng-content> | ||||||
|  |        */ | ||||||
|  |       const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { | ||||||
|  |         if (rf & RenderFlags.Create) { | ||||||
|  |           projectionDef(0, [[['', 'title', '']]], ['[title]']); | ||||||
|  |           { projection(1, 0, 1); } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       /** | ||||||
|  |        * <child> | ||||||
|  |        *  <span [title]="'Some title'">Has title</span> | ||||||
|  |        * </child> | ||||||
|  |        */ | ||||||
|  |       const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { | ||||||
|  |         if (rf & RenderFlags.Create) { | ||||||
|  |           elementStart(0, 'child'); | ||||||
|  |           { | ||||||
|  |             elementStart(1, 'span', [AttributeMarker.SELECT_ONLY, 'title']); | ||||||
|  |             { text(2, 'Has title'); } | ||||||
|  |             elementEnd(); | ||||||
|  |           } | ||||||
|  |           elementEnd(); | ||||||
|  |         } | ||||||
|  |         if (rf & RenderFlags.Update) { | ||||||
|  |           elementProperty(1, 'title', bind('Some title')); | ||||||
|  |         } | ||||||
|  |       }, [Child]); | ||||||
|  | 
 | ||||||
|  |       const fixture = new ComponentFixture(Parent); | ||||||
|  |       expect(fixture.html).toEqual('<child><span title="Some title">Has title</span></child>'); | ||||||
|  | 
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     it('should project nodes using class selectors', () => { |     it('should project nodes using class selectors', () => { | ||||||
|       /** |       /** | ||||||
|        *  <div id="first"><ng-content select="span.toFirst"></ng-content></div> |        *  <div id="first"><ng-content select="span.toFirst"></ng-content></div> | ||||||
|  | |||||||
| @ -12,9 +12,9 @@ import {RenderFlags} from '@angular/core/src/render3/interfaces/definition'; | |||||||
| import {defineComponent} from '../../src/render3/definition'; | import {defineComponent} from '../../src/render3/definition'; | ||||||
| import {bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di'; | import {bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di'; | ||||||
| import {NgOnChangesFeature, PublicFeature, defineDirective, directiveInject, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; | import {NgOnChangesFeature, PublicFeature, defineDirective, directiveInject, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; | ||||||
| import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; | import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; | ||||||
| import {LInjector} from '../../src/render3/interfaces/injector'; | import {LInjector} from '../../src/render3/interfaces/injector'; | ||||||
| import {TNodeType} from '../../src/render3/interfaces/node'; | import {AttributeMarker, TNodeType} from '../../src/render3/interfaces/node'; | ||||||
| import {LViewFlags} from '../../src/render3/interfaces/view'; | import {LViewFlags} from '../../src/render3/interfaces/view'; | ||||||
| import {ViewRef} from '../../src/render3/view_ref'; | import {ViewRef} from '../../src/render3/view_ref'; | ||||||
| 
 | 
 | ||||||
| @ -1199,21 +1199,61 @@ describe('di', () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('should injectAttribute', () => { |   describe('@Attribute', () => { | ||||||
|     let exist: string|undefined = 'wrong'; |  | ||||||
|     let nonExist: string|undefined = 'wrong'; |  | ||||||
| 
 | 
 | ||||||
|     const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { |     it('should inject attribute', () => { | ||||||
|       if (rf & RenderFlags.Create) { |       let exist: string|undefined = 'wrong'; | ||||||
|         elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']); |       let nonExist: string|undefined = 'wrong'; | ||||||
|         exist = injectAttribute('exist'); | 
 | ||||||
|         nonExist = injectAttribute('nonExist'); |       const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { | ||||||
|       } |         if (rf & RenderFlags.Create) { | ||||||
|  |           elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']); | ||||||
|  |           exist = injectAttribute('exist'); | ||||||
|  |           nonExist = injectAttribute('nonExist'); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       const fixture = new ComponentFixture(MyApp); | ||||||
|  |       expect(exist).toEqual('existValue'); | ||||||
|  |       expect(nonExist).toEqual(undefined); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const app = renderComponent(MyApp); |     // https://stackblitz.com/edit/angular-8ytqkp?file=src%2Fapp%2Fapp.component.ts
 | ||||||
|     expect(exist).toEqual('existValue'); |     it('should not inject attributes representing bindings and outputs', () => { | ||||||
|     expect(nonExist).toEqual(undefined); |       let exist: string|undefined = 'wrong'; | ||||||
|  |       let nonExist: string|undefined = 'wrong'; | ||||||
|  | 
 | ||||||
|  |       const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { | ||||||
|  |         if (rf & RenderFlags.Create) { | ||||||
|  |           elementStart(0, 'div', ['exist', 'existValue', AttributeMarker.SELECT_ONLY, 'nonExist']); | ||||||
|  |           exist = injectAttribute('exist'); | ||||||
|  |           nonExist = injectAttribute('nonExist'); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       const fixture = new ComponentFixture(MyApp); | ||||||
|  |       expect(exist).toEqual('existValue'); | ||||||
|  |       expect(nonExist).toEqual(undefined); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should not accidentally inject attributes representing bindings and outputs', () => { | ||||||
|  |       let exist: string|undefined = 'wrong'; | ||||||
|  |       let nonExist: string|undefined = 'wrong'; | ||||||
|  | 
 | ||||||
|  |       const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { | ||||||
|  |         if (rf & RenderFlags.Create) { | ||||||
|  |           elementStart(0, 'div', [ | ||||||
|  |             'exist', 'existValue', AttributeMarker.SELECT_ONLY, 'binding1', 'nonExist', 'binding2' | ||||||
|  |           ]); | ||||||
|  |           exist = injectAttribute('exist'); | ||||||
|  |           nonExist = injectAttribute('nonExist'); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       const fixture = new ComponentFixture(MyApp); | ||||||
|  |       expect(exist).toEqual('existValue'); | ||||||
|  |       expect(nonExist).toEqual(undefined); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('inject', () => { |   describe('inject', () => { | ||||||
|  | |||||||
| @ -6,10 +6,12 @@ | |||||||
|  * found in the LICENSE file at https://angular.io/license
 |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import {defineDirective} from '../../src/render3/index'; | import {EventEmitter} from '@angular/core'; | ||||||
| import {bind, elementEnd, elementProperty, elementStart, loadDirective} from '../../src/render3/instructions'; | 
 | ||||||
| import {RenderFlags} from '../../src/render3/interfaces/definition'; | import {AttributeMarker, defineDirective} from '../../src/render3/index'; | ||||||
| import {renderToHtml} from './render_util'; | import {bind, elementEnd, elementProperty, elementStart, listener, loadDirective} from '../../src/render3/instructions'; | ||||||
|  | 
 | ||||||
|  | import {TemplateFixture} from './render_util'; | ||||||
| 
 | 
 | ||||||
| describe('directive', () => { | describe('directive', () => { | ||||||
| 
 | 
 | ||||||
| @ -31,17 +33,151 @@ describe('directive', () => { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       function Template(rf: RenderFlags, ctx: any) { |       function Template() { | ||||||
|         if (rf & RenderFlags.Create) { |         elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'dir']); | ||||||
|           elementStart(0, 'span', ['dir', '']); |         elementEnd(); | ||||||
|           elementEnd(); |       } | ||||||
|  | 
 | ||||||
|  |       const fixture = new TemplateFixture(Template, () => {}, [Directive]); | ||||||
|  |       expect(fixture.html).toEqual('<span class="foo"></span>'); | ||||||
|  | 
 | ||||||
|  |       directiveInstance !.klass = 'bar'; | ||||||
|  |       fixture.update(); | ||||||
|  |       expect(fixture.html).toEqual('<span class="bar"></span>'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('selectors', () => { | ||||||
|  | 
 | ||||||
|  |     it('should match directives with attribute selectors on bindings', () => { | ||||||
|  |       let directiveInstance: Directive; | ||||||
|  | 
 | ||||||
|  |       class Directive { | ||||||
|  |         static ngDirectiveDef = defineDirective({ | ||||||
|  |           type: Directive, | ||||||
|  |           selectors: [['', 'test', '']], | ||||||
|  |           factory: () => directiveInstance = new Directive, | ||||||
|  |           inputs: {test: 'test', other: 'other'} | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         testValue: boolean; | ||||||
|  |         other: boolean; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * A setter to assert that a binding is not invoked with stringified attribute value | ||||||
|  |          */ | ||||||
|  |         set test(value: any) { | ||||||
|  |           // if a binding is processed correctly we should only be invoked with a false Boolean
 | ||||||
|  |           // and never with the "false" string literal
 | ||||||
|  |           this.testValue = value; | ||||||
|  |           if (value !== false) { | ||||||
|  |             fail('Should only be called with a false Boolean value, got a non-falsy value'); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const defs = [Directive]; |       /** | ||||||
|       expect(renderToHtml(Template, {}, defs)).toEqual('<span class="foo" dir=""></span>'); |        * <span [test]="false" [other]="true"></span> | ||||||
|       directiveInstance !.klass = 'bar'; |        */ | ||||||
|       expect(renderToHtml(Template, {}, defs)).toEqual('<span class="bar" dir=""></span>'); |       function createTemplate() { | ||||||
|  |         // using 2 bindings to show example shape of attributes array
 | ||||||
|  |         elementStart(0, 'span', ['class', 'fade', AttributeMarker.SELECT_ONLY, 'test', 'other']); | ||||||
|  |         elementEnd(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       function updateTemplate() { elementProperty(0, 'test', bind(false)); } | ||||||
|  | 
 | ||||||
|  |       const fixture = new TemplateFixture(createTemplate, updateTemplate, [Directive]); | ||||||
|  | 
 | ||||||
|  |       // the "test" attribute should not be reflected in the DOM as it is here only for directive
 | ||||||
|  |       // matching purposes
 | ||||||
|  |       expect(fixture.html).toEqual('<span class="fade"></span>'); | ||||||
|  |       expect(directiveInstance !.testValue).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should not accidentally set inputs from attributes extracted from bindings / outputs', | ||||||
|  |        () => { | ||||||
|  |          let directiveInstance: Directive; | ||||||
|  | 
 | ||||||
|  |          class Directive { | ||||||
|  |            static ngDirectiveDef = defineDirective({ | ||||||
|  |              type: Directive, | ||||||
|  |              selectors: [['', 'test', '']], | ||||||
|  |              factory: () => directiveInstance = new Directive, | ||||||
|  |              inputs: {test: 'test', prop1: 'prop1', prop2: 'prop2'} | ||||||
|  |            }); | ||||||
|  | 
 | ||||||
|  |            prop1: boolean; | ||||||
|  |            prop2: boolean; | ||||||
|  |            testValue: boolean; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |            /** | ||||||
|  |             * A setter to assert that a binding is not invoked with stringified attribute value | ||||||
|  |             */ | ||||||
|  |            set test(value: any) { | ||||||
|  |              // if a binding is processed correctly we should only be invoked with a false Boolean
 | ||||||
|  |              // and never with the "false" string literal
 | ||||||
|  |              this.testValue = value; | ||||||
|  |              if (value !== false) { | ||||||
|  |                fail('Should only be called with a false Boolean value, got a non-falsy value'); | ||||||
|  |              } | ||||||
|  |            } | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          /** | ||||||
|  |           * <span [prop1]="true" [test]="false" [prop2]="true"></span> | ||||||
|  |           */ | ||||||
|  |          function createTemplate() { | ||||||
|  |            // putting name (test) in the "usual" value position
 | ||||||
|  |            elementStart( | ||||||
|  |                0, 'span', ['class', 'fade', AttributeMarker.SELECT_ONLY, 'prop1', 'test', 'prop2']); | ||||||
|  |            elementEnd(); | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          function updateTemplate() { | ||||||
|  |            elementProperty(0, 'prop1', bind(true)); | ||||||
|  |            elementProperty(0, 'test', bind(false)); | ||||||
|  |            elementProperty(0, 'prop2', bind(true)); | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          const fixture = new TemplateFixture(createTemplate, updateTemplate, [Directive]); | ||||||
|  | 
 | ||||||
|  |          // the "test" attribute should not be reflected in the DOM as it is here only for directive
 | ||||||
|  |          // matching purposes
 | ||||||
|  |          expect(fixture.html).toEqual('<span class="fade"></span>'); | ||||||
|  |          expect(directiveInstance !.testValue).toBe(false); | ||||||
|  |        }); | ||||||
|  | 
 | ||||||
|  |     it('should match directives with attribute selectors on outputs', () => { | ||||||
|  |       let directiveInstance: Directive; | ||||||
|  | 
 | ||||||
|  |       class Directive { | ||||||
|  |         static ngDirectiveDef = defineDirective({ | ||||||
|  |           type: Directive, | ||||||
|  |           selectors: [['', 'out', '']], | ||||||
|  |           factory: () => directiveInstance = new Directive, | ||||||
|  |           outputs: {out: 'out'} | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         out = new EventEmitter(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       /** | ||||||
|  |        * <span (out)="someVar = true"></span> | ||||||
|  |        */ | ||||||
|  |       function createTemplate() { | ||||||
|  |         elementStart(0, 'span', [AttributeMarker.SELECT_ONLY, 'out']); | ||||||
|  |         { listener('out', () => {}); } | ||||||
|  |         elementEnd(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const fixture = new TemplateFixture(createTemplate, () => {}, [Directive]); | ||||||
|  | 
 | ||||||
|  |       // "out" should not be part of reflected attributes
 | ||||||
|  |       expect(fixture.html).toEqual('<span></span>'); | ||||||
|  |       expect(directiveInstance !).not.toBeUndefined(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|   }); |   }); | ||||||
|  | |||||||
| @ -6,12 +6,12 @@ | |||||||
|  * found in the LICENSE file at https://angular.io/license
 |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import {TNode, TNodeType} from '../../src/render3/interfaces/node'; | import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/interfaces/node'; | ||||||
| 
 | 
 | ||||||
| import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags,} from '../../src/render3/interfaces/projection'; | import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags,} from '../../src/render3/interfaces/projection'; | ||||||
| import {getProjectAsAttrValue, isNodeMatchingSelectorList, isNodeMatchingSelector} from '../../src/render3/node_selector_matcher'; | import {getProjectAsAttrValue, isNodeMatchingSelectorList, isNodeMatchingSelector} from '../../src/render3/node_selector_matcher'; | ||||||
| 
 | 
 | ||||||
| function testLStaticData(tagName: string, attrs: string[] | null): TNode { | function testLStaticData(tagName: string, attrs: TAttributes | null): TNode { | ||||||
|   return { |   return { | ||||||
|     type: TNodeType.Element, |     type: TNodeType.Element, | ||||||
|     index: 0, |     index: 0, | ||||||
| @ -29,7 +29,7 @@ function testLStaticData(tagName: string, attrs: string[] | null): TNode { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| describe('css selector matching', () => { | describe('css selector matching', () => { | ||||||
|   function isMatching(tagName: string, attrs: string[] | null, selector: CssSelector): boolean { |   function isMatching(tagName: string, attrs: TAttributes | null, selector: CssSelector): boolean { | ||||||
|     return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector); |     return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -177,6 +177,28 @@ describe('css selector matching', () => { | |||||||
|           '', 'class', 'foo' |           '', 'class', 'foo' | ||||||
|         ])).toBeTruthy(`Selector '[class="foo"]' should match <span class="foo">`); |         ])).toBeTruthy(`Selector '[class="foo"]' should match <span class="foo">`); | ||||||
|       }); |       }); | ||||||
|  | 
 | ||||||
|  |       it('should take optional binding attribute names into account', () => { | ||||||
|  |         expect(isMatching('span', [AttributeMarker.SELECT_ONLY, 'directive'], [ | ||||||
|  |           '', 'directive', '' | ||||||
|  |         ])).toBeTruthy(`Selector '[directive]' should match <span [directive]="exp">`); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should not match optional binding attribute names if attribute selector has value', | ||||||
|  |          () => { | ||||||
|  |            expect(isMatching('span', [AttributeMarker.SELECT_ONLY, 'directive'], [ | ||||||
|  |              '', 'directive', 'value' | ||||||
|  |            ])).toBeFalsy(`Selector '[directive=value]' should not match <span [directive]="exp">`); | ||||||
|  |          }); | ||||||
|  | 
 | ||||||
|  |       it('should not match optional binding attribute names if attribute selector has value and next name equals to value', | ||||||
|  |          () => { | ||||||
|  |            expect(isMatching( | ||||||
|  |                       'span', [AttributeMarker.SELECT_ONLY, 'directive', 'value'], | ||||||
|  |                       ['', 'directive', 'value'])) | ||||||
|  |                .toBeFalsy( | ||||||
|  |                    `Selector '[directive=value]' should not match <span [directive]="exp" [value]="otherExp">`); | ||||||
|  |          }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('class matching', () => { |     describe('class matching', () => { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user