@@ -5,7 +5,13 @@ import {voidAttributes} from '../vdom/domConstants.mjs';
55const 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