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:

committed by
Alex Rickabaugh

parent
3f4e02b8c7
commit
f95b8ce07e
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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};
|
||||
}
|
Reference in New Issue
Block a user