@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.
Files changed (84) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3532 -2763
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +82 -46
  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
@@ -24,14 +24,15 @@ import DitoComponent from '../DitoComponent.js'
24
24
  export default DitoComponent.component('DitoTrail', {
25
25
  computed: {
26
26
  trail() {
27
+ // Leave out the components that are being left, until they're unmounted.
27
28
  return this.appState.routeComponents.filter(
28
- component => !!component.routeRecord
29
+ component => !component.isLeavingRoute
29
30
  )
30
31
  }
31
32
  },
32
33
 
33
34
  methods: {
34
- getComponentPath(component) {
35
+ getComponentRoutePath(component) {
35
36
  // Do the same as in `DitoMenu`: Link menu items to the first children.
36
37
  const { schema } = component
37
38
  return schema.type === 'menu'
@@ -40,12 +41,12 @@ export default DitoComponent.component('DitoTrail', {
40
41
  },
41
42
 
42
43
  getComponentHref(component) {
43
- return this.$router.resolve(this.getComponentPath(component)).href
44
+ return this.$router.resolve(this.getComponentRoutePath(component)).href
44
45
  },
45
46
 
46
47
  onClickComponent(component) {
47
48
  this.$router.push({
48
- path: this.getComponentPath(component),
49
+ path: this.getComponentRoutePath(component),
49
50
  force: true
50
51
  })
51
52
  }
@@ -1,6 +1,6 @@
1
1
  <template lang="pug">
2
2
  .dito-tree-item(
3
- :id="dataPath"
3
+ :id="componentPath"
4
4
  :class=`{
5
5
  'dito-tree-item--dragging': isDragging,
6
6
  'dito-tree-item--active': active
@@ -66,7 +66,6 @@
66
66
  td
67
67
  DitoLabel(
68
68
  v-if="property.label !== false"
69
- :dataPath="getPropertyDataPath(property)"
70
69
  :label="getLabel(property)"
71
70
  )
72
71
  DitoTableCell(
@@ -83,7 +82,7 @@
83
82
  v-show="opened"
84
83
  :options="getDraggableOptions(true)"
85
84
  :draggable="childrenDraggable"
86
- :modelValue="updateOrder(childrenSchema, childrenList)"
85
+ :modelValue="childrenList"
87
86
  @update:modelValue="value => (childrenList = value)"
88
87
  )
89
88
  DitoTreeItem(
@@ -107,9 +106,10 @@
107
106
  import DitoComponent from '../DitoComponent.js'
108
107
  import ItemMixin from '../mixins/ItemMixin'
109
108
  import SortableMixin from '../mixins/SortableMixin.js'
110
- import { appendDataPath } from '../utils/data.js'
109
+ import { appendDataPath, getRelativeDataPath } from '../utils/data.js'
111
110
  import { getSchemaAccessor } from '../utils/accessor.js'
112
- import { getNamedSchemas, hasFormSchema } from '../utils/schema.js'
111
+ import { getNamedSchemas, hasFormSchema } from '../utils/schema/lookup.js'
112
+ import { updateOrder } from '../utils/schema/data.js'
113
113
  import { stripHtml } from '@ditojs/utils'
114
114
 
115
115
  // @vue/component
@@ -137,6 +137,17 @@ export default DitoComponent.component('DitoTreeItem', {
137
137
  },
138
138
 
139
139
  computed: {
140
+ componentPath() {
141
+ // Continue the path of the tree list with the item indices and children
142
+ // names of the data path. The root item's data path doesn't contain the
143
+ // list's name, see `DitoTypeTreeList.treeDataPath`.
144
+ const { componentPath, dataPath } = this.container
145
+ const relativeDataPath = getRelativeDataPath(this.dataPath, dataPath)
146
+ return relativeDataPath
147
+ ? appendDataPath(componentPath, relativeDataPath)
148
+ : componentPath
149
+ },
150
+
140
151
  meta() {
141
152
  return this.container.meta
142
153
  },
@@ -174,7 +185,7 @@ export default DitoComponent.component('DitoTreeItem', {
174
185
  set(value) {
175
186
  const name = this.childrenSchema?.name
176
187
  if (name) {
177
- this.updateOrder(this.childrenSchema, value)
188
+ updateOrder(this.childrenSchema, value)
178
189
  // eslint-disable-next-line vue/no-mutating-props
179
190
  this.data[name] = value
180
191
  this.$emit('update:data', value)
@@ -8,8 +8,13 @@ template(
8
8
  //- used to always render forms even when other nested forms are present.
9
9
  RouterView(
10
10
  v-if="!isLastRoute"
11
- :key="name"
11
+ v-slot="{ Component, route }"
12
12
  )
13
+ component(
14
+ v-if="Component"
15
+ :is="Component"
16
+ :key="getRouteComponentKey(route, routeLevel + 1)"
17
+ )
13
18
  .dito-view.dito-scroll-parent(
14
19
  v-else
15
20
  :data-resource="sourceSchema.path"
@@ -33,8 +38,9 @@ import RouteMixin from '../mixins/RouteMixin.js'
33
38
  import {
34
39
  isSingleComponentView,
35
40
  someNestedSchemaComponent
36
- } from '../utils/schema.js'
41
+ } from '../utils/schema/structure.js'
37
42
  import { hasResource } from '../utils/resource.js'
43
+ import { FormModel } from '../utils/FormModel.js'
38
44
 
39
45
  // @vue/component
40
46
  export default DitoComponent.component('DitoView', {
@@ -116,6 +122,16 @@ export default DitoComponent.component('DitoView', {
116
122
  }
117
123
  },
118
124
 
125
+ created() {
126
+ // Writes defaults and computed values into the view's data and resolves
127
+ // options, see `FormModel`:
128
+ this.formModel = new FormModel({
129
+ component: this,
130
+ getSchema: () => this.viewSchema,
131
+ getData: () => this.data
132
+ })
133
+ },
134
+
119
135
  mounted() {
120
136
  // Prevent bypassing of if-condition by direct URL access.
121
137
  if (!this.shouldRenderSchema(this.viewSchema)) {
@@ -123,9 +139,16 @@ export default DitoComponent.component('DitoView', {
123
139
  }
124
140
  },
125
141
 
142
+ beforeUnmount() {
143
+ // Stop the model before the component's own watchers are stopped, as the
144
+ // model's watchers aren't part of the component, see `FormModel`.
145
+ this.formModel.stop()
146
+ },
147
+
126
148
  methods: {
127
149
  setData(data) {
128
150
  this.data = data
151
+ return this.data
129
152
  },
130
153
 
131
154
  getChildPath(path) {
@@ -10,12 +10,10 @@ import {
10
10
  import appState from '../appState.js'
11
11
  import DitoContext from '../DitoContext.js'
12
12
  import EmitterMixin from './EmitterMixin.js'
13
- import {
14
- flattenViews,
15
- getSchemaValue,
16
- shouldRenderSchema
17
- } from '../utils/schema.js'
13
+ import { flattenViews } from '../utils/schema/setup.js'
14
+ import { getSchemaValue, shouldRenderSchema } from '../utils/schema/data.js'
18
15
  import { getResource, getMemberResource } from '../utils/resource.js'
16
+ import { appendDataPath, getRelativeDataPath } from '../utils/data.js'
19
17
  import { computed, reactive } from 'vue'
20
18
 
21
19
  // @vue/component
@@ -103,6 +101,34 @@ export default {
103
101
  return this.$root.$refs.root
104
102
  },
105
103
 
104
+ // The path of component names through views, tabs, panels and forms, with
105
+ // item indices from the data path, e.g. `main/chapters/0/title`. Unlike
106
+ // `dataPath`, it tells apart components that display the same data, and
107
+ // is used for registries and DOM ids. Components continue the path of
108
+ // their parent with the item indices that their data path adds to the
109
+ // parent's, e.g. inlined list items. `DitoContainer`, `DitoPane` and
110
+ // `DitoPanel` add the names of components, tabs and panels, `DitoForm`
111
+ // and `DitoTreeItem` continue the path of the source of their item.
112
+ componentPath() {
113
+ const { parentComponent } = this
114
+ const parentComponentPath = parentComponent?.componentPath ?? ''
115
+ const relativeDataPath = getRelativeDataPath(
116
+ this.dataPath,
117
+ parentComponent?.dataPathForComponentPath
118
+ )
119
+ return relativeDataPath
120
+ ? appendDataPath(parentComponentPath, relativeDataPath)
121
+ : parentComponentPath
122
+ },
123
+
124
+ // The data path that `componentPath` corresponds to, which children
125
+ // compare their data path with: the component's own, or for components
126
+ // without data path, e.g. `DitoDraggable` between lists and their items,
127
+ // the one of their parent.
128
+ dataPathForComponentPath() {
129
+ return this.dataPath ?? this.parentComponent?.dataPathForComponentPath
130
+ },
131
+
106
132
  // Use computed properties as links to injects, so DitoSchema can
107
133
  // override the property and return `this` instead of the parent.
108
134
  parentComponent() {
@@ -276,8 +302,9 @@ export default {
276
302
  : labelize(name) || ''
277
303
  },
278
304
 
279
- getButtonAttributes(verb) {
280
- const label = labelize(verb)
305
+ getButtonAttributes(verb, subject = null) {
306
+ // Name buttons by what they act on if known, e.g. 'Add Section'.
307
+ const label = subject ? `${labelize(verb)} ${subject}` : labelize(verb)
281
308
  return {
282
309
  'class': `dito-button--${verb}`,
283
310
  'title': label,
@@ -451,6 +478,14 @@ export default {
451
478
  body.removeChild(a)
452
479
  },
453
480
 
481
+ // Returns the key of the route component that `RouterView` renders at
482
+ // `routeLevel`, so that each route record gets its own component instance,
483
+ // as their schema and data belong to it, while the component is reused
484
+ // when only the parameters change, e.g. to edit another item.
485
+ getRouteComponentKey(route, routeLevel) {
486
+ return route.matched[routeLevel]?.path
487
+ },
488
+
454
489
  notify(options) {
455
490
  this.rootComponent.notify(options)
456
491
  },
@@ -1,10 +1,7 @@
1
1
  import DitoContext from '../DitoContext.js'
2
- import {
3
- getItemFormSchema,
4
- getItemId,
5
- getItemUid,
6
- isListSource
7
- } from '../utils/schema.js'
2
+ import { isListSource } from '../utils/schema/structure.js'
3
+ import { getItemFormSchema } from '../utils/schema/lookup.js'
4
+ import { getItemId, getItemUid } from '../utils/schema/data.js'
8
5
  import { appendDataPath } from '../utils/data.js'
9
6
  import { isObject, isString, isNumber, isFunction } from '@ditojs/utils'
10
7
 
@@ -1,8 +1,55 @@
1
1
  import { getSchemaAccessor } from '../utils/accessor.js'
2
+ import { getSchemaValue } from '../utils/schema/data.js'
2
3
  import { isArray } from '@ditojs/utils'
3
4
 
4
5
  // @vue/component
5
6
  export default {
7
+ getTypeValidations(schema, context) {
8
+ // Mirrors the `min`, `max`, `range`, `step` and `decimals` accessors below,
9
+ // but based on the schema alone, so they can be used without a component.
10
+ const isInteger = schema.type === 'integer'
11
+ const getNumberSchemaValue = key =>
12
+ getSchemaValue(key, { type: Number, schema, context })
13
+ const roundIfInteger = (value, roundFunction) =>
14
+ isInteger && value != null ? roundFunction(value) : value
15
+ const range = getSchemaValue('range', { type: Array, schema, context })
16
+ const min = roundIfInteger(
17
+ getNumberSchemaValue('min') ?? range?.[0],
18
+ Math.floor
19
+ )
20
+ const max = roundIfInteger(
21
+ getNumberSchemaValue('max') ?? range?.[1],
22
+ Math.ceil
23
+ )
24
+ const step = roundIfInteger(getNumberSchemaValue('step'), Math.ceil)
25
+ const decimals = getNumberSchemaValue('decimals')
26
+ const validations = {}
27
+ if (min != null && max != null) {
28
+ validations.range = [min, max]
29
+ } else {
30
+ if (min != null) {
31
+ validations.min = min
32
+ }
33
+ if (max != null) {
34
+ validations.max = max
35
+ }
36
+ }
37
+ if (decimals != null) {
38
+ validations.decimals = decimals
39
+ } else if (step) {
40
+ const stepDecimals = (`${step}`.split('.')[1] || '').length
41
+ if (stepDecimals > 0) {
42
+ validations.decimals = stepDecimals
43
+ } else {
44
+ validations.integer = true
45
+ }
46
+ }
47
+ if (isInteger) {
48
+ validations.integer = true
49
+ }
50
+ return validations
51
+ },
52
+
6
53
  computed: {
7
54
  inputValue: {
8
55
  get() {
@@ -83,36 +130,5 @@ export default {
83
130
  }
84
131
  }
85
132
  })
86
- },
87
-
88
- methods: {
89
- getValidations() {
90
- const validations = {}
91
- const { range, min, max, decimals, step } = this
92
- if (range) {
93
- validations.range = range
94
- } else {
95
- if (min != null) {
96
- validations.min = min
97
- }
98
- if (max != null) {
99
- validations.max = max
100
- }
101
- }
102
- if (decimals != null) {
103
- validations.decimals = decimals
104
- } else if (step) {
105
- const decimals = (`${step}`.split('.')[1] || '').length
106
- if (decimals > 0) {
107
- validations.decimals = decimals
108
- } else {
109
- validations.integer = true
110
- }
111
- }
112
- if (this.isInteger) {
113
- validations.integer = true
114
- }
115
- return validations
116
- }
117
133
  }
118
134
  }
@@ -1,10 +1,7 @@
1
+ import { toRaw } from 'vue'
1
2
  import DitoContext from '../DitoContext.js'
2
- import DataMixin from './DataMixin.js'
3
- import {
4
- hasViewSchema,
5
- getViewEditPath,
6
- getMultipleValue
7
- } from '../utils/schema.js'
3
+ import { hasViewSchema, getViewEditPath } from '../utils/schema/lookup.js'
4
+ import { getMultipleValue } from '../utils/schema/data.js'
8
5
  import { getSchemaAccessor } from '../utils/accessor.js'
9
6
  import { setTemporaryId, isReference } from '../utils/data.js'
10
7
  import {
@@ -19,8 +16,6 @@ import {
19
16
 
20
17
  // @vue/component
21
18
  export default {
22
- mixins: [DataMixin],
23
-
24
19
  computed: {
25
20
  // @overridable
26
21
  multiple() {
@@ -41,33 +36,27 @@ export default {
41
36
  : value
42
37
  }
43
38
 
44
- const value =
45
- this.multiple && isArray(this.value)
46
- ? this.value.map(convertValue).filter(value => value !== null)
47
- : convertValue(this.value)
48
-
49
- if (
50
- // As soon as the options are available, and...
51
- this.hasOptions && (
52
- // ...if the value is forced to null because a disappeared option...
53
- value === null && this.value !== null ||
54
- // ...or if the value is a reference, replace it with its option
55
- // value, so that it'll hold actual data, not just a reference id.
56
- isReference(this.value)
57
- )
58
- ) {
59
- // TODO: Fix side-effects
60
- // eslint-disable-next-line vue/no-side-effects-in-computed-properties
61
- this.selectedValue = value
62
- }
63
- return value
39
+ return this.multiple && isArray(this.value)
40
+ ? this.value.map(convertValue).filter(value => value !== null)
41
+ : convertValue(this.value)
64
42
  },
65
43
 
66
44
  set(value) {
67
- const convertValue = value =>
68
- this.relate
69
- ? this.getOptionForValue(value)
70
- : value
45
+ const convertValue = value => {
46
+ if (!this.relate) {
47
+ return value
48
+ }
49
+ const option = this.getOptionForValue(value)
50
+ if (this.shouldSetTemporaryId(option)) {
51
+ // Options without ids, e.g. new items of the edited data, get
52
+ // temporary ids when selected, so that the relation can reference
53
+ // them when saving, see `SchemaGraph`.
54
+ // NOTE: We need to modify the actual data, making a copy won't
55
+ // work as it won't propagate.
56
+ setTemporaryId(option, 'id')
57
+ }
58
+ return option
59
+ }
71
60
 
72
61
  this.value =
73
62
  this.multiple && isArray(value)
@@ -76,16 +65,48 @@ export default {
76
65
  }
77
66
  },
78
67
 
68
+ // Whether the value needs to be replaced with the selected value, once the
69
+ // options are available: if the value is forced to `null` because its
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`.
72
+ shouldReplaceValueWithSelectedValue() {
73
+ return (
74
+ this.hasOptions && (
75
+ this.selectedValue === null && this.value !== null ||
76
+ isReference(this.value)
77
+ )
78
+ )
79
+ },
80
+
79
81
  selectedOption() {
80
82
  return this.getOptionForValue(this.selectedValue)
81
83
  },
82
84
 
85
+ // The resolver of the options in the form model of the data, which loads
86
+ // them and shares them with the computes of the form. It is looked up with
87
+ // the entry of this component, in the shape of the entries of
88
+ // `processSchemaData()`, see `FormModel`.
89
+ optionsResolver() {
90
+ const formModel = (this.dialogComponent ?? this.routeComponent).formModel
91
+ return formModel.getOptionsResolver({
92
+ schema: this.schema,
93
+ data: this.data,
94
+ name: this.name,
95
+ dataPath: this.dataPath,
96
+ componentPath: this.componentPath
97
+ })
98
+ },
99
+
100
+ isLoadingOptions() {
101
+ return this.optionsResolver.isLoading
102
+ },
103
+
83
104
  options() {
84
- const data = this.handleDataSchema(this.schema.options, 'options') ?? []
85
- if (!isArray(data)) {
86
- throw new Error(`Invalid options data, should be array: ${data}`)
105
+ const options = this.optionsResolver.value ?? []
106
+ if (!isArray(options)) {
107
+ throw new Error(`Invalid options data, should be array: ${options}`)
87
108
  }
88
- return this.processOptions(data)
109
+ return this.groupBy ? this.groupOptions(options) : options
89
110
  },
90
111
 
91
112
  activeOptions() {
@@ -188,48 +209,73 @@ export default {
188
209
  }
189
210
  },
190
211
 
212
+ watch: {
213
+ // Replace the value with the selected value as soon as the options are
214
+ // available, and also when the value changes while it still needs to be
215
+ // replaced, e.g. a relation without value that is set to a reference.
216
+ 'shouldReplaceValueWithSelectedValue': {
217
+ handler(shouldReplace) {
218
+ if (shouldReplace) {
219
+ this.replaceValueWithSelectedValue()
220
+ }
221
+ },
222
+ immediate: true
223
+ },
224
+
225
+ 'value'() {
226
+ if (this.shouldReplaceValueWithSelectedValue) {
227
+ this.replaceValueWithSelectedValue()
228
+ }
229
+ },
230
+
231
+ 'optionsResolver.lastLoadError'(error) {
232
+ if (error) {
233
+ this.addError(error.message || error)
234
+ }
235
+ }
236
+ },
237
+
191
238
  methods: {
192
239
  getOptionKey(key) {
193
240
  const [option] = this.activeOptions
194
241
  return isObject(option) && key in option ? key : null
195
242
  },
196
243
 
197
- processOptions(options) {
198
- if (options.length) {
199
- if (this.relate) {
200
- // If ids are missing and we want to relate, set temporary ids.
201
- // NOTE: We need to modify the actual data, making a copy won't work
202
- // as it won't propagate.
203
- // NOTE: This only makes sense if the data is from the graph that
204
- // we're currently editing.
205
- for (const option of options) {
206
- if (!('id' in option)) {
207
- // TODO: Fix side-effects
208
- setTemporaryId(option, 'id')
244
+ // Writes the selected value back into the data, through the setter of
245
+ // `selectedValue`, which converts it to the value, e.g. a reference id to
246
+ // its option with `relate`.
247
+ replaceValueWithSelectedValue() {
248
+ const { selectedValue } = this
249
+ this.selectedValue = selectedValue
250
+ },
251
+
252
+ // Groups the options by the `groupBy` key, see `groupByLabel` and
253
+ // `groupByOptions`.
254
+ groupOptions(options) {
255
+ const groups = {}
256
+ return options.reduce(
257
+ (results, option) => {
258
+ const groupName = option[this.groupBy]
259
+ let group = groups[groupName]
260
+ if (!group) {
261
+ group = groups[groupName] = {
262
+ [this.groupByLabel]: groupName,
263
+ [this.groupByOptions]: []
209
264
  }
265
+ results.push(group)
210
266
  }
211
- }
212
- if (this.groupBy) {
213
- const grouped = {}
214
- options = options.reduce(
215
- (results, option) => {
216
- const group = option[this.groupBy]
217
- let entry = grouped[group]
218
- if (!entry) {
219
- entry = grouped[group] = {
220
- [this.groupByLabel]: group,
221
- [this.groupByOptions]: []
222
- }
223
- results.push(entry)
224
- }
225
- entry[this.groupByOptions].push(option)
226
- return results
227
- },
228
- []
229
- )
230
- }
231
- }
232
- return options
267
+ group[this.groupByOptions].push(option)
268
+ return results
269
+ },
270
+ []
271
+ )
272
+ },
273
+
274
+ // Whether the option needs a temporary id to be related to, as it has no
275
+ // id yet, e.g. a new item of the edited data. It gets one when selected,
276
+ // see `selectedValue`.
277
+ shouldSetTemporaryId(option) {
278
+ return this.relate && isObject(option) && option.id == null
233
279
  },
234
280
 
235
281
  getOptionForValue(value) {
@@ -259,6 +305,11 @@ export default {
259
305
  },
260
306
 
261
307
  getValueForOption(option) {
308
+ if (this.shouldSetTemporaryId(option)) {
309
+ // Until they're selected and get temporary ids, options without ids
310
+ // are their own values, compared by identity, see `selectedValue`.
311
+ return toRaw(option)
312
+ }
262
313
  const { optionValue } = this
263
314
  return isString(optionValue)
264
315
  ? option?.[optionValue] ?? null
@@ -281,7 +332,7 @@ export default {
281
332
  if (schema.relate) {
282
333
  // For internally relating data (`schema.options.dataPath`), we need to
283
334
  // process both the options (for '#ref') and the value ('#id').
284
- // See `DataMixin.handleDataSchema()`:
335
+ // See `DataSchemaResolver`:
285
336
  const path = schema.options?.dataPath
286
337
  const relatedDataPath = path
287
338
  ? normalizeDataPath(`${dataPath}/${path}`)
@@ -1,6 +1,6 @@
1
1
  import ItemMixin from './ItemMixin.js'
2
2
  import LoadingMixin from './LoadingMixin.js'
3
- import { setDefaultValues } from '../utils/schema.js'
3
+ import { initializeData } from '../utils/schema/data.js'
4
4
  import { assignDeeply, isObject, isString, labelize } from '@ditojs/utils'
5
5
  import { getResource } from '../utils/resource.js'
6
6
  import DitoContext from '../DitoContext.js'
@@ -160,9 +160,12 @@ export default {
160
160
  this.loadedData = null
161
161
  },
162
162
 
163
+ // Sets the loaded data and returns it as the reactive data of the
164
+ // component, which can be modified to update it.
163
165
  // @overridable
164
166
  setData(data) {
165
167
  this.loadedData = data
168
+ return this.loadedData
166
169
  },
167
170
 
168
171
  setupData() {
@@ -196,7 +199,7 @@ export default {
196
199
  },
197
200
 
198
201
  createData(schema, type) {
199
- return setDefaultValues(schema, type ? { type } : {}, this)
202
+ return initializeData(schema, type ? { type } : {}, this)
200
203
  },
201
204
 
202
205
  requestData() {