Skip to content

Commit e7f0953

Browse files
committed
main.DeltaUpdates: use initAsync(), and also update the JSDoc comments #6887
1 parent 86c1c92 commit e7f0953

1 file changed

Lines changed: 154 additions & 85 deletions

File tree

src/main/DeltaUpdates.mjs

Lines changed: 154 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import {voidAttributes} from '../vdom/domConstants.mjs';
55
const NeoConfig = Neo.config;
66

77
/**
8-
* Logic to apply the deltas generated by vdom.Helper to the real DOM
8+
* Manages and applies the Virtual DOM (VDom) delta updates generated by `Neo.vdom.Helper` to the real browser DOM.
9+
* This class acts as the bridge between the VDom worker's calculated changes and the actual rendering on the main thread.
10+
* It orchestrates various DOM manipulation operations such as node insertions, removals, moves, attribute updates,
11+
* and handles dynamic renderer switching based on `Neo.config.useDomApiRenderer`.
12+
*
13+
* As a singleton per browser window, it provides a centralized and efficient mechanism for synchronized DOM updates,
14+
* ensuring the UI accurately reflects the application state.
915
* @class Neo.main.DeltaUpdates
1016
* @extends Neo.core.Base
1117
* @singleton
@@ -48,56 +54,24 @@ class DeltaUpdates extends Base {
4854
* @protected
4955
*/
5056
logDeltasIntervalId = 0
51-
/**
52-
* Private property to store the dynamically loaded renderer module.
53-
* @member {Neo.main.render.DomApiRenderer|Neo.main.render.DomApiRenderer|null} #renderer=null
54-
* @private
55-
*/
56-
#renderer = null
57-
/**
58-
* Private property to signal that the renderer module has been loaded.
59-
* This will be a Promise that resolves when the module is ready.
60-
* @private
61-
* @member {Promise<void>|null} #_readyPromise
62-
*/
63-
#_readyPromise = null
6457

6558
/**
6659
* @param {Object} config
6760
*/
6861
construct(config) {
6962
super.construct(config);
7063

71-
let me = this,
72-
{environment} = NeoConfig;
64+
let {environment} = NeoConfig;
7365

7466
if (NeoConfig.renderCountDeltas) {
75-
me.renderCountDeltas = true
67+
this.renderCountDeltas = true
7668
}
7769

7870
// We need different publicPath values for the main thread inside the webpack based dist envs,
7971
// depending on the hierarchy level of the app entry point
8072
if (environment === 'dist/development' || environment === 'dist/production') {
8173
__webpack_require__.p = NeoConfig.basePath.substring(6)
8274
}
83-
84-
// Initiate the asynchronous loading of the renderer here.
85-
me.#_readyPromise = (async () => {
86-
try {
87-
let module;
88-
89-
if (NeoConfig.useDomApiRenderer) {
90-
module = await import('./render/DomApiRenderer.mjs')
91-
} else {
92-
module = await import('./render/StringBasedRenderer.mjs')
93-
}
94-
95-
me.#renderer = module.default
96-
} catch (err) {
97-
console.error('DeltaUpdates: Failed to load renderer module:', err);
98-
throw err // Re-throw to propagate initialization failures
99-
}
100-
})()
10175
}
10276

