@ditojs/admin 2.100.0 → 3.0.0

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.
Files changed (84) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3421 -2662
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +81 -45
  5. package/src/DitoComponent.js +1 -1
  6. package/src/DitoContext.js +51 -0
  7. package/src/DitoTypeComponent.js +6 -1
  8. package/src/components/DitoAffixes.vue +6 -3
  9. package/src/components/DitoContainer.vue +19 -10
  10. package/src/components/DitoCreateButton.vue +11 -2
  11. package/src/components/DitoDialog.vue +21 -2
  12. package/src/components/DitoForm.vue +156 -83
  13. package/src/components/DitoLabel.vue +3 -2
  14. package/src/components/DitoPane.vue +55 -11
  15. package/src/components/DitoPanel.vue +33 -5
  16. package/src/components/DitoPanels.vue +2 -1
  17. package/src/components/DitoRoot.vue +13 -6
  18. package/src/components/DitoSchema.vue +278 -145
  19. package/src/components/DitoSchemaInlined.vue +1 -1
  20. package/src/components/DitoTabs.vue +16 -18
  21. package/src/components/DitoTrail.vue +5 -4
  22. package/src/components/DitoTreeItem.vue +17 -6
  23. package/src/components/DitoView.vue +25 -2
  24. package/src/mixins/DitoMixin.js +42 -7
  25. package/src/mixins/ItemMixin.js +3 -6
  26. package/src/mixins/NumberMixin.js +47 -31
  27. package/src/mixins/OptionsMixin.js +122 -71
  28. package/src/mixins/ResourceMixin.js +5 -2
  29. package/src/mixins/RouteMixin.js +39 -1
  30. package/src/mixins/SortableMixin.js +0 -13
  31. package/src/mixins/SourceMixin.js +142 -95
  32. package/src/mixins/TextMixin.js +1 -1
  33. package/src/mixins/TypeMixin.js +37 -22
  34. package/src/mixins/ValidationMixin.js +9 -27
  35. package/src/mixins/ValidatorMixin.js +5 -7
  36. package/src/mixins/ValueMixin.js +5 -2
  37. package/src/types/DitoTypeButton.vue +1 -1
  38. package/src/types/DitoTypeCheckbox.vue +1 -1
  39. package/src/types/DitoTypeCheckboxes.vue +1 -1
  40. package/src/types/DitoTypeCode.vue +8 -1
  41. package/src/types/DitoTypeColor.vue +20 -34
  42. package/src/types/DitoTypeComponent.vue +1 -1
  43. package/src/types/DitoTypeComputed.vue +4 -27
  44. package/src/types/DitoTypeDate.vue +16 -5
  45. package/src/types/DitoTypeLabel.vue +1 -4
  46. package/src/types/DitoTypeList.vue +9 -5
  47. package/src/types/DitoTypeMarkup.vue +25 -13
  48. package/src/types/DitoTypeMultiselect.vue +26 -6
  49. package/src/types/DitoTypeNumber.vue +1 -1
  50. package/src/types/DitoTypeObject.vue +10 -7
  51. package/src/types/DitoTypePanel.vue +19 -6
  52. package/src/types/DitoTypeProgress.vue +1 -1
  53. package/src/types/DitoTypeRadio.vue +1 -1
  54. package/src/types/DitoTypeSection.vue +32 -4
  55. package/src/types/DitoTypeSelect.vue +1 -1
  56. package/src/types/DitoTypeSlider.vue +1 -1
  57. package/src/types/DitoTypeSwitch.vue +1 -1
  58. package/src/types/DitoTypeText.vue +13 -15
  59. package/src/types/DitoTypeTextarea.vue +1 -1
  60. package/src/types/DitoTypeTreeList.vue +12 -7
  61. package/src/types/DitoTypeUpload.vue +1 -2
  62. package/src/utils/DataSchemaResolver.js +141 -0
  63. package/src/utils/DataSchemaResolver.test.js +132 -0
  64. package/src/utils/FormModel.js +556 -0
  65. package/src/utils/FormModel.test.js +808 -0
  66. package/src/utils/data.js +20 -0
  67. package/src/utils/data.test.js +28 -0
  68. package/src/utils/filter.js +3 -2
  69. package/src/utils/options.js +3 -0
  70. package/src/utils/schema/data.js +617 -0
  71. package/src/utils/schema/data.test.js +471 -0
  72. package/src/utils/schema/lookup.js +266 -0
  73. package/src/utils/schema/lookup.test.js +61 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/structure.test.js +57 -0
  77. package/src/utils/schema/types.js +34 -0
  78. package/src/utils/schema/validation.js +142 -0
  79. package/src/utils/schema/validation.test.js +395 -0
  80. package/src/utils/units.js +70 -3
  81. package/src/utils/units.test.js +71 -0
  82. package/types/index.d.ts +149 -59
  83. package/src/mixins/DataMixin.js +0 -131
  84. package/src/utils/schema.js +0 -1121
