Skip to content

Commit

Permalink
refactor(core): support applying directives to the root component (#4…
Browse files Browse the repository at this point in the history
…7530)

Currently the code that creates a root component assumes that it's always going to deal with a single component definition which won't work with host directives. These changes rework the code so that it's able to apply multiple directives, allowing us to eventually add support for host directives.

I also tried to make the root component creation easier to follow by breaking it up into smaller functions.

PR Close #47530
  • Loading branch information
crisbeto authored and alxhub committed Sep 27, 2022
1 parent 551c641 commit 002ee32
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 259 deletions.
220 changes: 127 additions & 93 deletions packages/core/src/render3/component_ref.ts
Expand Up @@ -18,32 +18,33 @@ import {ComponentFactory as AbstractComponentFactory, ComponentRef as AbstractCo
import {ComponentFactoryResolver as AbstractComponentFactoryResolver} from '../linker/component_factory_resolver';
import {createElementRef, ElementRef} from '../linker/element_ref';
import {NgModuleRef} from '../linker/ng_module_factory';
import {RendererFactory2} from '../render/api';
import {Renderer2, RendererFactory2} from '../render/api';
import {Sanitizer} from '../sanitization/sanitizer';
import {assertDefined, assertIndexInRange} from '../util/assert';
import {assertDefined, assertGreaterThan, assertIndexInRange} from '../util/assert';
import {VERSION} from '../version';
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider_flags';

import {assertComponentType} from './assert';
import {attachPatchData} from './context_discovery';
import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode, NodeInjector} from './di';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode, NodeInjector} from './di';
import {throwProviderNotFoundError} from './errors_di';
import {registerPostOrderHooks} from './hooks';
import {reportUnknownPropertyError} from './instructions/element_validation';
import {addToViewTree, createLView, createTView, getOrCreateComponentTView, getOrCreateTNode, initTNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, markAsComponentHost, markDirtyIfOnPush, registerHostBindingOpCodes, renderView, setInputsForProperty} from './instructions/shared';
import {ComponentDef, RenderFlags} from './interfaces/definition';
import {addToViewTree, createLView, createTView, executeContentQueries, getOrCreateComponentTView, getOrCreateTNode, initializeDirectives, invokeDirectivesHostBindings, locateHostElement, markAsComponentHost, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {PropertyAliasValue, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
import {Renderer, RendererFactory} from './interfaces/renderer';
import {RElement, RNode} from './interfaces/renderer_dom';
import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {createElementNode, writeDirectClass, writeDirectStyle} from './node_manipulation';
import {createElementNode, setupStaticAttributes, writeDirectClass} from './node_manipulation';
import {extractAttrsAndClassesFromSelector, stringifyCSSSelectorList} from './node_selector_matcher';
import {enterView, getCurrentTNode, getLView, leaveView, setSelectedIndex} from './state';
import {enterView, getCurrentTNode, getLView, leaveView} from './state';
import {computeStaticStyling} from './styling/static_styling';
import {setUpAttributes} from './util/attrs_utils';
import {mergeHostAttrs, setUpAttributes} from './util/attrs_utils';
import {stringifyForError} from './util/stringify_utils';
import {getTNode} from './util/view_utils';
import {getNativeByTNode, getTNode} from './util/view_utils';
import {RootViewRef, ViewRef} from './view_ref';

export class ComponentFactoryResolver extends AbstractComponentFactoryResolver {
Expand Down Expand Up @@ -192,46 +193,30 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
let tElementNode: TElementNode;

try {
const rootDirectives = [this.componentDef];
const hostTNode = createRootComponentTNode(rootLView, hostRNode);
const componentView = createRootComponentView(
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(' '));
}
}
}
hostTNode, hostRNode, this.componentDef, rootDirectives, rootLView, rendererFactory,
hostRenderer);

tElementNode = getTNode(rootTView, HEADER_OFFSET) as TElementNode;

// TODO(crisbeto): in practice `hostRNode` should always be defined, but there are some tests
// where the renderer is mocked out and `undefined` is returned. We should update the tests so
// that this check can be removed.
if (hostRNode) {
setRootNodeAttributes(hostRenderer, this.componentDef, hostRNode, rootSelectorOrNode);
}

if (projectableNodes !== undefined) {
const projection: (TNode|RNode[]|null)[] = tElementNode.projection = [];
for (let i = 0; i < this.ngContentSelectors.length; i++) {
const nodesforSlot = projectableNodes[i];
// Projectable nodes can be passed as array of arrays or an array of iterables (ngUpgrade
// case). Here we do normalize passed data structure to be an array of arrays to avoid
// complex checks down the line.
// We also normalize the length of the passed in projectable nodes (to match the number of
// <ng-container> slots defined by a component).
projection.push(nodesforSlot != null ? Array.from(nodesforSlot) : null);
}
projectNodes(tElementNode, this.ngContentSelectors, projectableNodes);
}