10377
/**
@@ -130,8 +104,13 @@ class DeltaUpdates extends Base {
130104
}
131105

132106
/**
133-
* @param {HTMLElement} node
134-
* @param {String} nodeName
107+
* Changes the tag name (nodeName) of an existing HTMLElement in the DOM.
108+
* This operation is performed by creating a new HTML element with the desired `nodeName`,
109+
* meticulously copying all attributes and the `innerHTML` from the original `node` to the new one,
110+
* and then seamlessly replacing the original `node` with the newly created element within its parent.
111+
*
112+
* @param {HTMLElement} node The existing DOM HTMLElement whose tag name needs to be changed.
113+
* @param {String} nodeName The new tag name (e.g., 'div', 'span', 'p') for the element.
135114
*/
136115
changeNodeName(node, nodeName) {
137116
let {attributes} = node,
@@ -160,21 +139,49 @@ class DeltaUpdates extends Base {
160139
DomAccess.getElement(id)?.focus()
161140
}
162141

142+
/**
143+
* Imports either (if not already imported):
144+
* `Neo.main.render.DomApiRenderer` if Neo.config.useDomApiRenderer === true
145+
* `Neo.main.render.StringBasedRenderer` if Neo.config.useDomApiRenderer === false
146+
* @returns {Promise<void>}
147+
* @protected
148+
*/
149+
async importRenderer() {
150+
const {render} = Neo.main;
151+
152+
if (NeoConfig.useDomApiRenderer) {
153+
if (!render?.DomApiRenderer) {
154+
await import('./render/DomApiRenderer.mjs')
155+
}
156+
} else {
157+
if (!render?.StringBasedRenderer) {
158+
await import('./render/StringBasedRenderer.mjs')
159+
}
160+
}
161+
}
162+
163+
/**
164+
* @returns {Promise<void>}
165+
*/
166+
async initAsync() {
167+
super.initAsync();
168+
169+
let me = this;
170+
171+
// Subscribe to global Neo.config changes for dynamic renderer switching.
172+
Neo.worker.Manager.on({
173+
neoConfigChange: me.onNeoConfigChange,
174+
scope : me
175+
});
176+
177+
await me.importRenderer()
178+
}
179+
163180
/**
164181
* Inserts a new node into the DOM tree based on delta updates.
165182
* This method handles both string-based (outerHTML) and direct DOM API (vnode) mounting.
166183
* It ensures the node is inserted at the correct index within the parent.
167-
*
168-
* Implementation Details & Considerations:
169-
* - `parentNode.children` contains only element nodes (tags).
170-
* - `parentNode.childNodes` contains all nodes, including text and comment nodes.
171-
* - Since every `vtype:'text'` is wrapped inside a comment block (as an ID),
172-
* calculating a "realIndex" is necessary for string-based insertions to
173-
* correctly account for non-element nodes.
174-
* - `insertAdjacentHTML()` is generally faster than creating a node via template,
175-
* but it's only available for manipulating children (elements), not `childNodes` (all nodes).
176-
* - For performance, in cases where there are no comment nodes (i.e., no wrapped text nodes),
177-
* the method prioritizes `insertAdjacentHTML()` when `useDomApiRenderer` is false.
184+
* This method is synchronous and *expects* the appropriate renderer (DomApiRenderer or StringBasedRenderer) to be already loaded.
178185
*
179186
* @param {Object} delta
180187
* @param {Boolean} delta.hasLeadingTextChildren Flag to honor leading comments, which require special treatment.
@@ -184,33 +191,47 @@ class DeltaUpdates extends Base {
184191
* @param {Neo.vdom.VNode} [delta.vnode] The VNode representation of the new node (for direct DOM API mounting).
185192
*/
186193
insertNode({hasLeadingTextChildren, index, outerHTML, parentId, vnode}) {
187-
let me = this;
188-
189-
// This method is synchronous and *expects* the renderer to be loaded
190-
if (!me.#renderer) {
191-
console.error('DeltaUpdates renderer not ready during insertNode!');
192-
return
193-
}
194+
this.checkRendererAvailability();
194195

195-
const parentNode = DomAccess.getElementOrBody(parentId);
196+
let {render} = Neo.main,
197+
parentNode = DomAccess.getElementOrBody(parentId);
196198

197199
if (parentNode) {
198200
if (NeoConfig.useDomApiRenderer) {
199-
me.#renderer.createDomTree({index, isRoot: true, parentNode, vnode})
201+
render.DomApiRenderer.createDomTree({index, isRoot: true, parentNode, vnode})
200202
} else {
201-
me.#renderer.insertNodeAsString({hasLeadingTextChildren, index, outerHTML, parentNode})
203+
render.StringBasedRenderer.insertNodeAsString({hasLeadingTextChildren, index, outerHTML, parentNode})
204+
}
205+
}
206+
}
207+
208+
/**
209+
*
210+
*/
211+
checkRendererAvailability() {
212+
const {render} = Neo.main;
213+
214+
if (NeoConfig.useDomApiRenderer) {
215+
if (!render?.DomApiRenderer) {
216+
throw new Error('Neo.main.DeltaUpdates: DomApiRenderer is not loaded yet!')
217+
}
218+
} else {
219+
if (!render?.StringBasedRenderer) {
220+
throw new Error('Neo.main.DeltaUpdates: StringBasedRenderer is not loaded yet!')
202221
}
203222
}
204223
}
205224

206225
/**
207-
* Moves an existing DOM node to a new position within its parent
208-
* or to a new parent.
209-
* This method directly manipulates the DOM using the pre-calculated physical index.
226+
* Moves an existing DOM node to a new position within its parent or to a new parent.
227+
* This method directly manipulates the DOM using the pre-calculated physical index,
228+
* accounting for potential text nodes wrapped in comments.
229+
* It performs a direct sibling swap when an element is immediately followed by its target position,
230+
* which is necessary to prevent attempting to replace a node with itself.
210231
*
211232
* @param {Object} delta
212233
* @param {String} delta.id The ID of the DOM node to move.
213-
* @param {Number} delta.index The physical index at which to insert the node
234+
* @param {Number} delta.index The physical index at which to insert the node within the target parent's childNodes.
214235
* @param {String} delta.parentId The ID of the target parent DOM node.
215236
*/
216237
moveNode({id, index, parentId}) {
@@ -239,8 +260,25 @@ class DeltaUpdates extends Base {
239260
}
240261

241262
/**
263+
* Handler for global Neo.config changes.
264+
* If the `Neo.config.useDomApiRenderer` value changes, this method dynamically loads the renderer.
265+
* @param {Object} config
266+
* @return {Promise<void>}
267+
*/
268+
async onNeoConfigChange(config) {
269+
if (Object.hasOwn(config, 'useDomApiRenderer')) {
270+
await this.importRenderer()
271+
}
272+
}
273+
274+
/**
275+
* Clears all child nodes of a given parent DOM node.
276+
* This is achieved by setting its `innerHTML` property to an empty string,
277+
* which is generally considered the fastest and most efficient way to remove
278+
* all children from a DOM element in modern browsers.
279+
*
242280
* @param {Object} delta
243-
* @param {String} delta.parentId
281+
* @param {String} delta.parentId The ID of the parent DOM node whose children will be removed.
244282
*/
245283
removeAll({parentId}) {
246284
let node = DomAccess.getElement(parentId);
@@ -251,9 +289,13 @@ class DeltaUpdates extends Base {
251289
}
252290

253291
/**
292+
* Removes a DOM node from its parent.
293+
* This method handles both standard HTML elements and virtual text nodes,
294+
* which are typically wrapped within comment nodes in the DOM.
295+
*
254296
* @param {Object} delta
255-
* @param {String} delta.id
256-
* @param {String} delta.parentId
297+
* @param {String} delta.id The ID of the DOM node to remove.
298+
* @param {String} delta.parentId The ID of the parent DOM node (required for text node removal).
257299
*/
258300
removeNode({id, parentId}) {
259301
const node = DomAccess.getElement(id);
@@ -289,10 +331,17 @@ class DeltaUpdates extends Base {
289331
}
290332

291333
/**
334+
* Replaces an existing child DOM node (`fromId`) with a new DOM node (`toId`)
335+
* within a specified parent DOM node (`parentId`).
336+
* This operation directly invokes the native `Node.replaceChild()` API,
337+
* performing an atomic swap of the elements in the DOM tree.
338+
* It is typically used when a specific DOM element needs to be completely
339+
* exchanged for a different one at the same position.
340+
*
292341
* @param {Object} delta
293-
* @param {String} delta.fromId
294-
* @param {String} delta.parentId
295-
* @param {String} delta.toId
342+
* @param {String} delta.fromId The ID of the existing child DOM node to be replaced.
343+
* @param {String} delta.parentId The ID of the parent DOM node containing the child to be replaced.
344+
* @param {String} delta.toId The ID of the new DOM node that will replace the old one.
296345
*/
297346
replaceChild({fromId, parentId, toId}) {
298347
let node = DomAccess.getElement(parentId);
@@ -301,13 +350,20 @@ class DeltaUpdates extends Base {
301350
}
302351

303352
/**
353+
* Updates various properties of an existing DOM node based on the provided delta.
354+
* This includes updating attributes, class names, inner HTML, node name, and inline styles.
355+
* It handles specific cases for attribute types (e.g., boolean attributes, 'value')
356+
* and style properties (e.g., '!important').
357+
*
304358
* @param {Object} delta
305-
* @param {Object} [delta.attributes]
306-
* @param {String} [delta.cls]
307-
* @param {String} [delta.id]
308-
* @param {String} [delta.innerHTML]
309-
* @param {String} [delta.outerHTML]
310-
* @param {Object} [delta.style]
359+
* @param {String} delta.id The ID of the DOM node to update.
360+
* @param {Object} [delta.attributes] An object containing attribute key-value pairs to update or remove (if value is null/empty).
361+
* @param {Object} [delta.cls] An object containing 'add' and/or 'remove' arrays for CSS classes.
362+
* @param {String} [delta.innerHTML] The new inner HTML content for the node.
363+
* @param {String} [delta.nodeName] The new tag name for the node (will trigger a node replacement).
364+
* @param {String} [delta.outerHTML] The new outer HTML content for the node (will trigger a node replacement).
365+
* @param {Object} [delta.style] An object containing CSS style properties to update. Values can include '!important'.
366+
* @param {String} [delta.textContent] The new text content for the node (replaces innerHTML if present).
311367
*/
312368
updateNode(delta) {
313369
let me = this,
@@ -380,10 +436,16 @@ class DeltaUpdates extends Base {
380436
}
381437

382438
/**
439+
* Updates the text content of a virtual text node within the DOM.
440+
* Virtual text nodes are rendered within the DOM as a pair of HTML comments,
441+
* with their content embedded between them. This method locates the specific
442+
* text node by its ID (embedded in the start comment tag) within its parent's
443+
* innerHTML and replaces its content using a regular expression.
444+
*
383445
* @param {Object} delta
384-
* @param {String} delta.id
385-
* @param {String} delta.parentId
386-
* @param {String} delta.value
446+
* @param {String} delta.id The unique ID of the virtual text node, which is embedded in its opening comment tag.
447+
* @param {String} delta.parentId The ID of the parent DOM node whose `innerHTML` contains the virtual text node.
448+
* @param {String} delta.value The new text content to be applied to the virtual text node.
387449
*/
388450
updateVtext({id, parentId, value}) {
389451
let node = DomAccess.getElement(parentId),
@@ -395,17 +457,24 @@ class DeltaUpdates extends Base {
395457
}
396458

397459
/**
460+
* Applies a set of VDom delta updates to the real DOM.
461+
* This method is the core entry point for rendering changes initiated from the VDom worker.
462+
* It iterates through the provided deltas and dispatches them to specific DOM manipulation
463+
* methods (e.g., insertNode, removeNode, updateNode) based on their `action` property.
464+
* This method expects the appropriate renderer (DomApiRenderer or StringBasedRenderer)
465+
* to be loaded based on `Neo.config.useDomApiRenderer`.
466+
*
398467
* @param {Object} data
399-
* @param {Object|Object[]} data.deltas
400-
* @param {String} data.id
401-
* @param {String} [data.origin='app']
468+
* @param {Object|Object[]} data.deltas An array of delta objects, or a single delta object,
469+
* representing changes to be applied to the DOM.
470+
* Each delta object contains an `action` property
471+
* (e.g., 'insertNode', 'removeNode', 'updateNode', 'moveNode')
472+
* and additional properties relevant to the specific action.
473+
* @param {String} data.id The unique ID of the request, used for sending a reply back to the origin.
474+
* @param {String} [data.origin='app'] The origin of the message (e.g., 'app'), used for sending replies.
402475
*/
403476
update(data) {
404-
// This method is synchronous and *expects* the renderer to be loaded
405-
if (!this.#renderer) {
406-
console.error('DeltaUpdates renderer not ready during insertNode!');
407-
return
408-
}
477+
this.checkRendererAvailability();
409478

410479
let me = this,
411480
{deltas} = data,

0 commit comments

Comments
 (0)