@@ -2,6 +2,7 @@ import Base from '../core/Base.mjs';
22import ClassSystemUtil from '../util/ClassSystem.mjs' ;
33import Config from '../core/Config.mjs' ;
44import Effect from '../core/Effect.mjs' ;
5+ import EffectBatchManager from '../core/EffectBatchManager.mjs' ;
56import Observable from '../core/Observable.mjs' ;
67import { createHierarchicalDataProxy } from './createHierarchicalDataProxy.mjs' ;
78import { isDescriptor } from '../core/ConfigSymbols.mjs' ;
@@ -449,30 +450,33 @@ class Provider extends Base {
449450 }
450451 }
451452 }
452- return Array . from ( keys ) ;
453+
454+ return Array . from ( keys )
453455 }
454456
455457 /**
456- * Internal method to avoid code redundancy.
457- * Use setData() or setDataAtSameLevel() instead.
458+ * This is the core method for setting data, providing a single entry point for all data modifications.
459+ * It handles multiple scenarios:
460+ * 1. **Object-based updates:** If `key` is an object, it recursively calls itself for each key-value pair.
461+ * 2. **Data Records:** If `value` is a `Neo.data.Record`, it is treated as an atomic value and set directly.
462+ * 3. **Bubbling Reactivity:** For a given key (e.g., 'user.name'), it sets the leaf value and then "bubbles up"
463+ * the change, creating new parent objects (e.g., 'user') to ensure that effects depending on any part
464+ * of the path are triggered.
458465 *
459- * This method handles setting data properties, including nested paths and Neo.data.Record instances .
460- * It determines the owning StateProvider in the hierarchy and delegates to #setConfigValue .
466+ * All updates are batched by the public `setData` methods to ensure effects run only once .
467+ * Use `setData()` or `setDataAtSameLevel()` instead of calling this method directly .
461468 *
462- * Passing an originStateProvider param will try to set each key on the closest property match
463- * inside the parent stateProvider chain => setData()
464- * Not passing it will set all values on the stateProvider where the method gets called => setDataAtSameLevel()
465- * @param {Object|String } key
466- * @param {* } value
467- * @param {Neo.state.Provider } [originStateProvider]
469+ * @param {Object|String } key The property to set, or an object of key-value pairs.
470+ * @param {* } value The new value.
471+ * @param {Neo.state.Provider } [originStateProvider] The provider to start the search from for hierarchical updates.
468472 * @protected
469473 */
470474 internalSetData ( key , value , originStateProvider ) {
471475 const me = this ;
472476
473477 // If the value is a Neo.data.Record, treat it as an atomic value
474478 // and set it directly without further recursive processing of its properties.
475- if ( Neo . isObject ( value ) && value . isRecord ) {
479+ if ( Neo . isRecord ( value ) ) {
476480 const
477481 ownerDetails = me . getOwnerOfDataProperty ( key ) ,
478482 targetProvider = ownerDetails ? ownerDetails . owner : ( originStateProvider || me ) ;
@@ -481,42 +485,40 @@ class Provider extends Base {
481485 return
482486 }
483487
484- // If the key is an object, iterate over its entries and recursively call internalSetData.
485- // This handles setting multiple properties at once (e.g., setData({prop1: val1, prop2: val2})).
486488 if ( Neo . isObject ( key ) ) {
487489 Object . entries ( key ) . forEach ( ( [ dataKey , dataValue ] ) => {
488490 me . internalSetData ( dataKey , dataValue , originStateProvider )
489491 } ) ;
490492 return
491493 }
492494
493- // Handle single key/value pairs, including nested paths (e.g., 'user.firstName').
494495 const
495496 ownerDetails = me . getOwnerOfDataProperty ( key ) ,
496- targetProvider = ownerDetails ? ownerDetails . owner : ( originStateProvider || me ) ,
497- pathParts = key . split ( '.' ) ;
498-
499- let currentPath = '' ,
500- currentConfig = null ,
501- currentProvider = targetProvider ;
502-
503- for ( let i = 0 ; i < pathParts . length ; i ++ ) {
504- const part = pathParts [ i ] ;
505- currentPath = currentPath ? `${ currentPath } .${ part } ` : part ;
506- currentConfig = currentProvider . getDataConfig ( currentPath ) ;
507-
508- if ( i === pathParts . length - 1 ) { // Last part of the path
509- // Set the value for the final property in the path.
510- me . #setConfigValue( currentProvider , currentPath , value , null )
511- } else { // Intermediate part of the path
512- // Ensure intermediate paths exist as objects. If not, create them.
513- // If an intermediate path exists but is not an object, overwrite it with an empty object.
514- if ( ! currentConfig ) {
515- currentConfig = new Config ( { } ) ; // Create an empty object config
516- currentProvider . #dataConfigs[ currentPath ] = currentConfig
517- } else if ( ! Neo . isObject ( currentConfig . get ( ) ) ) {
518- currentConfig . set ( { } )
497+ targetProvider = ownerDetails ? ownerDetails . owner : ( originStateProvider || me ) ;
498+
499+ me . #setConfigValue( targetProvider , key , value , null ) ;
500+
501+ // Bubble up the change to parent configs to trigger their effects
502+ let path = key ,
503+ latestValue = value ;
504+
505+ while ( path . includes ( '.' ) ) {
506+ const leafKey = path . split ( '.' ) . pop ( ) ;
507+ path = path . substring ( 0 , path . lastIndexOf ( '.' ) ) ;
508+
509+ const parentConfig = targetProvider . getDataConfig ( path ) ;
510+
511+ if ( parentConfig ) {
512+ const oldParentValue = parentConfig . get ( ) ;
513+ if ( Neo . isObject ( oldParentValue ) ) {
514+ const newParentValue = { ...oldParentValue , [ leafKey ] : latestValue } ;
515+ parentConfig . set ( newParentValue ) ;
516+ latestValue = newParentValue ;
517+ } else {
518+ break // Stop if parent is not an object
519519 }
520+ } else {
521+ break // Stop if parent config does not exist
520522 }
521523 }
522524 }
@@ -622,21 +624,33 @@ class Provider extends Base {
622624 /**
623625 * The method will assign all values to the closest stateProvider where it finds an existing key.
624626 * In case no match is found inside the parent chain, a new data property will get generated.
627+ *
628+ * All updates within a single call are batched to ensure that reactive effects (bindings and formulas)
629+ * are run only once.
630+ *
625631 * @param {Object|String } key
626632 * @param {* } value
627633 */
628634 setData ( key , value ) {
629- this . internalSetData ( key , value , this )
635+ EffectBatchManager . startBatch ( ) ;
636+ this . internalSetData ( key , value , this ) ;
637+ EffectBatchManager . endBatch ( )
630638 }
631639
632640 /**
633641 * Use this method instead of setData() in case you want to enforce
634642 * setting all keys on this instance instead of looking for matches inside parent stateProviders.
643+ *
644+ * All updates within a single call are batched to ensure that reactive effects (bindings and formulas)
645+ * are run only once.
646+ *
635647 * @param {Object|String } key
636648 * @param {* } value
637649 */
638650 setDataAtSameLevel ( key , value ) {
639- this . internalSetData ( key , value )
651+ EffectBatchManager . startBatch ( ) ;
652+ this . internalSetData ( key , value ) ;
653+ EffectBatchManager . endBatch ( )
640654 }
641655}
642656
0 commit comments