// TODO: should LifecycleHooksFeature and other host features be generated by the compiler and
// executed here?
// Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref
component =
createRootComponent(componentView, this.componentDef, rootLView, [LifecycleHooksFeature]);
component = createRootComponent(
componentView, this.componentDef, rootDirectives, rootLView, [LifecycleHooksFeature]);
renderView(rootTView, rootLView, null);
} finally {
leaveView();
Expand Down Expand Up @@ -321,104 +306,153 @@ export const NULL_INJECTOR: Injector = {
}
};

/** Creates a TNode that can be used to instantiate a root component. */
function createRootComponentTNode(lView: LView, rNode: RNode): TElementNode {
const tView = lView[TVIEW];
const index = HEADER_OFFSET;
ngDevMode && assertIndexInRange(lView, index);
lView[index] = rNode;

// '#host' is added here as we don't know the real host DOM name (we don't want to read it) and at
// the same time we want to communicate the debug `TNode` that this is a special `TNode`
// representing a host element.
return getOrCreateTNode(tView, index, TNodeType.Element, '#host', null);
}

/**
* Creates the root component view and the root component node.
*
* @param rNode Render host element.
* @param def ComponentDef
* @param rootComponentDef ComponentDef
* @param rootView The parent view where the host node is stored
* @param rendererFactory Factory to be used for creating child renderers.
* @param hostRenderer The current renderer
* @param sanitizer The sanitizer, if provided
*
* @returns Component view created
*/
export function createRootComponentView(
rNode: RElement|null, def: ComponentDef<any>, rootView: LView, rendererFactory: RendererFactory,
function createRootComponentView(
tNode: TElementNode, rNode: RElement|null, rootComponentDef: ComponentDef<any>,
rootDirectives: DirectiveDef<any>[], rootView: LView, rendererFactory: RendererFactory,
hostRenderer: Renderer, sanitizer?: Sanitizer|null): LView {
const tView = rootView[TVIEW];
const index = HEADER_OFFSET;
ngDevMode && assertIndexInRange(rootView, index);
rootView[index] = rNode;
// '#host' is added here as we don't know the real host DOM name (we don't want to read it) and at
// the same time we want to communicate the debug `TNode` that this is a special `TNode`
// representing a host element.
const tNode: TElementNode = getOrCreateTNode(tView, index, TNodeType.Element, '#host', null);
const mergedAttrs = tNode.mergedAttrs = def.hostAttrs;
if (mergedAttrs !== null) {
computeStaticStyling(tNode, mergedAttrs, true);
if (rNode !== null) {
setUpAttributes(hostRenderer, rNode, mergedAttrs);
if (tNode.classes !== null) {
writeDirectClass(hostRenderer, rNode, tNode.classes);
}
if (tNode.styles !== null) {
writeDirectStyle(hostRenderer, rNode, tNode.styles);
}
}
}
applyRootComponentStyling(rootDirectives, tNode, rNode, hostRenderer);

const viewRenderer = rendererFactory.createRenderer(rNode, def);
const viewRenderer = rendererFactory.createRenderer(rNode, rootComponentDef);
const componentView = createLView(
rootView, getOrCreateComponentTView(def), null,
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[index], tNode,
rendererFactory, viewRenderer, sanitizer || null, null, null);
rootView, getOrCreateComponentTView(rootComponentDef), null,
rootComponentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[tNode.index],
tNode, rendererFactory, viewRenderer, sanitizer || null, null, null);

if (tView.firstCreatePass) {
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type);
diPublicInInjector(
getOrCreateNodeInjectorForNode(tNode, rootView), tView, rootComponentDef.type);
markAsComponentHost(tView, tNode, 0);
initTNodeFlags(tNode, rootView.length, 1);
}

addToViewTree(rootView, componentView);

// Store component view at node index, with node as the HOST
return rootView[index] = componentView;
return rootView[tNode.index] = componentView;
}

/** Sets up the styling information on a root component. */
function applyRootComponentStyling(
rootDirectives: DirectiveDef<any>[], tNode: TElementNode, rNode: RElement|null,
hostRenderer: Renderer): void {
for (const def of rootDirectives) {
tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs);
}

if (tNode.mergedAttrs !== null) {
computeStaticStyling(tNode, tNode.mergedAttrs, true);

if (rNode !== null) {
setupStaticAttributes(hostRenderer, rNode, tNode);
}
}
}

