Skip to content

Commit

Permalink
refactor(ivy): flatten css selectors (#23074)
Browse files Browse the repository at this point in the history
PR Close #23074
  • Loading branch information
kara authored and alxhub committed Mar 30, 2018
1 parent 6cb1adf commit 6e5fb99
Show file tree
Hide file tree
Showing 44 changed files with 631 additions and 361 deletions.
2 changes: 1 addition & 1 deletion modules/benchmarks/src/largetable/render3/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class LargeTableComponent {
/** @nocollapse */
static ngComponentDef: ComponentDef<LargeTableComponent> = defineComponent({
type: LargeTableComponent,
selector: [[['largetable'], null]],
selectors: [['largetable']],
template: function(ctx: LargeTableComponent, cm: boolean) {
if (cm) {
E(0, 'table');
Expand Down
4 changes: 2 additions & 2 deletions modules/benchmarks/src/tree/render3/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class TreeComponent {
/** @nocollapse */
static ngComponentDef: ComponentDef<TreeComponent> = defineComponent({
type: TreeComponent,
selector: [[['tree'], null]],
selectors: [['tree']],
template: function(ctx: TreeComponent, cm: boolean) {
if (cm) {
E(0, 'span');
Expand Down Expand Up @@ -91,7 +91,7 @@ export class TreeFunction {
/** @nocollapse */
static ngComponentDef: ComponentDef<TreeFunction> = defineComponent({
type: TreeFunction,
selector: [[['tree'], null]],
selectors: [['tree']],
template: function(ctx: TreeFunction, cm: boolean) {
// bit of a hack
TreeTpl(ctx.data, cm);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export {
InjectFlags as ɵInjectFlags,
PublicFeature as ɵPublicFeature,
NgOnChangesFeature as ɵNgOnChangesFeature,
CssSelector as ɵCssSelector,
CssSelectorList as ɵCssSelectorList,
NC as ɵNC,
C as ɵC,
E as ɵE,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ export function renderComponent<T>(
const componentDef = (componentType as ComponentType<T>).ngComponentDef as ComponentDef<T>;
if (componentDef.type != componentType) componentDef.type = componentType;
let component: T;
// TODO: Replace when flattening CssSelector type
const componentTag = componentDef.selector ![0] ![0] ![0];
// The first index of the first selector is the tag name.
const componentTag = componentDef.selectors ![0] ![0] as string;
const hostNode = locateHostElement(rendererFactory, opts.host || componentTag);
const rootContext: RootContext = {
// Incomplete initialization due to circular reference.
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/render3/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {resolveRendererType2} from '../view/util';

import {diPublic} from './di';
import {ComponentDef, ComponentDefFeature, ComponentTemplate, DirectiveDef, DirectiveDefFeature, DirectiveDefListOrFactory, PipeDef, PipeDefListOrFactory} from './interfaces/definition';
import {CssSelector} from './interfaces/projection';
import {CssSelectorList, SelectorFlags} from './interfaces/projection';



Expand All @@ -42,8 +42,8 @@ export function defineComponent<T>(componentDefinition: {
*/
type: Type<T>;

/** The selector that will be used to match nodes to this component. */
selector: CssSelector;
/** The selectors that will be used to match nodes to this component. */
selectors: CssSelectorList;

/**
* Factory method used to create an instance of directive.
Expand Down Expand Up @@ -185,7 +185,7 @@ export function defineComponent<T>(componentDefinition: {
onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
directiveDefs: componentDefinition.directiveDefs || null,
pipeDefs: componentDefinition.pipeDefs || null,
selector: componentDefinition.selector
selectors: componentDefinition.selectors
};
const feature = componentDefinition.features;
feature && feature.forEach((fn) => fn(def));
Expand Down Expand Up @@ -317,8 +317,8 @@ export const defineDirective = defineComponent as any as<T>(directiveDefinition:
*/
type: Type<T>;

/** The selector that will be used to match nodes to this directive. */
selector: CssSelector;
/** The selectors that will be used to match nodes to this directive. */
selectors: CssSelectorList;

/**
* Factory method used to create an instance of directive.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {InjectFlags} from './di';
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './interfaces/definition';

export {InjectFlags, QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di';
export {CssSelector} from './interfaces/projection';
export {CssSelectorList} from './interfaces/projection';



Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/render3/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import './ng_dev_mode';
import {assertEqual, assertLessThan, assertNotEqual, assertNotNull, assertNull, assertSame} from './assert';
import {LContainer, TContainer} from './interfaces/container';
import {LInjector} from './interfaces/injector';
import {CssSelector, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query';
import {LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view';

import {LContainerNode, LElementNode, LNode, LNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './interfaces/node';
import {assertNodeType} from './node_assert';
import {appendChild, insertChild, insertView, appendProjectedNode, removeView, canInsertNativeNode} from './node_manipulation';
import {isNodeMatchingSelector, matchingSelectorIndex} from './node_selector_matcher';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefList, DirectiveDefListOrFactory, DirectiveType, PipeDef, PipeDefListOrFactory} from './interfaces/definition';
import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, ObjectOrientedRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
import {isDifferent, stringify} from './util';
Expand Down Expand Up @@ -532,7 +532,7 @@ function cacheMatchingDirectivesForNode(tNode: TNode): void {

for (let i = 0; i < registry.length; i++) {
const def = registry[i];
if (isNodeMatchingSelector(tNode, def.selector !)) {
if (isNodeMatchingSelectorList(tNode, def.selectors !)) {
if ((def as ComponentDef<any>).template) {
if (componentFlag) throwMultipleComponentError(tNode);
componentFlag |= TNodeFlags.Component;
Expand Down Expand Up @@ -1567,7 +1567,7 @@ function viewAttached(view: LView): boolean {
* @param rawSelectors A collection of CSS selectors in the raw, un-parsed form
*/
export function projectionDef(
index: number, selectors?: CssSelector[], textSelectors?: string[]): void {
index: number, selectors?: CssSelectorList[], textSelectors?: string[]): void {
const noOfNodeBuckets = selectors ? selectors.length + 1 : 1;
const distributedNodes = new Array<LNode[]>(noOfNodeBuckets);
for (let i = 0; i < noOfNodeBuckets; i++) {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/render3/interfaces/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {Provider} from '../../core';
import {RendererType2} from '../../render/api';
import {Type} from '../../type';
import {resolveRendererType2} from '../../view/util';
import {CssSelector} from './projection';
import {CssSelectorList} from './projection';


/**
Expand Down Expand Up @@ -61,8 +61,8 @@ export interface DirectiveDef<T> {
/** Function that makes a directive public to the DI system. */
diPublic: ((def: DirectiveDef<any>) => void)|null;

/** The selector that will be used to match nodes to this directive. */
selector: CssSelector;
/** The selectors that will be used to match nodes to this directive. */
selectors: CssSelectorList;

/**
* A dictionary mapping the inputs' minified property names to their public API names, which
Expand Down
72 changes: 50 additions & 22 deletions packages/core/src/render3/interfaces/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,61 @@ export interface LProjection {
}

/**
* Parsed selector in the following format:
* [tagName, attr1Name, attr1Val, ..., attrnName, attrnValue, 'class', className1, className2, ...,
* classNameN]
*
* * For example, given the following selector:
* `div.foo.bar[attr1=val1][attr2]` a parsed format would be:
* `['div', 'attr1', 'val1', 'attr2', '', 'class', 'foo', 'bar']`.
*
* Things to notice:
* - tag name is always at the position 0
* - the `class` attribute is always the last attribute in a pre-parsed array
* - class names in a selector are at the end of an array (after the attribute with the name
* 'class').
* Expresses a single CSS Selector.
*
* Beginning of array
* - First index: element name
* - Subsequent odd indices: attr keys
* - Subsequent even indices: attr values
*
* After SelectorFlags.CLASS flag
* - Class name values
*
* SelectorFlags.NOT flag
* - Changes the mode to NOT
* - Can be combined with other flags to set the element / attr / class mode
*
* e.g. SelectorFlags.NOT | SelectorFlags.ELEMENT
*
* Example:
* Original: `div.foo.bar[attr1=val1][attr2]`
* Parsed: ['div', 'attr1', 'val1', 'attr2', '', SelectorFlags.CLASS, 'foo', 'bar']
*
* Original: 'div[attr1]:not(.foo[attr2])
* Parsed: [
* 'div', 'attr1', '',
* SelectorFlags.NOT | SelectorFlags.ATTRIBUTE 'attr2', '', SelectorFlags.CLASS, 'foo'
* ]
*
* See more examples in node_selector_matcher_spec.ts
*/
export type SimpleCssSelector = string[];
export type CssSelector = (string | SelectorFlags)[];

/**
* A complex selector expressed as an Array where:
* - element at index 0 is a selector (SimpleCSSSelector) to match
* - elements at index 1..n is a selector (SimpleCSSSelector) that should NOT match
* A list of CssSelectors.
*
* A directive or component can have multiple selectors. This type is used for
* directive defs so any of the selectors in the list will match that directive.
*
* Original: 'form, [ngForm]'
* Parsed: [['form'], ['', 'ngForm', '']]
*/
export type CssSelectorWithNegations = [SimpleCssSelector | null, SimpleCssSelector[] | null];
export type CssSelectorList = CssSelector[];

/**
* A collection of complex selectors (CSSSelectorWithNegations) in a parsed form
*/
export type CssSelector = CssSelectorWithNegations[];
/** Flags used to build up CssSelectors */
export const enum SelectorFlags {
/** Indicates this is the beginning of a new negative selector */
NOT = 0b0001,

/** Mode for matching attributes */
ATTRIBUTE = 0b0010,

/** Mode for matching tag names */
ELEMENT = 0b0100,

/** Mode for matching class names */
CLASS = 0b1000,
}

export const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs';

Expand Down
117 changes: 59 additions & 58 deletions packages/core/src/render3/node_selector_matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import './ng_dev_mode';

import {assertNotNull} from './assert';
import {TNode, unusedValueExportToPlacateAjd as unused1} from './interfaces/node';
import {CssSelector, CssSelectorWithNegations, NG_PROJECT_AS_ATTR_NAME, SimpleCssSelector, 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;

Expand All @@ -35,79 +35,80 @@ function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string):
* @param selector
* @returns true if node matches the selector.
*/
export function isNodeMatchingSimpleSelector(tNode: TNode, selector: SimpleCssSelector): boolean {
const noOfSelectorParts = selector.length;
ngDevMode && assertNotNull(selector[0], 'the selector should have a tag name');
const tagNameInSelector = selector[0];
export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boolean {
ngDevMode && assertNotNull(selector[0], 'Selector should have a tag name');

// check tag tame
if (tagNameInSelector !== '' && tagNameInSelector !== tNode.tagName) {
return false;
}
let mode: SelectorFlags = SelectorFlags.ELEMENT;
const nodeAttrs = tNode.attrs !;

// short-circuit case where we are only matching on element's tag name
if (noOfSelectorParts === 1) {
return true;
}
// When processing ":not" selectors, we skip to the next ":not" if the
// current one doesn't match
let skipToNextSelector = false;

// short-circuit case where an element has no attrs but a selector tries to match some
if (noOfSelectorParts > 1 && !tNode.attrs) {
return false;
}
for (let i = 0; i < selector.length; i++) {
const current = selector[i];
if (typeof current === 'number') {
// If we finish processing a :not selector and it hasn't failed, return false
if (!skipToNextSelector && !isPositive(mode) && !isPositive(current as number)) {
return false;
}
// If we are skipping to the next :not() and this mode flag is positive,
// it's a part of the current :not() selector, and we should keep skipping
if (skipToNextSelector && isPositive(current)) continue;
skipToNextSelector = false;
mode = (current as number) | (mode & SelectorFlags.NOT);
continue;
}

const attrsInNode = tNode.attrs !;
if (skipToNextSelector) continue;

for (let i = 1; i < noOfSelectorParts; i += 2) {
const attrNameInSelector = selector[i];
const attrIdxInNode = attrsInNode.indexOf(attrNameInSelector);
if (attrIdxInNode % 2 !== 0) { // attribute names are stored at even indexes
return false;
if (mode & SelectorFlags.ELEMENT) {
mode = SelectorFlags.ATTRIBUTE | mode & SelectorFlags.NOT;
if (current !== '' && current !== tNode.tagName) {
if (isPositive(mode)) return false;
skipToNextSelector = true;
}
} else {
const attrValInSelector = selector[i + 1];
if (attrValInSelector !== '') {
// selector should also match on an attribute value
const attrValInNode = attrsInNode[attrIdxInNode + 1];
if (attrNameInSelector === 'class') {
// iterate over all the remaining items in the selector selector array = class names
for (i++; i < noOfSelectorParts; i++) {
if (!isCssClassMatching(attrValInNode, selector[i])) {
return false;
}
}
} else if (attrValInSelector !== attrValInNode) {
return false;
const attrName = mode & SelectorFlags.CLASS ? 'class' : current;
const attrIndexInNode = findAttrIndexInNode(attrName, nodeAttrs);

if (attrIndexInNode === -1) {
if (isPositive(mode)) return false;
skipToNextSelector = true;
continue;
}

const selectorAttrValue = mode & SelectorFlags.CLASS ? current : selector[++i];
if (selectorAttrValue !== '') {
const nodeAttrValue = nodeAttrs[attrIndexInNode + 1];
if (mode & SelectorFlags.CLASS &&
!isCssClassMatching(nodeAttrValue, selectorAttrValue as string) ||
mode & SelectorFlags.ATTRIBUTE && selectorAttrValue !== nodeAttrValue) {
if (isPositive(mode)) return false;
skipToNextSelector = true;
}
}
}
}

return true;
return isPositive(mode) || skipToNextSelector;
}

export function isNodeMatchingSelectorWithNegations(
tNode: TNode, selector: CssSelectorWithNegations): boolean {
const positiveSelector = selector[0];
if (positiveSelector != null && !isNodeMatchingSimpleSelector(tNode, positiveSelector)) {
return false;
}
function isPositive(mode: SelectorFlags): boolean {
return (mode & SelectorFlags.NOT) === 0;
}

// do we have any negation parts in this selector?
const negativeSelectors = selector[1];
if (negativeSelectors) {
for (let i = 0; i < negativeSelectors.length; i++) {
// if one of negative selectors matched than the whole selector doesn't match
if (isNodeMatchingSimpleSelector(tNode, negativeSelectors[i])) {
return false;
}
}
function findAttrIndexInNode(name: string, attrs: string[] | null): number {
if (attrs === null) return -1;
for (let i = 0; i < attrs.length; i += 2) {
if (attrs[i] === name) return i;
}

return true;
return -1;
}

export function isNodeMatchingSelector(tNode: TNode, selector: CssSelector): boolean {
export function isNodeMatchingSelectorList(tNode: TNode, selector: CssSelectorList): boolean {
for (let i = 0; i < selector.length; i++) {
if (isNodeMatchingSelectorWithNegations(tNode, selector[i])) {
if (isNodeMatchingSelector(tNode, selector[i])) {
return true;
}
}
Expand Down Expand Up @@ -136,13 +137,13 @@ export function getProjectAsAttrValue(tNode: TNode): string|null {
* to the raw (un-parsed) CSS selector instead of using standard selector matching logic.
*/
export function matchingSelectorIndex(
tNode: TNode, selectors: CssSelector[], textSelectors: string[]): number {
tNode: TNode, selectors: CssSelectorList[], textSelectors: string[]): number {
const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
for (let i = 0; i < selectors.length; i++) {
// if a node has the ngProjectAs attribute match it against unparsed selector
// match a node against a parsed selector only if ngProjectAs attribute is not present
if (ngProjectAsAttrVal === textSelectors[i] ||
ngProjectAsAttrVal === null && isNodeMatchingSelector(tNode, selectors[i])) {
ngProjectAsAttrVal === null && isNodeMatchingSelectorList(tNode, selectors[i])) {
return i + 1; // first matching selector "captures" a given node
}
}
Expand Down

0 comments on commit 6e5fb99

Please sign in to comment.