@ditojs/admin 3.2.3 → 3.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "3.2.3",
3
+ "version": "3.2.5",
4
4
  "type": "module",
5
5
  "description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/main/packages/admin",
@@ -131,5 +131,5 @@
131
131
  "vue-router": "^5.3.1",
132
132
  "vue-upload-component": "^3.2.2"
133
133
  },
134
- "gitHead": "5fdd07d3192698bfab4a3bedc061a08341c67282"
134
+ "gitHead": "f149113bc95317c74b17f4af56ccfebed789c47b"
135
135
  }
@@ -149,17 +149,16 @@ export default class DitoContext {
149
149
  }
150
150
 
151
151
  get value() {
152
- return get(
153
- this,
154
- 'value',
155
- () =>
156
- // If the value is not defined on the underlying object, it's not a type
157
- // component, or its value is missing. If it is nested, we can still get
158
- // the value from the root, or `undefined` if it's missing there too.
159
- this.nested
160
- ? getValueAtDataPath(this.rootItem, this.dataPath, () => undefined)
161
- : undefined
162
- )
152
+ const object = toObject(this)
153
+ // Type components hold their value, also while it's `undefined`, e.g. a
154
+ // computed value that the data model hasn't written yet. Other components
155
+ // don't hold a value, but if they're nested, we can get it from the root,
156
+ // or `undefined` if it's missing there.
157
+ return 'value' in object
158
+ ? object.value
159
+ : this.nested
160
+ ? getValueAtDataPath(this.rootItem, this.dataPath, () => undefined)
161
+ : undefined
163
162
  }
164
163
 
