diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index ed56726ec4ab66..a07a44f8676895 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -11,6 +11,7 @@ import './ng_dev_mode'; import {assertDefined, assertNotEqual} from './assert'; import {AttributeMarker, TAttributes, TNode, TNodeType, unusedValueExportToPlacateAjd as unused1} from './interfaces/node'; import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags, unusedValueExportToPlacateAjd as unused2} from './interfaces/projection'; +import {getInitialClassNameValue} from './styling/class_and_style_bindings'; const unusedValueToPlacateAjd = unused1 + unused2; @@ -92,7 +93,19 @@ export function isNodeMatchingSelector( skipToNextSelector = true; } } else { - const attrName = mode & SelectorFlags.CLASS ? 'class' : current; + const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i]; + + // special case for matching against classes when a tNode has been instantiated with + // class and style values as separate attribute values (e.g. ['title', CLASS, 'foo']) + if ((mode & SelectorFlags.CLASS) && tNode.stylingTemplate) { + if (!isCssClassMatching(readClassValueFromTNode(tNode), selectorAttrValue as string)) { + if (isPositive(mode)) return false; + skipToNextSelector = true; + } + continue; + } + + const attrName = (mode & SelectorFlags.CLASS) ? 'class' : current; const attrIndexInNode = findAttrIndexInNode(attrName, nodeAttrs); if (attrIndexInNode === -1) { @@ -101,7 +114,6 @@ export function isNodeMatchingSelector( continue; } - const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i]; if (selectorAttrValue !== '') { let nodeAttrValue: string; const maybeAttrName = nodeAttrs[attrIndexInNode]; @@ -113,8 +125,10 @@ export function isNodeMatchingSelector( 'We do not match directives on namespaced attributes'); nodeAttrValue = nodeAttrs[attrIndexInNode + 1] as string; } - if (mode & SelectorFlags.CLASS && - !isCssClassMatching(nodeAttrValue as string, selectorAttrValue as string) || + + const compareAgainstClassName = mode & SelectorFlags.CLASS ? nodeAttrValue : null; + if (compareAgainstClassName && + !isCssClassMatching(compareAgainstClassName, selectorAttrValue as string) || mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) { if (isPositive(mode)) return false; skipToNextSelector = true; @@ -130,6 +144,16 @@ function isPositive(mode: SelectorFlags): boolean { return (mode & SelectorFlags.NOT) === 0; } +function readClassValueFromTNode(tNode: TNode): string { + // comparing against CSS class values is complex because the compiler doesn't place them as + // regular attributes when an element is created. Instead, the classes (and styles for + // that matter) are placed in a special styling context that is used for resolving all + // class/style values across static attributes, [style]/[class] and [style.prop]/[class.name] + // bindings. Therefore if and when the styling context exists then the class values are to be + // extracted by the context helper code below... + return tNode.stylingTemplate ? getInitialClassNameValue(tNode.stylingTemplate) : ''; +} + /** * Examines an attributes definition array from a node to find the index of the * attribute with the specified name. diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index 5974dd4391b07d..822dcca3ac910d 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -10,6 +10,7 @@ import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/ 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 {initializeStaticContext} from '../../src/render3/styling/class_and_style_bindings'; import {createTNode} from '@angular/core/src/render3/instructions'; import {getLView} from '@angular/core/src/render3/state'; @@ -18,9 +19,12 @@ function testLStaticData(tagName: string, attrs: TAttributes | null): TNode { } describe('css selector matching', () => { - function isMatching(tagName: string, attrs: TAttributes | null, selector: CssSelector): boolean { - return isNodeMatchingSelector( - createTNode(getLView(), TNodeType.Element, 0, tagName, attrs, null), selector, false); + function isMatching( + tagName: string, attrsOrTNode: TAttributes | TNode | null, selector: CssSelector): boolean { + const tNode = (!attrsOrTNode || Array.isArray(attrsOrTNode)) ? + createTNode(getLView(), TNodeType.Element, 0, tagName, attrsOrTNode as TAttributes, null) : + (attrsOrTNode as TNode); + return isNodeMatchingSelector(tNode, selector, false); } describe('isNodeMatchingSimpleSelector', () => { @@ -298,6 +302,26 @@ describe('css selector matching', () => { //
expect(isMatching('div', ['class', 'foo'], selector)).toBeFalsy(); }); + + it('should match against a class value before and after the styling context is created', + () => { + // selector: 'div.abc' + const selector = ['div', SelectorFlags.CLASS, 'abc']; + const tNode = createTNode(getLView(), TNodeType.Element, 0, 'div', [], null); + + //
(without attrs or styling context) + expect(isMatching('div', tNode, selector)).toBeFalsy(); + + //
(with attrs but without styling context) + tNode.attrs = ['class', 'abc']; + tNode.stylingTemplate = null; + expect(isMatching('div', tNode, selector)).toBeTruthy(); + + //
(with styling context but without attrs) + tNode.stylingTemplate = initializeStaticContext([AttributeMarker.Classes, 'abc']); + tNode.attrs = null; + expect(isMatching('div', tNode, selector)).toBeTruthy(); + }); }); });