@@ -0,0 +1,556 @@
1
+ import { effectScope, computed, watch, nextTick, shallowRef } from 'vue'
2
+ import {
3
+ isArray,
4
+ isPlainObject,
5
+ equals,
6
+ clone,
7
+ parseDataPath,
8
+ getValueAtDataPath
9
+ } from '@ditojs/utils'
10
+ import DitoContext from '../DitoContext.js'
11
+ import { DataSchemaResolver } from './DataSchemaResolver.js'
12
+ import { isNested, isSourceWithResource } from './schema/structure.js'
13
+ import { isEmptySchema } from './schema/lookup.js'
14
+ import {
15
+ shouldRenderSchema,
16
+ initializeData,
17
+ computeValue,
18
+ hasValueFromDataSchema,
19
+ processSchemaData,
20
+ processData
21
+ } from './schema/data.js'
22
+
23
+ // FormModel holds the state of the data edited by a form, view or dialog that
24
+ // is derived from schema and data, independently of what is rendered:
25
+ //
26
+ // - Missing values are set to their defaults when the data is set up, except
27
+ // the values of components with `compute()`, which fall back to their
28
+ // defaults when `compute()` doesn't return a value, unlike in new data, see
29
+ // `initializeData()`.
30
+ // - The items of lists with `orderKey` are numbered when the data is set up,
31
+ // which doesn't make the data dirty.
32
+ // - Computed values, the results of `schema.compute()` and of the data schemas
33
+ // of the `computed` types (`schema.data`, `schema.dataPath`), are written
34
+ // into the data by watchers that the model owns, one scope per component,
35
+ // for all components whose `if` doesn't evaluate to `false`.
36
+ // - Options, `schema.options`, are resolved per component when they are first
37
+ // read, by `compute()` through `context.options` or by the component that
38
+ // displays them, see `getOptions()`. Both get the same option objects, and
39
+ // computes that read options run again when they are loaded.
40
+ // - Submitting waits for the loads of data schemas and options that are still
41
+ // pending, see `waitForPendingLoads()`.
42
+ // - The data is dirty when its processed data differs from a snapshot taken
43
+ // when the data was set up, saved or applied, see `isDirty`. The values that
44
+ // the model derives until it settled, e.g. from loaded options, don't count,
45
+ // see `takeProcessedDataSnapshot()`. Only forms track this, by passing
46
+ // `getSourceSchema()`, see the constructor.
47
+ //
48
+ // Sources with their own resource are skipped, as their items are edited
49
+ // through their own forms.
50
+ //
51
+ // The model handles components through their entries, the objects that
52
+ // `processSchemaData()` passes to `before()` and `after()`, with the schema,
53
+ // data, name, data path and component path of each component. Components that
54
+ // read their options pass entries of the same shape, see `getOptions()`.
55
+ //
56
+ // `component` is the component that owns the data, e.g. `DitoForm`. Its
57
+ // `dataPath`, `componentPath`, `rootData` and `mainSchemaComponent` are used
58
+ // when present. The model needs to be stopped before the component unmounts.
59
+
60
+ export class FormModel {
61
+ // The entries of the components with computed values and the scopes of the
62
+ // watchers that write these values into the data, by component path:
63
+ computedValueRecords = new Map()
64
+ // The entries of the components whose options were read and the resolvers
65
+ // of their options, by component path, see `getOptionsResolver()`:
66
+ optionsRecords = new Map()
67
+ // The promises of the loads of data schemas and options that are pending,
68
+ // see `waitForPendingLoads()`:
69
+ pendingLoads = new Set()
70
+ // The data that the snapshot was taken of, and its processed data to compare
71
+ // with in `isDirty`, see `takeProcessedDataSnapshot()`:
72
+ processedDataSnapshot = shallowRef(null)
73
+ // The data paths of the values that the model writes while it settles after
74
+ // the snapshot is taken, which `isDirty` takes from the current data, `null`
75
+ // once it settled, see `takeProcessedDataSnapshot()`:
76
+ derivedValueDataPaths = null
77
+
78
+ // `getSchema()` and `getData()` return the schema and the data that the
79
+ // model handles. `getSourceSchema()` returns the schema of the source that
80
+ // the data is an item of, as needed by `processData()`. Only forms pass it,
81
+ // to track whether their data is dirty, see `isDirty`.
82
+ constructor({ component, getSchema, getData, getSourceSchema = null }) {
83
+ this.component = component
84
+ this.getSchema = getSchema
85
+ this.getData = getData
86
+ this.getSourceSchema = getSourceSchema
87
+ this.rootScope = effectScope(true)
88
+ this.rootScope.run(() => {
89
+ watch(
90
+ [getSchema, getData],
91
+ ([schema, data]) => this.initializeData(schema, data),
92
+ { immediate: true }
93
+ )
94
+ // The entries are read through a computed property, so that the watchers
95
+ // of the entries can check synchronously whether their entry is still
96
+ // current, see `createComputedValueScope()`.
97
+ this.computedValueEntries = computed(() => this.getComputedValueEntries())
98
+ watch(
99
+ () => this.computedValueEntries.value,
100
+ entries => this.updateComputedValueRecords(entries),
101
+ { immediate: true }
102
+ )
103
+ if (getSourceSchema) {
104
+ // Data that is set up, e.g. loaded, saved or applied, isn't dirty:
105
+ watch(
106
+ getData,
107
+ () => this.takeProcessedDataSnapshot().catch(console.error),
108
+ { immediate: true }
109
+ )
110
+ }
111
+ })
112
+ }
113
+
114
+ get dataPath() {
115
+ return this.component.dataPath ?? ''
116
+ }
117
+
118
+ get componentPath() {
119
+ return this.component.componentPath ?? ''
120
+ }
121
+
122
+ get rootData() {
123
+ return this.component.rootData ?? this.getData()
124
+ }
125
+
126
+ // Returns whether the processed data differs from the snapshot taken when
127
+ // the data was set up, saved or applied, see `takeProcessedDataSnapshot()`.
128
+ // Excluded values don't count, unless their components have `process()`,
129
+ // see `processData()`, and neither do the values that the model derives
130
+ // until it settled.
131
+ get isDirty() {
132
+ const processedDataSnapshot = this.processedDataSnapshot.value
133
+ const data = this.getData()
134
+ // A snapshot only applies to the data that it was taken of. Data that was
135
+ // replaced since isn't dirty until its own snapshot is taken, and neither
136
+ // is missing data.
137
+ if (!data || processedDataSnapshot?.data !== data) {
138
+ return false
139
+ }
140
+ const processedData = this.getProcessedDataForDirtyCheck()
141
+ const processedDataToCompare = this.derivedValueDataPaths
142
+ ? takeOverValuesAtDataPaths(
143
+ processedDataSnapshot.processedData,
144
+ processedData,
145
+ this.derivedValueDataPaths
146
+ )
147
+ : processedDataSnapshot.processedData
148
+ return !equals(processedData, processedDataToCompare)
149
+ }
150
+
151
+ stop() {
152
+ this.rootScope.stop()
153
+ this.computedValueRecords.clear()
154
+ this.optionsRecords.clear()
155
+ this.pendingLoads.clear()
156
+ }
157
+
158
+ // Waits for the pending loads of data schemas and options, including the
159
+ // ones that they cause, e.g. options that depend on computed values that
160
+ // depend on loaded options, and for the watchers to write the resulting
161
+ // computed values into the data.
162
+ async waitForPendingLoads() {
163
+ while (this.pendingLoads.size > 0) {
164
+ await Promise.all(this.pendingLoads)
165
+ await nextTick()
166
+ }
167
+ }
168
+
169
+ // Takes the snapshot of the processed data that `isDirty` compares with,
170
+ // when the data is set up, saved or applied. Until the model settled, i.e.
171
+ // the pending loads finished and the computed values that depend on them are
172
+ // written into the data, see `waitForPendingLoads()`, the values that the
173
+ // model writes are recorded in `derivedValueDataPaths` and don't count, as
174
+ // they're derived from the data, e.g. from loaded options. Once settled, they
175
+ // are taken over into the snapshot, so that changes made in the meantime,
176
+ // e.g. by the user, count right away. Values that the model derives from
177
+ // these changes while it settles are taken over too, so reverting the
178
+ // changes afterwards leaves the data dirty, which only asks for confirmation
179
+ // unnecessarily.
180
+ async takeProcessedDataSnapshot() {
181
+ const data = this.getData()
182
+ const derivedValueDataPaths = data ? new Set() : null
183
+ this.derivedValueDataPaths = derivedValueDataPaths
184
+ this.processedDataSnapshot.value = data
185
+ ? { data, processedData: clone(this.getProcessedDataForDirtyCheck()) }
186
+ : null
187
+ if (!data) {
188
+ return
189
+ }
190
+ // Let the watchers and the rendering start their loads first:
191
+ await nextTick()
192
+ await this.waitForPendingLoads()
193
+ // Snapshots that were taken since settle by themselves.
194
+ const isSnapshotReplaced = (
195
+ this.derivedValueDataPaths !==
196
+ derivedValueDataPaths
197
+ )
198
+ // Check whether the model stopped first, as its component may be gone.
199
+ const isModelStopped = !this.rootScope.active
200
+ if (isSnapshotReplaced || isModelStopped) {
201
+ return
202
+ }
203
+ this.derivedValueDataPaths = null
204
+ const processedDataSnapshot = this.processedDataSnapshot.value
205
+ if (processedDataSnapshot?.data === this.getData()) {
206
+ this.processedDataSnapshot.value = {
207
+ data,
208
+ processedData: takeOverValuesAtDataPaths(
209
+ processedDataSnapshot.processedData,
210
+ this.getProcessedDataForDirtyCheck(),
211
+ derivedValueDataPaths
212
+ )
213
+ }
214
+ }
215
+ }
216
+
217
+ // Returns the processed data that `isDirty` compares with
218
+ // `processedDataSnapshot`, and that the snapshot is taken of. The data is
219
+ // processed like for the clipboard, not for the server, which gives
220
+ // references to new items random prefixes each time, see
221
+ // `SchemaGraph.getReferencePrefix()`.
222
+ getProcessedDataForDirtyCheck() {
223
+ const schema = this.getSchema()
224
+ const data = this.getData()
225
+ return data && !isEmptySchema(schema)
226
+ ? processData(schema, this.getSourceSchema(), data, this.dataPath, {
227
+ // Like `getComputedValue()`:
228
+ component: this.component.mainSchemaComponent ?? this.component,
229
+ rootData: this.rootData,
230
+ schemaOnly: true,
231
+ target: 'clipboard',
232
+ // Reading the dirty state doesn't call `compute()` and `process()`,
233
+ // see `processData()`:
234
+ shouldCallComputeAndProcess: false
235
+ })
236
+ : null
237
+ }
238
+
239
+ // Makes clean changes to the data, which don't make it dirty, while other
240
+ // changes still do, e.g. to apply what an action on the server already
241
+ // saved: Once the model settled, the values that changed in the processed
242
+ // data, including the values derived from the changes, are taken over into
243
+ // the snapshot. Arrays whose length changes are taken over as a whole, see
244
+ // `takeOverChangedValues()`. `makeChanges()` is called synchronously.
245
+ async applyCleanChanges(makeChanges) {
246
+ const data = this.getData()
247
+ const hasSnapshotOfData = this.processedDataSnapshot.value?.data === data
248
+ // Clone the processed data, as it shares nested values with the data,
249
+ // which `makeChanges()` may change in place.
250
+ const processedDataBeforeChanges = hasSnapshotOfData
251
+ ? clone(this.getProcessedDataForDirtyCheck())
252
+ : null
253
+ makeChanges()
254
+ if (hasSnapshotOfData) {
255
+ // Let the watchers write the values derived from the changes first:
256
+ await nextTick()
257
+ await this.waitForPendingLoads()
258
+ // Take the changes over into the current snapshot of the data, which may
259
+ // have been replaced in the meantime, e.g. once the model settled.
260
+ const processedDataSnapshot = this.processedDataSnapshot.value
261
+ if (processedDataSnapshot?.data === data && this.rootScope.active) {
262
+ this.processedDataSnapshot.value = {
263
+ data,
264
+ processedData: takeOverChangedValues(
265
+ processedDataSnapshot.processedData,
266
+ processedDataBeforeChanges,
267
+ this.getProcessedDataForDirtyCheck()
268
+ )
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ // Tracks the promise of a pending load until it settles, so that
275
+ // `waitForPendingLoads()` waits for it.
276
+ trackPendingLoad(promise) {
277
+ this.pendingLoads.add(promise)
278
+ promise.finally(() => this.pendingLoads.delete(promise))
279
+ }
280
+
281
+ // Sets missing values to their defaults and numbers the items of lists by
282
+ // their order key when the data is set up, see `initializeData()`.
283
+ initializeData(schema, data) {
284
+ if (data && !isEmptySchema(schema)) {
285
+ initializeData(schema, data, this.component, {
286
+ dataPath: this.dataPath,
287
+ rootData: this.rootData,
288
+ shouldProcess: entry => !isSourceWithResource(entry.schema),
289
+ // The model writes the values of components with `compute()`,
290
+ // including their defaults:
291
+ shouldSetDefaultsOfComponentsWithCompute: false
292
+ })
293
+ }
294
+ }
295
+
296
+ // Returns the entries of `processSchemaData()` for all components with
297
+ // computed values whose `if` doesn't evaluate to `false`, by component path.
298
+ // Called by a computed property, so that it runs again when the data
299
+ // structure changes, e.g. when list items are added or removed, or when `if`
300
+ // conditions change.
301
+ getComputedValueEntries() {
302
+ const schema = this.getSchema()
303
+ const data = this.getData()
304
+ const entries = new Map()
305
+ if (data && !isEmptySchema(schema)) {
306
+ processSchemaData(schema, data, {
307
+ dataPath: this.dataPath,
308
+ componentPath: this.componentPath,
309
+ shouldProcess: entry => (
310
+ !isSourceWithResource(entry.schema) &&
311
+ shouldRenderSchema(entry.schema, this.createEntryContext(entry))
312
+ ),
313
+ before: entry => {
314
+ if (hasComputedValueSource(entry.schema)) {
315
+ entries.set(entry.componentPath, entry)
316
+ }
317
+ },
318
+ options: { component: this.component, rootData: this.rootData }
319
+ })
320
+ }
321
+ return entries
322
+ }
323
+
324
+ // Keeps the records of the components that are still present with the same
325
+ // schema and data, creates records with new scopes for new ones, and stops
326
+ // the scopes of the others.
327
+ updateComputedValueRecords(entries) {
328
+ const previousRecords = this.computedValueRecords
329
+ this.computedValueRecords = new Map()
330
+ for (const [componentPath, entry] of entries) {
331
+ let computedValueRecord = previousRecords.get(componentPath)
332
+ if (
333
+ computedValueRecord?.entry.schema === entry.schema &&
334
+ computedValueRecord.entry.data === entry.data
335
+ ) {
336
+ previousRecords.delete(componentPath)
337
+ } else {
338
+ computedValueRecord = {
339
+ entry,
340
+ scope: this.createComputedValueScope(entry)
341
+ }
342
+ }
343
+ this.computedValueRecords.set(componentPath, computedValueRecord)
344
+ }
345
+ for (const { scope } of previousRecords.values()) {
346
+ scope.stop()
347
+ }
348
+ }
349
+
350
+ // Creates the scope of the watchers that write the computed values of the
351
+ // entry into the data. Entries stop being current before their scope is
352
+ // stopped, as watchers that don't belong to components run in the order in
353
+ // which they're triggered, e.g. when list items are removed or `if`
354
+ // conditions change. The watchers skip these entries, see `isEntryCurrent()`.
355
+ createComputedValueScope(entry) {
356
+ const scope = this.rootScope.run(() => effectScope())
357
+ scope.run(() => {
358
+ const isEntryCurrent = computed(() => this.isEntryCurrent(entry))
359
+ if (entry.schema.compute) {
360
+ watch(
361
+ // Return a new object each time, so that the value is also written
362
+ // when only the value in the data changed, e.g. through user input.
363
+ () =>
364
+ isEntryCurrent.value
365
+ ? { value: this.getComputedValue(entry) }
366
+ : null,
367
+ computedResult => {
368
+ if (computedResult) {
369
+ this.writeComputedValue(entry, computedResult.value)
370
+ }
371
+ },
372
+ { immediate: true }
373
+ )
374
+ }
375
+ if (hasValueFromDataSchema(entry.schema)) {
376
+ const dataSchemaResolver = this.createDataSchemaResolver(
377
+ entry.schema,
378
+ entry
379
+ )
380
+ watch(
381
+ () =>
382
+ isEntryCurrent.value && !dataSchemaResolver.isLoading
383
+ ? { value: dataSchemaResolver.value }
384
+ : null,
385
+ resolved => {
386
+ if (resolved) {
387
+ this.writeComputedValue(entry, resolved.value)
388
+ }
389
+ },
390
+ { immediate: true }
391
+ )
392
+ }
393
+ })
394
+ return scope
395
+ }
396
+
397
+ // Returns whether the entry is still one of the current entries, with the
398
+ // same schema and data, see `getComputedValueEntries()`.
399
+ isEntryCurrent({ componentPath, schema, data }) {
400
+ const currentEntry = this.computedValueEntries.value.get(componentPath)
401
+ return currentEntry?.schema === schema && currentEntry.data === data
402
+ }
403
+
404
+ // Calls `schema.compute()` with the main schema component of the data, like
405
+ // `processData()`, and with the options resolved by the model, so that the
406
+ // result doesn't depend on whether the component is rendered.
407
+ getComputedValue(entry) {
408
+ const { schema, data, name, dataPath } = entry
409
+ return computeValue(schema, data, name, dataPath, {
410
+ component: this.component.mainSchemaComponent ?? this.component,
411
+ rootData: this.rootData,
412
+ getOptions: schema.options ? () => this.getOptions(entry) : null
413
+ })
414
+ }
415
+
416
+ // Returns the options of the component of the entry, resolved from
417
+ // `schema.options` when they're first read, `undefined` while loading.
418
+ getOptions(entry) {
419
+ return this.getOptionsResolver(entry).value
420
+ }
421
+
422
+ // Returns the resolver of the options of the component of the entry, shared
423
+ // by all callers with the same component path, schema and data, so that
424
+ // computes and the component that displays the options get the same option
425
+ // objects, and the options only load once.
426
+ getOptionsResolver(entry) {
427
+ const { schema, data, componentPath } = entry
428
+ let optionsRecord = this.optionsRecords.get(componentPath)
429
+ if (
430
+ !optionsRecord ||
431
+ optionsRecord.entry.schema.options !== schema.options ||
432
+ optionsRecord.entry.data !== data
433
+ ) {
434
+ optionsRecord = {
435
+ entry,
436
+ resolver: this.createDataSchemaResolver(schema.options, entry)
437
+ }
438
+ this.optionsRecords.set(componentPath, optionsRecord)
439
+ }
440
+ return optionsRecord.resolver
441
+ }
442
+
443
+ createDataSchemaResolver(dataSchema, entry) {
444
+ return new DataSchemaResolver(dataSchema, {
445
+ createContext: () => this.createEntryContext(entry),
446
+ onLoadStart: promise => this.trackPendingLoad(promise)
447
+ })
448
+ }
449
+
450
+ writeComputedValue(entry, value) {
451
+ const { data, name } = entry
452
+ if (!equals(value, data[name])) {
453
+ // Values written while the model settles are derived, see
454
+ // `takeProcessedDataSnapshot()`:
455
+ this.derivedValueDataPaths?.add(this.getRelativeDataPath(entry))
456
+ // Access `data[name]` directly to update the value without calling
457
+ // `parse()`, see `ValueMixin`:
458
+ data[name] = value
459
+ }
460
+ }
461
+
462
+ // Returns the data path of the value of the entry, relative to the model's
463
+ // data, as in its processed data.
464
+ getRelativeDataPath({ schema, name, dataPath }) {
465
+ // The data paths of nested components include their own name.
466
+ const tokens = isNested(schema)
467
+ ? parseDataPath(dataPath)
468
+ : [...parseDataPath(dataPath), name]
469
+ return tokens.slice(parseDataPath(this.dataPath).length)
470
+ }
471
+
472
+ // Returns the context for `if`, data schemas and options, with the component
473
+ // that owns the data, which stays the same when components are mounted, so
474
+ // that data schemas and options don't load again.
475
+ createEntryContext({ schema, data, name, dataPath }) {
476
+ return DitoContext.createForSchema(this.component, {
477
+ schema,
478
+ name,
479
+ data,
480
+ dataPath,
481
+ rootData: this.rootData
482
+ })
483
+ }
484
+ }
485
+
486
+ // Returns a copy of `processedData` with the values at `dataPaths` taken over
487
+ // from `currentProcessedData`, or removed if they're missing there. Values
488
+ // whose parents are missing in `processedData` are left out, as their parents
489
+ // differ anyway.
490
+ function takeOverValuesAtDataPaths(
491
+ processedData,
492
+ currentProcessedData,
493
+ dataPaths
494
+ ) {
495
+ if (dataPaths.size === 0) {
496
+ return processedData
497
+ }
498
+ const result = clone(processedData)
499
+ for (const dataPath of dataPaths) {
500
+ const parentDataPath = dataPath.slice(0, -1)
501
+ const key = dataPath.at(-1)
502
+ const parent = getValueAtDataPath(result, parentDataPath, () => null)
503
+ if (parent && typeof parent === 'object') {
504
+ const value = getValueAtDataPath(
505
+ currentProcessedData,
506
+ dataPath,
507
+ () => notFound
508
+ )
509
+ if (value === notFound) {
510
+ delete parent[key]
511
+ } else {
512
+ parent[key] = clone(value)
513
+ }
514
+ }
515
+ }
516
+ return result
517
+ }
518
+
519
+ // Returns a copy of `target` with the values that differ between `before` and
520
+ // `after` taken over from `after`, comparing objects and arrays of the same
521
+ // length entry by entry. Arrays whose length changed are taken over as a whole,
522
+ // as their entries can't be matched, including changes of their entries that
523
+ // were made before.
524
+ function takeOverChangedValues(target, before, after) {
525
+ if (equals(before, after)) {
526
+ return target
527
+ }
528
+ const isSameShape = (
529
+ isPlainObject(before) && isPlainObject(after) && isPlainObject(target) ||
530
+ isArray(before) &&
531
+ isArray(after) &&
532
+ isArray(target) &&
533
+ before.length === after.length &&
534
+ after.length === target.length
535
+ )
536
+ if (!isSameShape) {
537
+ return clone(after)
538
+ }
539
+ const result = isArray(target) ? [...target] : { ...target }
540
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
541
+ if (!(key in after)) {
542
+ delete result[key]
543
+ } else {
544
+ result[key] = takeOverChangedValues(target[key], before[key], after[key])
545
+ }
546
+ }
547
+ return result
548
+ }
549
+
550
+ const notFound = Symbol('notFound')
551
+
552
+ // Returns whether the schema is a source of computed values, through
553
+ // `schema.compute()` or a data schema of the `computed` types.
554
+ function hasComputedValueSource(schema) {
555
+ return !!schema.compute || hasValueFromDataSchema(schema)
556
+ }