165
164
  get dataPath() {
@@ -32,6 +32,7 @@
32
32
  </template>
33
33
 
34
34
  <script>
35
+ import { markRaw } from 'vue'
35
36
  import { clone } from '@ditojs/utils'
36
37
  import DitoComponent from '../DitoComponent.js'
37
38
  import DomMixin from '../mixins/DomMixin.js'
@@ -47,8 +48,7 @@ export default DitoComponent.component('DitoDialog', {
47
48
 
48
49
  provide() {
49
50
  return {
50
- $dialogComponent: () => this,
51
- $dataModel: () => this.ownDataModel
51
+ $dialogComponent: () => this
52
52
  }
53
53
  },
54
54
 
@@ -80,11 +80,6 @@ export default DitoComponent.component('DitoDialog', {
80
80
  },
81
81
 
82
82
  computed: {
83
- // @override DitoMixin.dataModel
84
- dataModel() {
85
- return this.ownDataModel
86
- },
87
-
88
83
  dialogComponent() {
89
84
  return this
90
85
  },
@@ -136,11 +131,13 @@ export default DitoComponent.component('DitoDialog', {
136
131
  created() {
137
132
  // Writes defaults and computed values into the dialog's data and resolves
138
133
  // options, see `DataModel`:
139
- this.ownDataModel = new DataModel({
140
- component: this,
141
- getSchema: () => this.schema,
142
- getData: () => this.dialogData
143
- })
134
+ this.ownDataModel = markRaw(
135
+ new DataModel({
136
+ component: this,
137
+ getSchema: () => this.schema,
138
+ getData: () => this.dialogData
139
+ })
140
+ )
144
141
  },
145
142
 
146
143
  mounted() {
@@ -185,7 +182,7 @@ export default DitoComponent.component('DitoDialog', {
185
182
 
186
183
  async submit() {
187
184
  // Wait for computed values that depend on pending loads, e.g. options.
188
- await this.waitForPendingLoadsOf(this.dataModel)
185
+ await this.waitUntilDataModelSettled()
189
186
  this.resolve(this.dialogData)
190
187
  },
191
188
 
@@ -51,6 +51,7 @@
51
51
  </template>
52
52
 
53
53
  <script>
54
+ import { markRaw } from 'vue'
54
55
  import { clone, capitalize, parseDataPath, assignDeeply } from '@ditojs/utils'
55
56
  import DitoComponent from '../DitoComponent.js'
56
57
  import RouteMixin from '../mixins/RouteMixin.js'
@@ -66,12 +67,6 @@ import { DataModel } from '../utils/DataModel.js'
66
67
  export default DitoComponent.component('DitoForm', {
67
68
  mixins: [RouteMixin, ResourceMixin],
68
69
 
69
- provide() {
70
- return {
71
- $dataModel: () => this.ownDataModel
72
- }
73
- },
74
-
75
70
  data() {
76
71
  return {
77
72
  createdData: null,
@@ -80,11 +75,6 @@ export default DitoComponent.component('DitoForm', {
80
75
  },
81
76
 
82
77
  computed: {
83
- // @override DitoMixin.dataModel
84
- dataModel() {
85
- return this.ownDataModel
86
- },
87
-
88
78
  isForm() {
89
79
  return true
90
80
  },
@@ -366,12 +356,14 @@ export default DitoComponent.component('DitoForm', {
366
356
  created() {
367
357
  // Writes defaults and computed values into the form's data, resolves
368
358
  // options and tracks whether the data is dirty, see `DataModel`:
369
- this.ownDataModel = new DataModel({
370
- component: this,
371
- getSchema: () => this.schema,
372
- getData: () => this.data,
373
- getSourceSchema: () => this.sourceSchema
374
- })
359
+ this.ownDataModel = markRaw(
360
+ new DataModel({
361
+ component: this,
362
+ getSchema: () => this.schema,
363
+ getData: () => this.data,
364
+ getSourceSchema: () => this.sourceSchema
365
+ })
366
+ )
375
367
  },
376
368
 
377
369
  beforeUnmount() {
@@ -473,7 +465,7 @@ export default DitoComponent.component('DitoForm', {
473
465
 
474
466
  async submit(button, { validate = true, closeForm = false } = {}) {
475
467
  // Wait for computed values that depend on pending loads, e.g. options.
476
- await this.waitForPendingLoadsOf(this.dataModel)
468
+ await this.waitUntilDataModelSettled()
477
469
  if (validate && !this.validateAll()) {
478
470
  return false
479
471
  }
@@ -28,6 +28,7 @@ template(
28
28
  </template>
29
29
 
30
30
  <script>
31
+ import { markRaw } from 'vue'
31
32
  import DitoComponent from '../DitoComponent.js'
32
33
  import RouteMixin from '../mixins/RouteMixin.js'
33
34
  import {
@@ -45,8 +46,7 @@ export default DitoComponent.component('DitoView', {
45
46
  // Redirect $sourceComponent and $resourceComponent to the main component:
46
47
  return {
47
48
  $sourceComponent: () => this.mainComponent?.sourceComponent || null,
48
- $resourceComponent: () => this.mainComponent?.resourceComponent || null,
49
- $dataModel: () => this.ownDataModel
49
+ $resourceComponent: () => this.mainComponent?.resourceComponent || null
50
50
  }
51
51
  },
52
52
 
@@ -61,11 +61,6 @@ export default DitoComponent.component('DitoView', {
61
61
  },
62
62
 
63
63
  computed: {
64
- // @override DitoMixin.dataModel
65
- dataModel() {
66
- return this.ownDataModel
67
- },
68
-
69
64
  schema() {
70
65
  return this.meta.schema ?? {}
71
66
  },
@@ -126,11 +121,13 @@ export default DitoComponent.component('DitoView', {
126
121
  created() {
127
122
  // Writes defaults and computed values into the view's data and resolves
128
123
  // options, see `DataModel`:
129
- this.ownDataModel = new DataModel({
130
- component: this,
131
- getSchema: () => this.viewSchema,
132
- getData: () => this.data
133
- })
124
+ this.ownDataModel = markRaw(
125
+ new DataModel({
126
+ component: this,
127
+ getSchema: () => this.viewSchema,
128
+ getData: () => this.data
129
+ })
130
+ )
134
131
  },
135
132
 
136
133
  mounted() {
@@ -39,29 +39,28 @@ export default {
39
39
 
40
40
  provide() {
41
41
  const self = () => this
42
- return this.providesData
43
- ? {
44
- $parentComponent: self,
45
- $dataComponent: self
46
- }
47
- : {
48
- $parentComponent: self
49
- }
42
+ return {
43
+ $parentComponent: self,
44
+ ...(this.providesData && { $dataComponent: self }),
45
+ $dataModel: () => this.dataModel
46
+ }
50
47
  },
51
48
 
52
49
  data() {
53
50
  return {
54
51
  appState,
55
52
  isMounted: false,
56
- overrides: null // See accessor.js
53
+ overrides: null, // See accessor.js
54
+ // The data model that the component owns, see `dataModel`.
55
+ ownDataModel: null
57
56
  }
58
57
  },
59
58
 
60
59
  computed: {
61
- // The data model, provided by the form, view or dialog that owns it, which
62
- // override it with their own.
60
+ // The data model of the data: the component's own, e.g. of a form, view
61
+ // or dialog, or else the one of its closest ancestor that owns one.
63
62
  dataModel() {
64
- return this.$dataModel()
63
+ return this.ownDataModel ?? this.$dataModel()
65
64
  },
66
65
 
67
66
  providesData() {
@@ -498,17 +497,6 @@ export default {
498
497
  : parentComponentPath
499
498
  },
500
499
 
501
- // Waits for the pending loads of the data model, e.g. before submitting,
502
- // showing the spinner in the header in the meantime, like requests do.
503
- async waitForPendingLoadsOf(dataModel) {
504
- this.rootComponent.registerLoading(true)
505
- try {
506
- await dataModel.waitForPendingLoads()
507
- } finally {
508
- this.rootComponent.registerLoading(false)
509
- }
510
- },
511
-
512
500
  notify(options) {
513
501
  this.rootComponent.notify(options)
514
502
  },
@@ -627,16 +615,25 @@ export default {
627
615
  // data changes register, e.g. for `processedItem`, and the form
628
616
  // model writes the values that it derives from the data.
629
617
  // NOTE: The result of `handleListeners()` is returned as expected.
630
- this.waitForDataModel().then(handleListeners)
618
+ this.waitUntilDataModelSettled().then(handleListeners)
631
619
  : handleListeners()
632
620
  }
633
621
  },
634
622
 
635
623
  // Waits until the data model settled: its loads finished, and it wrote the
636
- // values that it derives from the data.
637
- async waitForDataModel() {
624
+ // values that it derives from the data, e.g. before submitting. Shows the
625
+ // spinner in the header while loads are pending, like requests do.
626
+ async waitUntilDataModelSettled() {
638
627
  await this.$nextTick()
639
- await this.dataModel?.waitForPendingLoads()
628
+ const { dataModel } = this
629
+ if (dataModel?.hasPendingLoads) {
630
+ this.rootComponent.registerLoading(true)
631
+ try {
632
+ await dataModel.waitUntilSettled()
633
+ } finally {
634
+ this.rootComponent.registerLoading(false)
635
+ }
636
+ }
640
637
  },
641
638
 
642
639
  emitSchemaEvent(event, params) {
@@ -68,10 +68,12 @@ export default {
68
68
  // Whether the value needs to be replaced with the selected value, once the
69
69
  // options are available: if the value is forced to `null` because its
70
70
  // option disappeared, or if the value is a reference, so that it'll hold
71
- // actual data, not just a reference id. See `watch`.
71
+ // actual data, not just a reference id. See `watch`. Filtered options, e.g.
72
+ // the results of a search, don't tell whether the value's option exists.
72
73
  shouldReplaceValueWithSelectedValue() {
73
74
  return (
74
- this.hasOptions && (
75
+ this.hasOptions &&
76
+ !this.areOptionsFiltered && (
75
77
  this.selectedValue === null && this.value !== null ||
76
78
  isReference(this.value)
77
79
  )
@@ -114,6 +116,12 @@ export default {
114
116
  return this.options
115
117
  },
116
118
 
119
+ // Whether the active options are a filtered subset of the options, e.g.
120
+ // the results of a search, see `activeOptions`.
121
+ areOptionsFiltered() {
122
+ return this.activeOptions !== this.options
123
+ },
124
+
117
125
  hasOptions() {
118
126
  return this.activeOptions.length > 0
119
127
  },
@@ -304,8 +312,16 @@ export default {
304
312
  }
305
313
  }
306
314
 
307
- return this.optionValue
308
- ? findOption(this.activeOptions, value, this.groupBy)
315
+ const { optionValue, groupBy } = this
316
+ return optionValue
317
+ ? (findOption(this.activeOptions, value, groupBy) ??
318
+ // Options filtered out of the active options, e.g. by a search, are
319
+ // still options of the value.
320
+ (
321
+ this.areOptionsFiltered
322
+ ? findOption(this.options, value, groupBy)
323
+ : undefined
324
+ ))
309
325
  : value
310
326
  },
311
327
 
@@ -9,12 +9,13 @@ import {
9
9
  } from '@ditojs/utils'
10
10
  import DitoContext from '../DitoContext.js'
11
11
  import { DataSchemaResolver } from './DataSchemaResolver.js'
12
+ import { getParentDataPath } from './data.js'
12
13
  import { isNested } from './schema/structure.js'
13
14
  import { isEmptySchema } from './schema/lookup.js'
14
15
  import {
15
16
  getSchemaValue,
16
17
  shouldSetDefaultValue,
17
- getDefaultValue,
18
+ setDefaultValue,
18
19
  initializeData,
19
20
  computeValue,
20
21
  hasValueFromDataSchema,
@@ -41,7 +42,7 @@ import {
41
42
  // displays them, see `getOptions()`. Both get the same option objects, and
42
43
  // computes that read options only run once they are loaded.
43
44
  // - Submitting waits for the loads of data schemas and options that are still
44
- // pending, see `waitForPendingLoads()`.
45
+ // pending, see `waitUntilSettled()`.
45
46
  // - The data is dirty when its processed data differs from a snapshot taken
46
47
  // when the data was set up, saved or applied, see `isDirty`. The values that
47
48
  // the model derives until it settled, e.g. from loaded options, don't count,
@@ -68,7 +69,7 @@ export class DataModel {
68
69
  // of their options, by component path, see `getOptionsResolver()`:
69
70
  optionsRecords = new Map()
70
71
  // The promises of the loads of data schemas and options that are pending,
71
- // see `waitForPendingLoads()`:
72
+ // see `waitUntilSettled()`:
72
73
  pendingLoads = new Set()
73
74
  // The data that the snapshot was taken of, and its processed data to compare
74
75
  // with in `isDirty`, see `takeProcessedDataSnapshot()`:
@@ -77,6 +78,9 @@ export class DataModel {
77
78
  // the snapshot is taken, which `isDirty` takes from the current data, `null`
78
79
  // once it settled, see `takeProcessedDataSnapshot()`:
79
80
  derivedValueDataPaths = null
81
+ // The number of writes of computed values per data path in the current
82
+ // microtask, see `isWriteLoop()`:
83
+ computedValueWriteCounts = null
80
84
 
81
85
  // `getSchema()` and `getData()` return the schema and the data that the
82
86
  // model handles. `getSourceSchema()` returns the schema of the source that
@@ -92,7 +96,7 @@ export class DataModel {
92
96
  watch(
93
97
  [getSchema, getData],
94
98
  ([schema, data]) => this.initializeData(schema, data),
95
- modelWatchOptions
99
+ modelSetupWatchOptions
96
100
  )
97
101
  // The entries are read through a computed property, so that the watchers
98
102
  // of the entries can check synchronously whether their entry is still
@@ -101,19 +105,18 @@ export class DataModel {
101
105
  watch(
102
106
  () => this.dataEntries.value.computedValueEntries,
103
107
  entries => this.updateComputedValueRecords(entries),
104
- modelWatchOptions
108
+ modelSetupWatchOptions
105
109
  )
106
110
  // Values that go missing after the data was set up, e.g. in items that
107
111
  // code adds, get their defaults too:
108
112
  watch(
109
113
  () => this.dataEntries.value.entriesWithMissingValues,
110
114
  entries => this.setDefaultValues(entries),
111
- modelWatchOptions
115
+ modelSetupWatchOptions
112
116
  )
113
117
  if (getSourceSchema) {
114
118
  // Data that is set up, e.g. loaded, saved or applied, isn't dirty:
115
- // After the data is set up, as the watchers run in the order in which
116
- // they're created, see `modelWatchOptions`.
119
+ // After the data is set up, see `modelSetupWatchOptions`.
117
120
  watch(
118
121
  getData,
119
122
  () => this.takeProcessedDataSnapshot().catch(console.error),
@@ -167,12 +170,18 @@ export class DataModel {
167
170
  this.pendingLoads.clear()
168
171
  }
169
172
 
173
+ // Whether loads of data schemas or options are pending, see
174
+ // `waitUntilSettled()`.
175
+ get hasPendingLoads() {
176
+ return this.pendingLoads.size > 0
177
+ }
178
+
170
179
  // Waits for the pending loads of data schemas and options, including the
171
180
  // ones that they cause, e.g. options that depend on computed values that
172
181
  // depend on loaded options, and for the watchers to write the resulting
173
182
  // computed values into the data.
174
- async waitForPendingLoads() {
175
- while (this.pendingLoads.size > 0) {
183
+ async waitUntilSettled() {
184
+ while (this.hasPendingLoads) {
176
185
  await Promise.all(this.pendingLoads)
177
186
  await nextTick()
178
187
  }
@@ -181,7 +190,7 @@ export class DataModel {
181
190
  // Takes the snapshot of the processed data that `isDirty` compares with,
182
191
  // when the data is set up, saved or applied. Until the model settled, i.e.
183
192
  // the pending loads finished and the computed values that depend on them are
184
- // written into the data, see `waitForPendingLoads()`, the values that the
193
+ // written into the data, see `waitUntilSettled()`, the values that the
185
194
  // model writes are recorded in `derivedValueDataPaths` and don't count, as
186
195
  // they're derived from the data, e.g. from loaded options. Once settled, they
187
196
  // are taken over into the snapshot, so that changes made in the meantime,
@@ -201,7 +210,7 @@ export class DataModel {
201
210
  }
202
211
  // Let the watchers and the rendering start their loads first:
203
212
  await nextTick()
204
- await this.waitForPendingLoads()
213
+ await this.waitUntilSettled()
205
214
  // Snapshots that were taken since settle by themselves.
206
215
  const isSnapshotReplaced = (
207
216
  this.derivedValueDataPaths !==
@@ -266,7 +275,7 @@ export class DataModel {
266
275
  if (hasSnapshotOfData) {
267
276
  // Let the watchers write the values derived from the changes first:
268
277
  await nextTick()
269
- await this.waitForPendingLoads()
278
+ await this.waitUntilSettled()
270
279
  // Take the changes over into the current snapshot of the data, which may
271
280
  // have been replaced in the meantime, e.g. once the model settled.
272
281
  const processedDataSnapshot = this.processedDataSnapshot.value
@@ -284,7 +293,7 @@ export class DataModel {
284
293
  }
285
294
 
286
295
  // Tracks the promise of a pending load until it settles, so that
287
- // `waitForPendingLoads()` waits for it.
296
+ // `waitUntilSettled()` waits for it.
288
297
  trackPendingLoad(promise) {
289
298
  this.pendingLoads.add(promise)
290
299
  promise.finally(() => this.pendingLoads.delete(promise))
@@ -340,16 +349,23 @@ export class DataModel {
340
349
  // `false`. Unlike `shouldRenderSchema()`, it doesn't evaluate the components
341
350
  // of sections and tabs, as the walk of the data visits them anyway, see
342
351
  // `getDataEntries()`, which leads to the same entries.
352
+ // An `if` that fails only affects its own component, which is treated as
353
+ // shown, not the walk of all others, which would stop computing them.
343
354
  isEntryShown(entry) {
344
- return (
345
- entry.schema.if === undefined ||
346
- getSchemaValue('if', {
355
+ if (entry.schema.if === undefined) {
356
+ return true
357
+ }
358
+ try {
359
+ return getSchemaValue('if', {
347
360
  type: Boolean,
348
361
  schema: entry.schema,
349
362
  context: this.createEntryContext(entry),
350
363
  default: true
351
364
  })
352
- )
365
+ } catch (error) {
366
+ console.error(error)
367
+ return true
368
+ }
353
369
  }
354
370
 
355
371
  // Returns whether the value of the entry is missing and needs its default.
@@ -372,7 +388,7 @@ export class DataModel {
372
388
  for (const entry of entries) {
373
389
  if (this.shouldSetDefaultValue(entry)) {
374
390
  const { schema, data, name } = entry
375
- data[name] = getDefaultValue(schema, () =>
391
+ setDefaultValue(schema, data, name, () =>
376
392
  this.createEntryContext(entry)
377
393
  )
378
394
  }
@@ -533,18 +549,82 @@ export class DataModel {
533
549
  })
534
550
  }
535
551
 
552
+ // Writes the computed value into the data, with the defaults of its nested
553
+ // values, unless it equals the value in the data.
536
554
  writeComputedValue(entry, value) {
537
555
  const { data, name } = entry
556
+ this.setNestedDefaultValues(entry, value)
538
557
  if (!equals(value, data[name])) {
558
+ const dataPath = this.getRelativeDataPath(entry)
559
+ if (this.isWriteLoop(dataPath)) {
560
+ return
561
+ }
539
562
  // Values written while the model settles are derived, see
540
563
  // `takeProcessedDataSnapshot()`:
541
- this.derivedValueDataPaths?.add(this.getRelativeDataPath(entry))
564
+ this.derivedValueDataPaths?.add(dataPath)
542
565
  // Access `data[name]` directly to update the value without calling
543
566
  // `parse()`, see `ValueMixin`:
544
567
  data[name] = value
545
568
  }
546
569
  }
547
570
 
571
+ // Sets the missing defaults of the values nested in the computed value of
572
+ // the entry, e.g. of the items that `compute()` returns, as
573
+ // `setDefaultValues()` would once the value is written. Otherwise, the two
574
+ // would disagree on the value and keep replacing each other's writes.
575
+ setNestedDefaultValues(entry, value) {
576
+ const { schema, data, name, dataPath } = entry
577
+ if (!isPlainObject(value) && !isArray(value)) {
578
+ return
579
+ }
580
+ // Walk the computed value in place of the current one, in a copy of the
581
+ // data that holds it.
582
+ const valueData = { ...data, [name]: value }
583
+ processSchemaData({ components: { [name]: schema } }, valueData, {
584
+ dataPath: isNested(schema) ? getParentDataPath(dataPath) : dataPath,
585
+ shouldProcess: nestedEntry => this.isEntryShown(nestedEntry),
586
+ shouldSkipSourcesWithResource: true,
587
+ before: nestedEntry => {
588
+ if (
589
+ nestedEntry.data !== valueData &&
590
+ this.shouldSetDefaultValue(nestedEntry)
591
+ ) {
592
+ setDefaultValue(
593
+ nestedEntry.schema,
594
+ nestedEntry.data,
595
+ nestedEntry.name,
596
+ () => this.createEntryContext(nestedEntry)
597
+ )
598
+ }
599
+ },
600
+ options: { component: this.component, rootData: this.rootData }
601
+ })
602
+ }
603
+
604
+ // Returns whether the computed value at the data path was written too often
605
+ // in the current microtask, which happens when the sources of the value keep
606
+ // disagreeing on it, e.g. two computes. Reports the data path once, instead
607
+ // of letting the writes loop endlessly.
608
+ isWriteLoop(dataPath) {
609
+ if (!this.computedValueWriteCounts) {
610
+ this.computedValueWriteCounts = new Map()
611
+ queueMicrotask(() => {
612
+ this.computedValueWriteCounts = null
613
+ })
614
+ }
615
+ const count = (this.computedValueWriteCounts.get(dataPath) ?? 0) + 1
616
+ this.computedValueWriteCounts.set(dataPath, count)
617
+ if (count === maxComputedValueWrites + 1) {
618
+ console.error(
619
+ new Error(
620
+ `The computed value at '${dataPath}' keeps changing, as its ` +
621
+ `sources disagree on it. It isn't written anymore in this update.`
622
+ )
623
+ )
624
+ }
625
+ return count > maxComputedValueWrites
626
+ }
627
+
548
628
  // Returns the data path of the value of the entry, relative to the model's
549
629
  // data, as in its processed data.
550
630
  getRelativeDataPath({ schema, name, dataPath }) {
@@ -641,19 +721,30 @@ function takeOverChangedValues(target, before, after) {
641
721
 
642
722
  const notFound = Symbol('notFound')
643
723
 
644
- // The watchers of the model run after the post-flush hooks, so that they write
645
- // the values that they derive from the data after the components that display
646
- // them are mounted. Otherwise, the `mounted` hooks of `v-model` directives
647
- // would restore the values that inputs had when they were mounted, see
648
- // https://github.com/vuejs/core/issues/15774. Vue processes the updates that
649
- // the written values cause in the same flush, before the browser renders them.
650
- // The watchers run in the order in which they're created.
724
+ // The watchers of the model that set up the data and the scopes of its
725
+ // components run before the components render, so that components of new
726
+ // data, e.g. loaded data or added items, receive their defaults and computed
727
+ // values from the start: The scopes compute their values right away, see
728
+ // `createComputedValueScope()`.
729
+ const modelSetupWatchOptions = { immediate: true, flush: 'pre' }
730
+
731
+ // The other watchers of the model run after the post-flush hooks, so that they
732
+ // write the values that they derive from the data after the components that
733
+ // display them are mounted. Otherwise, the `mounted` hooks of `v-model`
734
+ // directives would restore the values that inputs had when they were mounted,
735
+ // see https://github.com/vuejs/core/issues/15774. Vue processes the updates
736
+ // that the written values cause in the same flush, before the browser renders
737
+ // them.
651
738
  const modelWatchOptions = { immediate: true, flush: 'post' }
652
739
 
653
740
  // Aborts `compute()` when it reads options that aren't loaded, see
654
741
  // `DataModel.getComputedValueResult()`.
655
742
  const optionsNotLoaded = Symbol('optionsNotLoaded')
656
743
 
744
+ // The number of writes of a computed value in one microtask after which they
745
+ // are considered a loop, see `DataModel.isWriteLoop()`.
746
+ const maxComputedValueWrites = 10
747
+
657
748
  // Returns whether the schema is a source of computed values, through
658
749
  // `schema.compute()` or a data schema of the `computed` types.
659
750
  function hasComputedValueSource(schema) {
@@ -110,6 +110,17 @@ export function getDefaultValue(schema, context) {
110
110
  : clone(defaultValue)
111
111
  }
112
112
 
113
+ // Sets the value of the component described by `schema` and `name` in `data`
114
+ // to its default. Types without values, e.g. buttons, have no default, and
115
+ // don't get a key in the data, which would make the data differ from data
116
+ // without it, e.g. from items that `compute()` returns.
117
+ export function setDefaultValue(schema, data, name, context) {
118
+ const value = getDefaultValue(schema, context)
119
+ if (value !== undefined) {
120
+ data[name] = value
121
+ }
122
+ }
123
+
113
124
  export function shouldExcludeValue(schema, context) {
114
125
  const excludeValue =
115
126
  schema.exclude !== undefined
@@ -207,7 +218,7 @@ export function initializeData(schema, data = {}, component, {
207
218
  { shouldSetDefaultsOfComponentsWithCompute }
208
219
  )
209
220
  if (shouldSetDefault) {
210
- data[name] = getDefaultValue(schema, context)
221
+ setDefaultValue(schema, data, name, context)
211
222
  }
212
223
  if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
213
224
  updateOrder(schema, data[name])