/**
* Creates a root component and sets it up with features and host bindings.Shared by
* renderComponent() and ViewContainerRef.createComponent().
*/
export function createRootComponent<T>(
componentView: LView, componentDef: ComponentDef<T>, rootLView: LView,
hostFeatures: HostFeature[]|null): any {
function createRootComponent<T>(
componentView: LView, rootComponentDef: ComponentDef<T>, rootDirectives: DirectiveDef<any>[],
rootLView: LView, hostFeatures: HostFeature[]|null): any {
const rootTNode = getCurrentTNode() as TElementNode;
ngDevMode && assertDefined(rootTNode, 'tNode should have been already created');
const tView = rootLView[TVIEW];
// Create directive instance with factory() and store at next index in viewData
const component = instantiateRootComponent(tView, rootLView, componentDef);
const native = getNativeByTNode(rootTNode, rootLView);

initializeDirectives(tView, rootLView, rootTNode, rootDirectives, null);

for (let i = 0; i < rootDirectives.length; i++) {
const directiveIndex = rootTNode.directiveStart + i;
const directiveInstance = getNodeInjectable(rootLView, tView, directiveIndex, rootTNode);
attachPatchData(directiveInstance, rootLView);
}

invokeDirectivesHostBindings(tView, rootLView, rootTNode);

// Root view only contains an instance of this component,
// so we use a reference to that component instance as a context.
if (native) {
attachPatchData(native, rootLView);
}

// We're guaranteed for the `componentOffset` to be positive here
// since a root component always matches a component def.
ngDevMode &&
assertGreaterThan(rootTNode.componentOffset, -1, 'componentOffset must be great than -1');
const component = getNodeInjectable(
rootLView, tView, rootTNode.directiveStart + rootTNode.componentOffset, rootTNode);
componentView[CONTEXT] = rootLView[CONTEXT] = component;

if (hostFeatures !== null) {
for (const feature of hostFeatures) {
feature(component, componentDef);
feature(component, rootComponentDef);
}
}

// We want to generate an empty QueryList for root content queries for backwards
// compatibility with ViewEngine.
if (componentDef.contentQueries) {
const tNode = getCurrentTNode()!;
ngDevMode && assertDefined(tNode, 'TNode expected');
componentDef.contentQueries(RenderFlags.Create, component, tNode.directiveStart);
}
executeContentQueries(tView, rootTNode, componentView);

const rootTNode = getCurrentTNode()!;
ngDevMode && assertDefined(rootTNode, 'tNode should have been already created');
if (tView.firstCreatePass &&
(componentDef.hostBindings !== null || componentDef.hostAttrs !== null)) {
setSelectedIndex(rootTNode.index);
return component;
}

const rootTView = rootLView[TVIEW];
registerHostBindingOpCodes(
rootTView, rootTNode, rootLView, rootTNode.directiveStart, rootTNode.directiveEnd,
componentDef);
/** Sets the static attributes on a root component. */
function setRootNodeAttributes(
hostRenderer: Renderer2, componentDef: ComponentDef<unknown>, hostRNode: RElement,
rootSelectorOrNode: any) {
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(componentDef.selectors[0]);
if (attrs) {
setUpAttributes(hostRenderer, hostRNode, attrs);
}
if (classes && classes.length > 0) {
writeDirectClass(hostRenderer, hostRNode, classes.join(' '));
}
}
}

invokeHostBindingsInCreationMode(componentDef, component);
/** Projects the `projectableNodes` that were specified when creating a root component. */
function projectNodes(
tNode: TElementNode, ngContentSelectors: string[], projectableNodes: any[][]) {
const projection: (TNode|RNode[]|null)[] = tNode.projection = [];
for (let i = 0; i < ngContentSelectors.length; i++) {
const nodesforSlot = projectableNodes[i];
// Projectable nodes can be passed as array of arrays or an array of iterables (ngUpgrade
// case). Here we do normalize passed data structure to be an array of arrays to avoid
// complex checks down the line.
// We also normalize the length of the passed in projectable nodes (to match the number of
// <ng-container> slots defined by a component).
projection.push(nodesforSlot != null ? Array.from(nodesforSlot) : null);
}
return component;
}

/**
Expand Down
17 changes: 2 additions & 15 deletions packages/core/src/render3/instructions/element.ts
Expand Up @@ -15,10 +15,9 @@ import {RElement} from '../interfaces/renderer_dom';
import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, LView, RENDERER, TView} from '../interfaces/view';
import {assertTNodeType} from '../node_assert';
import {appendChild, createElementNode, writeDirectClass, writeDirectStyle} from '../node_manipulation';
import {appendChild, createElementNode, setupStaticAttributes} from '../node_manipulation';
import {decreaseElementDepthCount, getBindingIndex, getCurrentTNode, getElementDepthCount, getLView, getNamespace, getTView, increaseElementDepthCount, isCurrentTNodeParent, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
import {computeStaticStyling} from '../styling/static_styling';
import {setUpAttributes} from '../util/attrs_utils';
import {getConstant} from '../util/view_utils';

import {validateElementIsKnown} from './element_validation';
Expand Down Expand Up @@ -92,19 +91,7 @@ export function ɵɵelementStart(
adjustedIndex, tView, lView, native, name, attrsIndex, localRefsIndex) :
tView.data[adjustedIndex] as TElementNode;
setCurrentTNode(tNode, true);

const mergedAttrs = tNode.mergedAttrs;
if (mergedAttrs !== null) {
setUpAttributes(renderer, native, mergedAttrs);
}
const classes = tNode.classes;
if (classes !== null) {
writeDirectClass(renderer, native, classes);
}
const styles = tNode.styles;
if (styles !== null) {
writeDirectStyle(renderer, native, styles);
}
setupStaticAttributes(renderer, native, tNode);

if ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) {
// In the i18n case, the translation may have removed this element, so only add it if it is not
Expand Down

0 comments on commit 002ee32

Please sign in to comment.