@@ -5,7 +5,15 @@ import { combineSpacedArray, normalizeModelName } from './string_utils';
55import { haveRenderedValuesChanged } from './have_rendered_values_changed' ;
66import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison' ;
77import ValueStore from './ValueStore' ;
8- import { elementBelongsToThisController , getModelDirectiveFromInput , getValueFromInput , cloneHTMLElement , htmlToElement , getElementAsTagText } from './dom_utils' ;
8+ import {
9+ elementBelongsToThisController ,
10+ getModelDirectiveFromElement ,
11+ getValueFromElement ,
12+ cloneHTMLElement ,
13+ htmlToElement ,
14+ getElementAsTagText ,
15+ setValueOnElement
16+ } from './dom_utils' ;
917import UnsyncedInputContainer from './UnsyncedInputContainer' ;
1018
1119interface ElementLoadingDirectives {
@@ -88,6 +96,7 @@ export default class extends Controller implements LiveController {
8896 this . originalDataJSON = this . valueStore . asJson ( ) ;
8997 this . unsyncedInputs = new UnsyncedInputContainer ( ) ;
9098 this . _exposeOriginalData ( ) ;
99+ this . synchronizeValueOfModelFields ( ) ;
91100 }
92101
93102 connect ( ) {
@@ -198,7 +207,7 @@ export default class extends Controller implements LiveController {
198207 // if so, to be safe, slightly delay the action so that the
199208 // change/input listener on LiveController can process the
200209 // model change *before* sending the action
201- if ( getModelDirectiveFromInput ( event . currentTarget , false ) ) {
210+ if ( getModelDirectiveFromElement ( event . currentTarget , false ) ) {
202211 this . pendingActionTriggerModelElement = event . currentTarget ;
203212 this . #clearRequestDebounceTimeout( ) ;
204213 window . setTimeout ( ( ) => {
@@ -234,7 +243,7 @@ export default class extends Controller implements LiveController {
234243 throw new Error ( 'Could not update model for non HTMLElement' ) ;
235244 }
236245
237- const modelDirective = getModelDirectiveFromInput ( element , false ) ;
246+ const modelDirective = getModelDirectiveFromElement ( element , false ) ;
238247 if ( eventName === 'input' ) {
239248 const modelName = modelDirective ? modelDirective . action : null ;
240249 // track any inputs that are "unsynced"
@@ -300,7 +309,7 @@ export default class extends Controller implements LiveController {
300309 }
301310 }
302311
303- const finalValue = getValueFromInput ( element , this . valueStore ) ;
312+ const finalValue = getValueFromElement ( element , this . valueStore ) ;
304313
305314 this . $updateModel (
306315 modelDirective . action ,
@@ -368,6 +377,9 @@ export default class extends Controller implements LiveController {
368377 // the string "4" - back into an array with [id=4, title=new_title].
369378 this . valueStore . set ( modelName , value ) ;
370379
380+ // the model's data is no longer unsynced
381+ this . unsyncedInputs . markModelAsSynced ( modelName ) ;
382+
371383 // skip rendering if there is an action Ajax call processing
372384 if ( shouldRender ) {
373385 let debounce : number = this . getDefaultDebounce ( ) ;
@@ -376,6 +388,9 @@ export default class extends Controller implements LiveController {
376388 }
377389
378390 this . #clearRequestDebounceTimeout( ) ;
391+ // debouncing even with a 0 value is enough to allow any other potential
392+ // events happening right now (e.g. from custom user JavaScript) to
393+ // finish setting other models before making the request.
379394 this . requestDebounceTimeout = window . setTimeout ( ( ) => {
380395 this . requestDebounceTimeout = null ;
381396 this . isRerenderRequested = true ;
@@ -405,15 +420,6 @@ export default class extends Controller implements LiveController {
405420 // we're making a request NOW, so no need to make another one after debouncing
406421 this . #clearRequestDebounceTimeout( ) ;
407422
408- // check if any unsynced inputs are now "in sync": their value matches what's in the store
409- // if they ARE, then they are on longer "unsynced", which means that any
410- // potential new values from the server *should* now be respected and used
411- this . unsyncedInputs . allMappedFields ( ) . forEach ( ( element , modelName ) => {
412- if ( getValueFromInput ( element , this . valueStore ) === this . valueStore . get ( modelName ) ) {
413- this . unsyncedInputs . remove ( modelName ) ;
414- }
415- } ) ;
416-
417423 const fetchOptions : RequestInit = { } ;
418424 fetchOptions . headers = {
419425 'Accept' : 'application/vnd.live-component+html' ,
@@ -506,16 +512,14 @@ export default class extends Controller implements LiveController {
506512 }
507513
508514 /**
509- * If this re-render contains "mapped" fields that were updated after
510- * the Ajax call started, then we need those "unsynced" values to
511- * take precedence over the (out-of-date) values returned by the server .
515+ * For any models modified since the last request started, grab
516+ * their value now: we will re-set them after the new data from
517+ * the server has been processed .
512518 */
513519 const modifiedModelValues : any = { } ;
514- if ( this . unsyncedInputs . allMappedFields ( ) . size > 0 ) {
515- for ( const [ modelName ] of this . unsyncedInputs . allMappedFields ( ) ) {
516- modifiedModelValues [ modelName ] = this . valueStore . get ( modelName ) ;
517- }
518- }
520+ this . valueStore . updatedModels . forEach ( ( modelName ) => {
521+ modifiedModelValues [ modelName ] = this . valueStore . get ( modelName ) ;
522+ } ) ;
519523
520524 // merge/patch in the new HTML
521525 this . _executeMorphdom ( html , this . unsyncedInputs . all ( ) ) ;
@@ -524,6 +528,8 @@ export default class extends Controller implements LiveController {
524528 Object . keys ( modifiedModelValues ) . forEach ( ( modelName ) => {
525529 this . valueStore . set ( modelName , modifiedModelValues [ modelName ] ) ;
526530 } ) ;
531+
532+ this . synchronizeValueOfModelFields ( ) ;
527533 }
528534
529535 _onLoadingStart ( ) {
@@ -694,9 +700,10 @@ export default class extends Controller implements LiveController {
694700 return false ;
695701 }
696702
697- // if this field has been modified since this HTML was requested, do not update it
703+ // if this field's value has been modified since this HTML was
704+ // requested, set the toEl's value to match the fromEl
698705 if ( modifiedElements . includes ( fromEl ) ) {
699- return false ;
706+ setValueOnElement ( toEl , getValueFromElement ( fromEl , this . valueStore ) )
700707 }
701708
702709 // https://github.com/patrick-steele-idem/morphdom#can-i-make-morphdom-blaze-through-the-dom-tree-even-faster-yes
@@ -1080,6 +1087,51 @@ export default class extends Controller implements LiveController {
10801087 this . requestDebounceTimeout = null ;
10811088 }
10821089 }
1090+
1091+ /**
1092+ * Sets the "value" of all model fields to the component data.
1093+ *
1094+ * This is called when the component initializes and after re-render.
1095+ * Take the following element:
1096+ *
1097+ * <input data-model="firstName">
1098+ *
1099+ * This method will set the "value" of that element to the value of
1100+ * the "firstName" model.
1101+ */
1102+ private synchronizeValueOfModelFields ( ) : void {
1103+ this . element . querySelectorAll ( '[data-model]' ) . forEach ( ( element ) => {
1104+ if ( ! ( element instanceof HTMLElement ) ) {
1105+ throw new Error ( 'Invalid element using data-model.' ) ;
1106+ }
1107+
1108+ if ( element instanceof HTMLFormElement ) {
1109+ return ;
1110+ }
1111+
1112+ const modelDirective = getModelDirectiveFromElement ( element ) ;
1113+ if ( ! modelDirective ) {
1114+ return ;
1115+ }
1116+
1117+ const modelName = modelDirective . action ;
1118+
1119+ // skip any elements whose model name is currently in an unsynced state
1120+ if ( this . unsyncedInputs . getModifiedModels ( ) . includes ( modelName ) ) {
1121+ return ;
1122+ }
1123+
1124+ if ( this . valueStore . has ( modelName ) ) {
1125+ setValueOnElement ( element , this . valueStore . get ( modelName ) )
1126+ }
1127+
1128+ // for select elements without a blank value, one might be selected automatically
1129+ // https://github.com/symfony/ux/issues/469
1130+ if ( element instanceof HTMLSelectElement && ! element . multiple ) {
1131+ this . valueStore . set ( modelName , getValueFromElement ( element , this . valueStore ) ) ;
1132+ }
1133+ } )
1134+ }
10831135}
10841136
10851137class BackendRequest {
0 commit comments