@ditojs/admin 2.99.1 → 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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3532 -2763
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +82 -46
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +142 -95
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- 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
|
+
}
|