fix(ivy): add attributes and classes to host elements based on selector (#34481)

In View Engine, host element of dynamically created component received attributes and classes extracted from component's selector. For example, if component selector is `[attr] .class`, the `attr` attribute and `.class` class will be add to host element. This commit adds similar logic to Ivy, to make sure this behavior is aligned with View Engine.

PR Close #34481
This commit is contained in:
Andrew Kushnir
2019-12-18 16:52:32 -08:00
committed by Alex Rickabaugh
parent 3f4e02b8c7
commit f95b8ce07e
5 changed files with 206 additions and 16 deletions

View File

@ -20,7 +20,7 @@ import {CLEAN_PROMISE, addHostBindingsToExpandoInstructions, addToViewTree, crea
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view';
import {writeDirectClass, writeDirectStyle} from './node_manipulation';
import {enterView, getPreviousOrParentTNode, leaveView, setSelectedIndex} from './state';
@ -139,7 +139,7 @@ export function renderComponent<T>(
try {
if (rendererFactory.begin) rendererFactory.begin();
const componentView = createRootComponentView(
hostRNode, componentDef, rootView, rendererFactory, renderer, null, sanitizer);
hostRNode, componentDef, rootView, rendererFactory, renderer, sanitizer);
component = createRootComponent(
componentView, componentDef, rootView, rootContext, opts.hostFeatures || null);
@ -169,8 +169,8 @@ export function renderComponent<T>(
*/
export function createRootComponentView(
rNode: RElement | null, def: ComponentDef<any>, rootView: LView,
rendererFactory: RendererFactory3, hostRenderer: Renderer3, addVersion: string | null,
sanitizer: Sanitizer | null): LView {
rendererFactory: RendererFactory3, hostRenderer: Renderer3,
sanitizer?: Sanitizer | null): LView {
const tView = rootView[TVIEW];
ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET);
rootView[0 + HEADER_OFFSET] = rNode;
@ -188,14 +188,8 @@ export function createRootComponentView(
}
}
}
const viewRenderer = rendererFactory.createRenderer(rNode, def);
if (rNode !== null && addVersion) {
ngDevMode && ngDevMode.rendererSetAttribute++;
isProceduralRenderer(hostRenderer) ?
hostRenderer.setAttribute(rNode, 'ng-version', addVersion) :
rNode.setAttribute('ng-version', addVersion);
}
const viewRenderer = rendererFactory.createRenderer(rNode, def);
const componentView = createLView(
rootView, getOrCreateTComponentView(def), null,
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode,

View File

@ -30,8 +30,10 @@ import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/
import {RNode, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {stringifyCSSSelectorList} from './node_selector_matcher';
import {writeDirectClass} from './node_manipulation';
import {extractAttrsAndClassesFromSelector, stringifyCSSSelectorList} from './node_selector_matcher';
import {enterView, leaveView} from './state';
import {setUpAttributes} from './util/attrs_utils';
import {defaultScheduler} from './util/misc_utils';
import {getTNode} from './util/view_utils';
import {createElementRef} from './view_engine_compatibility';
@ -165,7 +167,6 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
const rootLView = createLView(
null, rootTView, rootContext, rootFlags, null, null, rendererFactory, hostRenderer,
sanitizer, rootViewInjector);
const addVersion = rootSelectorOrNode && hostRNode ? VERSION.full : null;
// rootView is the parent when bootstrapping
// TODO(misko): it looks like we are entering view here but we don't really need to as
@ -179,7 +180,24 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
try {
const componentView = createRootComponentView(
hostRNode, this.componentDef, rootLView, rendererFactory, hostRenderer, addVersion, null);
hostRNode, this.componentDef, rootLView, rendererFactory, hostRenderer);
if (hostRNode) {
if (rootSelectorOrNode) {
setUpAttributes(hostRenderer, hostRNode, ['ng-version', VERSION.full]);
} else {
// If host element is created as a part of this function call (i.e. `rootSelectorOrNode`
// is not defined), also apply attributes and classes extracted from component selector.
// Extract attributes and classes from the first selector only to match VE behavior.
const {attrs, classes} =
extractAttrsAndClassesFromSelector(this.componentDef.selectors[0]);
if (attrs) {
setUpAttributes(hostRenderer, hostRNode, attrs);
}
if (classes && classes.length > 0) {
writeDirectClass(hostRenderer, hostRNode, classes.join(' '));
}
}
}
tElementNode = getTNode(rootLView[TVIEW], 0) as TElementNode;

View File

@ -388,4 +388,42 @@ function stringifyCSSSelector(selector: CssSelector): string {
*/
export function stringifyCSSSelectorList(selectorList: CssSelectorList): string {
return selectorList.map(stringifyCSSSelector).join(',');
}
/**
* Extracts attributes and classes information from a given CSS selector.
*
* This function is used while creating a component dynamically. In this case, the host element
* (that is created dynamically) should contain attributes and classes specified in component's CSS
* selector.
*
* @param selector CSS selector in parsed form (in a form of array)
* @returns object with `attrs` and `classes` fields that contain extracted information
*/
export function extractAttrsAndClassesFromSelector(selector: CssSelector):
{attrs: string[], classes: string[]} {
const attrs: string[] = [];
const classes: string[] = [];
let i = 1;
let mode = SelectorFlags.ATTRIBUTE;
while (i < selector.length) {
let valueOrMarker = selector[i];
if (typeof valueOrMarker === 'string') {
if (mode === SelectorFlags.ATTRIBUTE) {
if (valueOrMarker !== '') {
attrs.push(valueOrMarker, selector[++i] as string);
}
} else if (mode === SelectorFlags.CLASS) {
classes.push(valueOrMarker);
}
} else {
// According to CssSelector spec, once we come across `SelectorFlags.NOT` flag, the negative
// mode is maintained for remaining chunks of a selector. Since attributes and classes are
// extracted only for "positive" part of the selector, we can stop here.
if (!isPositive(mode)) break;
mode = valueOrMarker;
}
i++;
}
return {attrs, classes};
}