fix(ivy): properly bootstrap components with attribute selectors (#34450)

Fixes #34349

PR Close #34450
This commit is contained in:
Pawel Kozlowski
2019-12-18 14:35:22 +01:00
committed by atscott
parent b3af2202b9
commit 2c0b9ea310
7 changed files with 246 additions and 19 deletions

View File

@ -29,6 +29,7 @@ import {ComponentDef} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node';
import {RNode, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
import {stringifyCSSSelectorList} from './node_selector_matcher';
import {enterView, leaveView} from './state';
import {defaultScheduler} from './util/misc_utils';
import {getTNode} from './util/view_utils';
@ -113,9 +114,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
private componentDef: ComponentDef<any>, private ngModule?: viewEngine_NgModuleRef<any>) {
super();
this.componentType = componentDef.type;
// default to 'div' in case this component has an attribute selector
this.selector = componentDef.selectors[0][0] as string || 'div';
this.selector = stringifyCSSSelectorList(componentDef.selectors);
this.ngContentSelectors =
componentDef.ngContentSelectors ? componentDef.ngContentSelectors : [];
this.isBoundToModule = !!ngModule;
@ -135,7 +134,12 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
const hostRNode = rootSelectorOrNode ?
locateHostElement(rendererFactory, rootSelectorOrNode, this.componentDef.encapsulation) :
elementCreate(this.selector, rendererFactory.createRenderer(null, this.componentDef), null);
// Determine a tag name used for creating host elements when this component is created
// dynamically. Default to 'div' if this component did not specify any tag name in its
// selector.
elementCreate(
this.componentDef.selectors[0][0] as string || 'div',
rendererFactory.createRenderer(null, this.componentDef), null);
const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
LViewFlags.CheckAlways | LViewFlags.IsRoot;

View File

@ -297,3 +297,76 @@ export function isSelectorInSelectorList(selector: CssSelector, list: CssSelecto
}
return false;
}
function maybeWrapInNotSelector(isNegativeMode: boolean, chunk: string): string {
return isNegativeMode ? ':not(' + chunk.trim() + ')' : chunk;
}
function stringifyCSSSelector(selector: CssSelector): string {
let result = selector[0] as string;
let i = 1;
let mode = SelectorFlags.ATTRIBUTE;
let currentChunk = '';
let isNegativeMode = false;
while (i < selector.length) {
let valueOrMarker = selector[i];
if (typeof valueOrMarker === 'string') {
if (mode & SelectorFlags.ATTRIBUTE) {
const attrValue = selector[++i] as string;
currentChunk +=
'[' + valueOrMarker + (attrValue.length > 0 ? '="' + attrValue + '"' : '') + ']';
} else if (mode & SelectorFlags.CLASS) {
currentChunk += '.' + valueOrMarker;
} else if (mode & SelectorFlags.ELEMENT) {
currentChunk += ' ' + valueOrMarker;
}
} else {
//
// Append current chunk to the final result in case we come across SelectorFlag, which
// indicates that the previous section of a selector is over. We need to accumulate content
// between flags to make sure we wrap the chunk later in :not() selector if needed, e.g.
// ```
// ['', Flags.CLASS, '.classA', Flags.CLASS | Flags.NOT, '.classB', '.classC']
// ```
// should be transformed to `.classA :not(.classB .classC)`.
//
// Note: for negative selector part, we accumulate content between flags until we find the
// next negative flag. This is needed to support a case where `:not()` rule contains more than
// one chunk, e.g. the following selector:
// ```
// ['', Flags.ELEMENT | Flags.NOT, 'p', Flags.CLASS, 'foo', Flags.CLASS | Flags.NOT, 'bar']
// ```
// should be stringified to `:not(p.foo) :not(.bar)`
//
if (currentChunk !== '' && !isPositive(valueOrMarker)) {
result += maybeWrapInNotSelector(isNegativeMode, currentChunk);
currentChunk = '';
}
mode = valueOrMarker;
// According to CssSelector spec, once we come across `SelectorFlags.NOT` flag, the negative
// mode is maintained for remaining chunks of a selector.
isNegativeMode = isNegativeMode || !isPositive(mode);
}
i++;
}
if (currentChunk !== '') {
result += maybeWrapInNotSelector(isNegativeMode, currentChunk);
}
return result;
}
/**
* Generates string representation of CSS selector in parsed form.
*
* ComponentDef and DirectiveDef are generated with the selector in parsed form to avoid doing
* additional parsing at runtime (for example, for directive matching). However in some cases (for
* example, while bootstrapping a component), a string version of the selector is required to query
* for the host element on the page. This function takes the parsed form of a selector and returns
* its string representation.
*
* @param selectorList selector in parsed form
* @returns string representation of a given selector
*/
export function stringifyCSSSelectorList(selectorList: CssSelectorList): string {
return selectorList.map(stringifyCSSSelector).join(',');
}