Skip to content

Commit

Permalink
fix(ivy): ensure component/directive class selectors are properly u…
Browse files Browse the repository at this point in the history
…nderstood
  • Loading branch information
matsko committed Dec 27, 2018
1 parent c4f7727 commit 365c02e
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 7 deletions.
32 changes: 28 additions & 4 deletions packages/core/src/render3/node_selector_matcher.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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];
Expand All @@ -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;
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Expand Up @@ -1058,6 +1058,9 @@
{
"name": "queueViewHooks"
},
{
"name": "readClassValueFromTNode"
},
{
"name": "readElementValue"
},
Expand Down
30 changes: 27 additions & 3 deletions packages/core/test/render3/node_selector_matcher_spec.ts
Expand Up @@ -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';

Expand All @@ -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', () => {
Expand Down Expand Up @@ -298,6 +302,26 @@ describe('css selector matching', () => {
// <div class="foo">
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);

// <div> (without attrs or styling context)
expect(isMatching('div', tNode, selector)).toBeFalsy();

// <div class="abc"> (with attrs but without styling context)
tNode.attrs = ['class', 'abc'];
tNode.stylingTemplate = null;
expect(isMatching('div', tNode, selector)).toBeTruthy();

// <div class="abc"> (with styling context but without attrs)
tNode.stylingTemplate = initializeStaticContext([AttributeMarker.Classes, 'abc']);
tNode.attrs = null;
expect(isMatching('div', tNode, selector)).toBeTruthy();
});
});
});

Expand Down

0 comments on commit 365c02e

Please sign in to comment.