Skip to content

Commit c44f087

Browse files
crisbetoalxhub
authored andcommitted
refactor(core): add initial implementation of function to replace metadata at runtime (angular#57953)
Adds the new `ɵɵreplaceMedata` function that can be used to replace the metadata of a component class and re-render all instances in place without refreshing the page. The function isn't used anywhere at the moment, but it will be necessary for future functionality. PR Close angular#57953
1 parent ea54426 commit c44f087

File tree

20 files changed

+2345
-31
lines changed

20 files changed

+2345
-31
lines changed

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,8 @@ export class Identifiers {
410410
static forwardRef: o.ExternalReference = {name: 'forwardRef', moduleName: CORE};
411411
static resolveForwardRef: o.ExternalReference = {name: 'resolveForwardRef', moduleName: CORE};
412412

413+
static replaceMetadata: o.ExternalReference = {name: 'ɵɵreplaceMedata', moduleName: CORE};
414+
413415
static ɵɵdefineInjectable: o.ExternalReference = {name: 'ɵɵdefineInjectable', moduleName: CORE};
414416
static declareInjectable: o.ExternalReference = {name: 'ɵɵngDeclareInjectable', moduleName: CORE};
415417
static InjectableDeclaration: o.ExternalReference = {

packages/core/src/core_render3_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export {
248248
ɵɵdeclareLet,
249249
ɵɵstoreLet,
250250
ɵɵreadContextLet,
251+
ɵɵreplaceMedata,
251252
} from './render3/index';
252253
export {CONTAINER_HEADER_OFFSET as ɵCONTAINER_HEADER_OFFSET} from './render3/interfaces/container';
253254
export {LContext as ɵLContext} from './render3/interfaces/context';

packages/core/src/linker/view_container_ref.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {assertNodeInjector} from '../render3/assert';
2222
import {ComponentFactory as R3ComponentFactory} from '../render3/component_ref';
2323
import {getComponentDef} from '../render3/definition';
2424
import {getParentInjectorLocation, NodeInjector} from '../render3/di';
25-
import {addToViewTree, createLContainer} from '../render3/instructions/shared';
25+
import {addToEndOfViewTree, createLContainer} from '../render3/instructions/shared';
2626
import {
2727
CONTAINER_HEADER_OFFSET,
2828
DEHYDRATED_VIEWS,
@@ -703,7 +703,7 @@ export function createContainerRef(
703703
// `_locateOrCreateAnchorNode`).
704704
lContainer = createLContainer(slotValue, hostLView, null!, hostTNode);
705705
hostLView[hostTNode.index] = lContainer;
706-
addToViewTree(hostLView, lContainer);
706+
addToEndOfViewTree(hostLView, lContainer);
707707
}
708708
_locateOrCreateAnchorNode(lContainer, hostLView, hostTNode, slotValue);
709709

packages/core/src/render3/component_ref.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {Renderer2, RendererFactory2} from '../render/api';
3030
import {Sanitizer} from '../sanitization/sanitizer';
3131
import {assertDefined, assertGreaterThan, assertIndexInRange} from '../util/assert';
3232

33-
import {AfterRenderManager} from './after_render/manager';
3433
import {assertComponentType, assertNoDuplicateDirectives} from './assert';
3534
import {attachPatchData} from './context_discovery';
3635
import {getComponentDef} from './definition';
@@ -41,10 +40,11 @@ import {reportUnknownPropertyError} from './instructions/element_validation';
4140
import {markViewDirty} from './instructions/mark_view_dirty';
4241
import {renderView} from './instructions/render';
4342
import {
44-
addToViewTree,
43+
addToEndOfViewTree,
4544
createLView,
4645
createTView,
4746
executeContentQueries,
47+
getInitialLViewFlagsFromDef,
4848
getOrCreateComponentTView,
4949
getOrCreateTNode,
5050
initializeDirectives,
@@ -534,17 +534,11 @@ function createRootComponentView(
534534
hydrationInfo = retrieveHydrationInfo(hostRNode, rootView[INJECTOR]!);
535535
}
536536
const viewRenderer = environment.rendererFactory.createRenderer(hostRNode, rootComponentDef);
537-
let lViewFlags = LViewFlags.CheckAlways;
538-
if (rootComponentDef.signals) {
539-
lViewFlags = LViewFlags.SignalView;
540-
} else if (rootComponentDef.onPush) {
541-
lViewFlags = LViewFlags.Dirty;
542-
}
543537
const componentView = createLView(
544538
rootView,
545539
getOrCreateComponentTView(rootComponentDef),
546540
null,
547-
lViewFlags,
541+
getInitialLViewFlagsFromDef(rootComponentDef),
548542
rootView[tNode.index],
549543
tNode,
550544
environment,
@@ -558,7 +552,7 @@ function createRootComponentView(
558552
markAsComponentHost(tView, tNode, rootDirectives.length - 1);
559553
}
560554

561-
addToViewTree(rootView, componentView);
555+
addToEndOfViewTree(rootView, componentView);
562556

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

packages/core/src/render3/hmr.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Type} from '../interface/type';
10+
import {assertDefined} from '../util/assert';
11+
import {assertLView} from './assert';
12+
import {getComponentDef} from './definition';
13+
import {assertComponentDef} from './errors';
14+
import {refreshView} from './instructions/change_detection';
15+
import {renderView} from './instructions/render';
16+
import {
17+
createLView,
18+
getInitialLViewFlagsFromDef,
19+
getOrCreateComponentTView,
20+
} from './instructions/shared';
21+
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
22+
import {ComponentDef} from './interfaces/definition';
23+
import {getTrackedLViews} from './interfaces/lview_tracking';
24+
import {isTNodeShape, TElementNode, TNodeFlags, TNodeType} from './interfaces/node';
25+
import {isLContainer, isLView} from './interfaces/type_checks';
26+
import {
27+
CHILD_HEAD,
28+
CHILD_TAIL,
29+
CONTEXT,
30+
ENVIRONMENT,
31+
FLAGS,
32+
HEADER_OFFSET,
33+
HOST,
34+
LView,
35+
LViewFlags,
36+
NEXT,
37+
PARENT,
38+
T_HOST,
39+
TVIEW,
40+
} from './interfaces/view';
41+
import {assertTNodeType} from './node_assert';
42+
import {destroyLView, removeViewFromDOM} from './node_manipulation';
43+
44+
/**
45+
* Replaces the metadata of a component type and re-renders all live instances of the component.
46+
* @param type Class whose metadata will be replaced.
47+
* @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked.
48+
* @codeGenApi
49+
*/
50+
export function ɵɵreplaceMedata(type: Type<unknown>, applyMetadata: () => void) {
51+
ngDevMode && assertComponentDef(type);
52+
const oldDef = getComponentDef(type)!;
53+
54+
// The reason `applyMetadata` is a callback that is invoked (almost) immediately is because
55+
// the compiler usually produces more code than just the component definition, e.g. there
56+
// can be functions for embedded views, the variables for the constant pool and `setClassMetadata`
57+
// calls. The callback allows us to keep them isolate from the rest of the app and to invoke
58+
// them at the right time.
59+
applyMetadata();
60+
61+
// If a `tView` hasn't been created yet, it means that this component hasn't been instantianted
62+
// before. In this case there's nothing left for us to do aside from patching it in.
63+
if (oldDef.tView) {
64+
const trackedViews = getTrackedLViews().values();
65+
for (const root of trackedViews) {
66+
// Note: we have the additional check, because `IsRoot` can also indicate
67+
// a component created through something like `createComponent`.
68+
if (root[FLAGS] & LViewFlags.IsRoot && root[PARENT] === null) {
69+
recreateMatchingLViews(oldDef, root);
70+
}
71+
}
72+
}
73+
}
74+
75+
/**
76+
* Finds all LViews matching a specific component definition and recreates them.
77+
* @param def Component definition to search for.
78+
* @param rootLView View from which to start the search.
79+
*/
80+
function recreateMatchingLViews(def: ComponentDef<unknown>, rootLView: LView): void {
81+
ngDevMode &&
82+
assertDefined(
83+
def.tView,
84+
'Expected a component definition that has been instantiated at least once',
85+
);
86+
87+
const tView = rootLView[TVIEW];
88+
89+
// Use `tView` to match the LView since `instanceof` can
90+
// produce false positives when using inheritance.
91+
if (tView === def.tView) {
92+
ngDevMode && assertComponentDef(def.type);
93+
recreateLView(getComponentDef(def.type)!, rootLView);
94+
return;
95+
}
96+
97+
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
98+
const current = rootLView[i];
99+
100+
if (isLContainer(current)) {
101+
for (let i = CONTAINER_HEADER_OFFSET; i < current.length; i++) {
102+
recreateMatchingLViews(def, current[i]);
103+
}
104+
} else if (isLView(current)) {
105+
recreateMatchingLViews(def, current);
106+
}
107+
}
108+
}
109+
110+
/**
111+
* Recreates an LView in-place from a new component definition.
112+
* @param def Definition from which to recreate the view.
113+
* @param lView View to be recreated.
114+
*/
115+
function recreateLView(def: ComponentDef<unknown>, lView: LView<unknown>): void {
116+
const instance = lView[CONTEXT];
117+
const host = lView[HOST]!;
118+
// In theory the parent can also be an LContainer, but it appears like that's
119+
// only the case for embedded views which we won't be replacing here.
120+
const parentLView = lView[PARENT] as LView;
121+
ngDevMode && assertLView(parentLView);
122+
const tNode = lView[T_HOST] as TElementNode;
123+
ngDevMode && assertTNodeType(tNode, TNodeType.Element);
124+
125+
// Recreate the TView since the template might've changed.
126+
const newTView = getOrCreateComponentTView(def);
127+
128+
// Create a new LView from the new TView, but reusing the existing TNode and DOM node.
129+
const newLView = createLView(
130+
parentLView,
131+
newTView,
132+
instance,
133+
getInitialLViewFlagsFromDef(def),
134+
host,
135+
tNode,
136+
null,
137+
lView[ENVIRONMENT].rendererFactory.createRenderer(host, def),
138+
null,
139+
null,
140+
null,
141+
);
142+
143+
// Detach the LView from its current place in the tree so we don't
144+
// start traversing any siblings and modifying their structure.
145+
replaceLViewInTree(parentLView, lView, newLView, tNode.index);
146+
147+
// Destroy the detached LView.
148+
destroyLView(lView[TVIEW], lView);
149+
150+
// Remove the nodes associated with the destroyed LView. This removes the
151+
// descendants, but not the host which we want to stay in place.
152+
removeViewFromDOM(lView[TVIEW], lView);
153+
154+
// Reset the content projection state of the TNode before the first render.
155+
// Note that this has to happen after the LView has been destroyed or we
156+
// risk some projected nodes not being removed correctly.
157+
resetProjectionState(tNode);
158+
159+
// Creation pass for the new view.
160+
renderView(newTView, newLView, instance);
161+
162+
// Update pass for the new view.
163+
refreshView(newTView, newLView, newTView.template, instance);
164+
}
165+
166+
/**
167+
* Replaces one LView in the tree with another one.
168+
* @param parentLView Parent of the LView being replaced.
169+
* @param oldLView LView being replaced.
170+
* @param newLView Replacement LView to be inserted.
171+
* @param index Index at which the LView should be inserted.
172+
*/
173+
function replaceLViewInTree(
174+
parentLView: LView,
175+
oldLView: LView,
176+
newLView: LView,
177+
index: number,
178+
): void {
179+
// Update the sibling whose `NEXT` pointer refers to the old view.
180+
for (let i = HEADER_OFFSET; i < parentLView[TVIEW].bindingStartIndex; i++) {
181+
const current = parentLView[i];
182+
183+
if ((isLView(current) || isLContainer(current)) && current[NEXT] === oldLView) {
184+
current[NEXT] = newLView;
185+
break;
186+
}
187+
}
188+
189+
// Set the new view as the head, if the old view was first.
190+
if (parentLView[CHILD_HEAD] === oldLView) {
191+
parentLView[CHILD_HEAD] = newLView;
192+
}
193+
194+
// Set the new view as the tail, if the old view was last.
195+
if (parentLView[CHILD_TAIL] === oldLView) {
196+
parentLView[CHILD_TAIL] = newLView;
197+
}
198+
199+
// Update the `NEXT` pointer to the same as the old view.
200+
newLView[NEXT] = oldLView[NEXT];
201+
202+
// Clear out the `NEXT` of the old view.
203+
oldLView[NEXT] = null;
204+
205+
// Insert the new LView at the correct index.
206+
parentLView[index] = newLView;
207+
}
208+
209+
/**
210+
* Child nodes mutate the `projection` state of their parent node as they're being projected.
211+
* This function resets the `project` back to its initial state.
212+
* @param tNode
213+
*/
214+
function resetProjectionState(tNode: TElementNode): void {
215+
// The `projection` is mutated by child nodes as they're being projected. We need to
216+
// reset it to the initial state so projection works after the template is swapped out.
217+
if (tNode.projection !== null) {
218+
for (const current of tNode.projection) {
219+
if (isTNodeShape(current)) {
220+
// Reset `projectionNext` since it can affect the traversal order during projection.
221+
current.projectionNext = null;
222+
current.flags &= ~TNodeFlags.isProjected;
223+
}
224+
}
225+
tNode.projection = null;
226+
}
227+
}

packages/core/src/render3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export {ɵɵresolveBody, ɵɵresolveDocument, ɵɵresolveWindow} from './util/mi
219219
export {ɵɵtemplateRefExtractor} from './view_engine_compatibility_prebound';
220220
export {ɵɵgetComponentDepsFactory} from './local_compilation';
221221
export {ɵsetClassDebugInfo} from './debug/set_debug_info';
222+
export {ɵɵreplaceMedata} from './hmr';
222223

223224
export {
224225
ComponentDebugMetadata,

packages/core/src/render3/instructions/shared.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,26 +1641,34 @@ export function configureViewWithDirective<T>(
16411641
);
16421642
}
16431643

1644+
/**
1645+
* Gets the initial set of LView flags based on the component definition that the LView represents.
1646+
* @param def Component definition from which to determine the flags.
1647+
*/
1648+
export function getInitialLViewFlagsFromDef(def: ComponentDef<unknown>): LViewFlags {
1649+
let flags = LViewFlags.CheckAlways;
1650+
if (def.signals) {
1651+
flags = LViewFlags.SignalView;
1652+
} else if (def.onPush) {
1653+
flags = LViewFlags.Dirty;
1654+
}
1655+
return flags;
1656+
}
1657+
16441658
function addComponentLogic<T>(lView: LView, hostTNode: TElementNode, def: ComponentDef<T>): void {
16451659
const native = getNativeByTNode(hostTNode, lView) as RElement;
16461660
const tView = getOrCreateComponentTView(def);
16471661

16481662
// Only component views should be added to the view tree directly. Embedded views are
16491663
// accessed through their containers because they may be removed / re-added later.
16501664
const rendererFactory = lView[ENVIRONMENT].rendererFactory;
1651-
let lViewFlags = LViewFlags.CheckAlways;
1652-
if (def.signals) {
1653-
lViewFlags = LViewFlags.SignalView;
1654-
} else if (def.onPush) {
1655-
lViewFlags = LViewFlags.Dirty;
1656-
}
1657-
const componentView = addToViewTree(
1665+
const componentView = addToEndOfViewTree(
16581666
lView,
16591667
createLView(
16601668
lView,
16611669
tView,
16621670
null,
1663-
lViewFlags,
1671+
getInitialLViewFlagsFromDef(def),
16641672
native,
16651673
hostTNode as TElementNode,
16661674
null,
@@ -1894,7 +1902,10 @@ export function refreshContentQueries(tView: TView, lView: LView): void {
18941902
* @param lViewOrLContainer The LView or LContainer to add to the view tree
18951903
* @returns The state passed in
18961904
*/
1897-
export function addToViewTree<T extends LView | LContainer>(lView: LView, lViewOrLContainer: T): T {
1905+
export function addToEndOfViewTree<T extends LView | LContainer>(
1906+
lView: LView,
1907+
lViewOrLContainer: T,
1908+
): T {
18981909
// TODO(benlesh/misko): This implementation is incorrect, because it always adds the LContainer
18991910
// to the end of the queue, which means if the developer retrieves the LContainers from RNodes out
19001911
// of order, the change detection will run out of order, as the act of retrieving the the

packages/core/src/render3/instructions/template.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
import {getConstant} from '../util/view_utils';
3838

3939
import {
40-
addToViewTree,
40+
addToEndOfViewTree,
4141
createDirectivesInstances,
4242
createLContainer,
4343
createTView,
@@ -146,7 +146,7 @@ export function declareTemplate(
146146

147147
const lContainer = createLContainer(comment, declarationLView, comment, tNode);
148148
declarationLView[adjustedIndex] = lContainer;
149-
addToViewTree(declarationLView, lContainer);
149+
addToEndOfViewTree(declarationLView, lContainer);
150150

151151
// If hydration is enabled, looks up dehydrated views in the DOM
152152
// using hydration annotation info and stores those views on LContainer.

0 commit comments

Comments
 (0)