@@ -5,7 +5,15 @@ import { combineSpacedArray, normalizeModelName } from './string_utils';
5
5
import { haveRenderedValuesChanged } from './have_rendered_values_changed' ;
6
6
import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison' ;
7
7
import 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' ;
9
17
import UnsyncedInputContainer from './UnsyncedInputContainer' ;
10
18
11
19
interface ElementLoadingDirectives {
@@ -88,6 +96,7 @@ export default class extends Controller implements LiveController {
88
96
this . originalDataJSON = this . valueStore . asJson ( ) ;
89
97
this . unsyncedInputs = new UnsyncedInputContainer ( ) ;
90
98
this . _exposeOriginalData ( ) ;
99
+ this . synchronizeValueOfModelFields ( ) ;
91
100
}
92
101
93
102
connect ( ) {
@@ -198,7 +207,7 @@ export default class extends Controller implements LiveController {
198
207
// if so, to be safe, slightly delay the action so that the
199
208
// change/input listener on LiveController can process the
200
209
// model change *before* sending the action
201
- if ( getModelDirectiveFromInput ( event . currentTarget , false ) ) {
210
+ if ( getModelDirectiveFromElement ( event . currentTarget , false ) ) {
202
211
this . pendingActionTriggerModelElement = event . currentTarget ;
203
212
this . #clearRequestDebounceTimeout( ) ;
204
213
window . setTimeout ( ( ) => {
@@ -234,7 +243,7 @@ export default class extends Controller implements LiveController {
234
243
throw new Error ( 'Could not update model for non HTMLElement' ) ;
235
244
}
236
245
237
- const modelDirective = getModelDirectiveFromInput ( element , false ) ;
246
+ const modelDirective = getModelDirectiveFromElement ( element , false ) ;
238
247
if ( eventName === 'input' ) {
239
248
const modelName = modelDirective ? modelDirective . action : null ;
240
249
// track any inputs that are "unsynced"
@@ -300,7 +309,7 @@ export default class extends Controller implements LiveController {
300
309
}
301
310
}
302
311
303
- const finalValue = getValueFromInput ( element , this . valueStore ) ;
312
+ const finalValue = getValueFromElement ( element , this . valueStore ) ;
304
313
305
314
this . $updateModel (
306
315
modelDirective . action ,
@@ -368,6 +377,9 @@ export default class extends Controller implements LiveController {
368
377
// the string "4" - back into an array with [id=4, title=new_title].
369
378
this . valueStore . set ( modelName , value ) ;
370
379
380
+ // the model's data is no longer unsynced
381
+ this . unsyncedInputs . markModelAsSynced ( modelName ) ;
382
+
371
383
// skip rendering if there is an action Ajax call processing
372
384
if ( shouldRender ) {
373
385
let debounce : number = this . getDefaultDebounce ( ) ;
@@ -376,6 +388,9 @@ export default class extends Controller implements LiveController {
376
388
}
377
389
378
390
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.
379
394
this . requestDebounceTimeout = window . setTimeout ( ( ) => {
380
395
this . requestDebounceTimeout = null ;
381
396
this . isRerenderRequested = true ;
@@ -405,15 +420,6 @@ export default class extends Controller implements LiveController {
405
420
// we're making a request NOW, so no need to make another one after debouncing
406
421
this . #clearRequestDebounceTimeout( ) ;
407
422
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
-
417
423
const fetchOptions : RequestInit = { } ;
418
424
fetchOptions . headers = {
419
425
'Accept' : 'application/vnd.live-component+html' ,
@@ -506,16 +512,14 @@ export default class extends Controller implements LiveController {
506
512
}
507
513
508
514
/**
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 .
512
518
*/
513
519
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
+ } ) ;
519
523
520
524
// merge/patch in the new HTML
521
525
this . _executeMorphdom ( html , this . unsyncedInputs . all ( ) ) ;
@@ -524,6 +528,8 @@ export default class extends Controller implements LiveController {
524
528
Object . keys ( modifiedModelValues ) . forEach ( ( modelName ) => {
525
529
this . valueStore . set ( modelName , modifiedModelValues [ modelName ] ) ;
526
530
} ) ;
531
+
532
+ this . synchronizeValueOfModelFields ( ) ;
527
533
}
528
534
529
535
_onLoadingStart ( ) {
@@ -694,9 +700,10 @@ export default class extends Controller implements LiveController {
694
700
return false ;
695
701
}
696
702
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
698
705
if ( modifiedElements . includes ( fromEl ) ) {
699
- return false ;
706
+ setValueOnElement ( toEl , getValueFromElement ( fromEl , this . valueStore ) )
700
707
}
701
708
702
709
// 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 {
1080
1087
this . requestDebounceTimeout = null ;
1081
1088
}
1082
1089
}
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
+ }
1083
1135
}
1084
1136
1085
1137
class BackendRequest {
0 commit comments