refactor(core): reuse existing logic in ComponentRef impl (#59806)

This change removes some code and logic duplication by
re-using the existing functionality. It also pulls some
code into separate methods for clarity.

PR Close #59806
This commit is contained in:
Pawel Kozlowski
2025-01-29 18:10:58 +01:00
committed by Alex Rickabaugh
parent 419634c391
commit 2a0731f9c7
12 changed files with 117 additions and 103 deletions
+106 -103
View File
@@ -16,8 +16,6 @@ import {
import {Injector} from '../di/injector';
import {EnvironmentInjector} from '../di/r3_injector';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {DehydratedView} from '../hydration/interfaces';
import {retrieveHydrationInfo} from '../hydration/utils';
import {Type} from '../interface/type';
import {
ComponentFactory as AbstractComponentFactory,
@@ -28,7 +26,6 @@ import {createElementRef, ElementRef} from '../linker/element_ref';
import {NgModuleRef} from '../linker/ng_module_factory';
import {RendererFactory2} from '../render/api';
import {Sanitizer} from '../sanitization/sanitizer';
import {assertDefined} from '../util/assert';
import {assertComponentType} from './assert';
import {attachPatchData} from './context_discovery';
@@ -43,6 +40,7 @@ import {
createDirectivesInstances,
createLView,
createTView,
getInitialLViewFlagsFromDef,
initializeDirectives,
locateHostElement,
resolveHostDirectives,
@@ -58,7 +56,7 @@ import {
TNode,
TNodeType,
} from './interfaces/node';
import {RNode} from './interfaces/renderer_dom';
import {RElement, RNode} from './interfaces/renderer_dom';
import {
CONTEXT,
HEADER_OFFSET,
@@ -70,16 +68,18 @@ import {
} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {retrieveHydrationInfo} from '../hydration/utils';
import {ChainedInjector} from './chained_injector';
import {createElementNode, setupStaticAttributes} from './dom_node_manipulation';
import {unregisterLView} from './interfaces/lview_tracking';
import {Renderer} from './interfaces/renderer';
import {
extractAttrsAndClassesFromSelector,
stringifyCSSSelectorList,
} from './node_selector_matcher';
import {executeContentQueries} from './queries/query_execution';
import {enterView, getCurrentTNode, getLView, leaveView} from './state';
import {enterView, leaveView} from './state';
import {computeStaticStyling} from './styling/static_styling';
import {getOrCreateTNode} from './tnode_manipulation';
import {mergeHostAttrs} from './util/attrs_utils';
@@ -149,9 +149,74 @@ function toRefArray<
return array;
}
function getNamespace(elementName: string): string | null {
const name = elementName.toLowerCase();
return name === 'svg' ? SVG_NAMESPACE : name === 'math' ? MATH_ML_NAMESPACE : null;
function verifyNotAnOrphanComponent(componentDef: ComponentDef<unknown>) {
// TODO(pk): create assert that verifies ngDevMode
if (
(typeof ngJitMode === 'undefined' || ngJitMode) &&
componentDef.debugInfo?.forbidOrphanRendering
) {
if (depsTracker.isOrphanComponent(componentDef.type)) {
throw new RuntimeError(
RuntimeErrorCode.RUNTIME_DEPS_ORPHAN_COMPONENT,
`Orphan component found! Trying to render the component ${debugStringifyTypeForError(
componentDef.type,
)} without first loading the NgModule that declares it. It is recommended to make this component standalone in order to avoid this error. If this is not possible now, import the component's NgModule in the appropriate NgModule, or the standalone component in which you are trying to render this component. If this is a lazy import, load the NgModule lazily as well and use its module injector.`,
);
}
}
}
function createRootViewInjector(
componentDef: ComponentDef<unknown>,
environmentInjector: EnvironmentInjector | NgModuleRef<any> | undefined,
injector: Injector,
): Injector {
let realEnvironmentInjector =
environmentInjector instanceof EnvironmentInjector
? environmentInjector
: environmentInjector?.injector;
if (realEnvironmentInjector && componentDef.getStandaloneInjector !== null) {
realEnvironmentInjector =
componentDef.getStandaloneInjector(realEnvironmentInjector) || realEnvironmentInjector;
}
const rootViewInjector = realEnvironmentInjector
? new ChainedInjector(injector, realEnvironmentInjector)
: injector;
return rootViewInjector;
}
function createRootLViewEnvironment(rootLViewInjector: Injector): LViewEnvironment {
const rendererFactory = rootLViewInjector.get(RendererFactory2, null);
if (rendererFactory === null) {
throw new RuntimeError(
RuntimeErrorCode.RENDERER_NOT_FOUND,
ngDevMode &&
'Angular was not able to inject a renderer (RendererFactory2). ' +
'Likely this is due to a broken DI hierarchy. ' +
'Make sure that any injector used to create this component has a correct parent.',
);
}
const sanitizer = rootLViewInjector.get(Sanitizer, null);
const changeDetectionScheduler = rootLViewInjector.get(ChangeDetectionScheduler, null);
return {
rendererFactory,
sanitizer,
changeDetectionScheduler,
};
}
function createHostElement(componentDef: ComponentDef<unknown>, render: Renderer): RElement {
// 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.
const tagName = ((componentDef.selectors[0][0] as string) || 'div').toLowerCase();
const namespace =
tagName === 'svg' ? SVG_NAMESPACE : tagName === 'math' ? MATH_ML_NAMESPACE : null;
return createElementNode(render, tagName, namespace);
}
/**
@@ -211,84 +276,8 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
): AbstractComponentRef<T> {
const prevConsumer = setActiveConsumer(null);
try {
// Check if the component is orphan
if (
ngDevMode &&
(typeof ngJitMode === 'undefined' || ngJitMode) &&
this.componentDef.debugInfo?.forbidOrphanRendering
) {
if (depsTracker.isOrphanComponent(this.componentType)) {
throw new RuntimeError(
RuntimeErrorCode.RUNTIME_DEPS_ORPHAN_COMPONENT,
`Orphan component found! Trying to render the component ${debugStringifyTypeForError(
this.componentType,
)} without first loading the NgModule that declares it. It is recommended to make this component standalone in order to avoid this error. If this is not possible now, import the component's NgModule in the appropriate NgModule, or the standalone component in which you are trying to render this component. If this is a lazy import, load the NgModule lazily as well and use its module injector.`,
);
}
}
environmentInjector = environmentInjector || this.ngModule;
let realEnvironmentInjector =
environmentInjector instanceof EnvironmentInjector
? environmentInjector
: environmentInjector?.injector;
if (realEnvironmentInjector && this.componentDef.getStandaloneInjector !== null) {
realEnvironmentInjector =
this.componentDef.getStandaloneInjector(realEnvironmentInjector) ||
realEnvironmentInjector;
}
const rootViewInjector = realEnvironmentInjector
? new ChainedInjector(injector, realEnvironmentInjector)
: injector;
const rendererFactory = rootViewInjector.get(RendererFactory2, null);
if (rendererFactory === null) {
throw new RuntimeError(
RuntimeErrorCode.RENDERER_NOT_FOUND,
ngDevMode &&
'Angular was not able to inject a renderer (RendererFactory2). ' +
'Likely this is due to a broken DI hierarchy. ' +
'Make sure that any injector used to create this component has a correct parent.',
);
}
const sanitizer = rootViewInjector.get(Sanitizer, null);
const changeDetectionScheduler = rootViewInjector.get(ChangeDetectionScheduler, null);
const environment: LViewEnvironment = {
rendererFactory,
sanitizer,
changeDetectionScheduler,
};
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
// 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.
const elementName = (this.componentDef.selectors[0][0] as string) || 'div';
const hostRNode = rootSelectorOrNode
? locateHostElement(
hostRenderer,
rootSelectorOrNode,
this.componentDef.encapsulation,
rootViewInjector,
)
: createElementNode(hostRenderer, elementName, getNamespace(elementName));
let rootFlags = LViewFlags.IsRoot;
if (this.componentDef.signals) {
rootFlags |= LViewFlags.SignalView;
} else if (!this.componentDef.onPush) {
rootFlags |= LViewFlags.CheckAlways;
}
let hydrationInfo: DehydratedView | null = null;
if (hostRNode !== null) {
hydrationInfo = retrieveHydrationInfo(hostRNode, rootViewInjector, true /* isRootView */);
}
const cmpDef = this.componentDef;
ngDevMode && verifyNotAnOrphanComponent(cmpDef);
// Create the root view. Uses empty TView and ContentTemplate.
const rootTView = createTView(
@@ -304,21 +293,39 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
null,
null,
);
const rootViewInjector = createRootViewInjector(
cmpDef,
environmentInjector || this.ngModule,
injector,
);
const environment = createRootLViewEnvironment(rootViewInjector);
const hostRenderer = environment.rendererFactory.createRenderer(null, cmpDef);
const hostElement = rootSelectorOrNode
? locateHostElement(
hostRenderer,
rootSelectorOrNode,
cmpDef.encapsulation,
rootViewInjector,
)
: createHostElement(cmpDef, hostRenderer);
const rootLView = createLView<T>(
null,
rootTView,
null,
rootFlags,
LViewFlags.IsRoot | getInitialLViewFlagsFromDef(cmpDef),
null,
null,
environment,
hostRenderer,
rootViewInjector,
null,
hydrationInfo,
retrieveHydrationInfo(hostElement, rootViewInjector, true /* isRootView */),
);
rootLView[HEADER_OFFSET] = hostRNode;
rootLView[HEADER_OFFSET] = hostElement;
// rootView is the parent when bootstrapping
// TODO(misko): it looks like we are entering view here but we don't really need to as
@@ -334,9 +341,9 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
const tAttributes = rootSelectorOrNode
? ['ng-version', '0.0.0-PLACEHOLDER']
: // Extract attributes and classes from the first selector only to match VE behavior.
extractAttrsAndClassesFromSelector(this.componentDef.selectors[0]);
extractAttrsAndClassesFromSelector(cmpDef.selectors[0]);
// TODO: this logic is shared with the element instruction first create pass
// TODO: this logic is shared with the element instruction first create pass - minus directive matching
const hostTNode = getOrCreateTNode(
rootTView,
HEADER_OFFSET,
@@ -346,13 +353,9 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
);
const [directiveDefs, hostDirectiveDefs] = resolveHostDirectives(rootTView, hostTNode, [
this.componentDef,
cmpDef,
]);
initializeDirectives(rootTView, rootLView, hostTNode, directiveDefs, {}, hostDirectiveDefs);
for (const def of directiveDefs) {
hostTNode.mergedAttrs = mergeHostAttrs(hostTNode.mergedAttrs, def.hostAttrs);
}
hostTNode.mergedAttrs = mergeHostAttrs(hostTNode.mergedAttrs, tAttributes);
computeStaticStyling(hostTNode, hostTNode.mergedAttrs, true);
@@ -360,13 +363,9 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
// 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) {
setupStaticAttributes(hostRenderer, hostRNode, hostTNode);
attachPatchData(hostRNode, rootLView);
}
if (projectableNodes !== undefined) {
projectNodes(hostTNode, this.ngContentSelectors, projectableNodes);
if (hostElement) {
setupStaticAttributes(hostRenderer, hostElement, hostTNode);
attachPatchData(hostElement, rootLView);
}
// TODO(pk): this logic is similar to the instruction code where a node can have directives
@@ -376,6 +375,10 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
// TODO(pk): code / logic duplication with the elementEnd and similar instructions
registerPostOrderHooks(rootTView, hostTNode);
if (projectableNodes !== undefined) {
projectNodes(hostTNode, this.ngContentSelectors, projectableNodes);
}
componentView = getComponentLViewByIndex(hostTNode.index, rootLView);
// TODO(pk): why do we need this logic?
@@ -306,6 +306,7 @@
"getDirectiveDef",
"getFactoryDef",
"getFirstLContainer",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -327,6 +327,7 @@
"getDirectiveDef",
"getFactoryDef",
"getFirstLContainer",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -255,6 +255,7 @@
"getDirectiveDef",
"getFactoryDef",
"getFirstLContainer",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -309,6 +309,7 @@
"getFactoryDef",
"getFirstLContainer",
"getFirstNativeNode",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -372,6 +372,7 @@
"getFactoryOf",
"getFirstLContainer",
"getFirstNativeNode",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -358,6 +358,7 @@
"getFactoryOf",
"getFirstLContainer",
"getFirstNativeNode",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -198,6 +198,7 @@
"getDeclarationTNode",
"getFactoryDef",
"getFirstLContainer",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -263,6 +263,7 @@
"getFactoryDef",
"getFilteredHeaders",
"getFirstLContainer",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -443,6 +443,7 @@
"getFirstNativeNode",
"getIdxOfMatchingSelector",
"getInherited",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -228,6 +228,7 @@
"getDirectiveDef",
"getFactoryDef",
"getFirstLContainer",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",
@@ -301,6 +301,7 @@
"getFactoryDef",
"getFirstLContainer",
"getFirstNativeNode",
"getInitialLViewFlagsFromDef",
"getInjectImplementation",
"getInjectableDef",
"getInjectorDef",