@koumoul/vjsf 3.0.0-beta.3 → 3.0.0-beta.30

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 (82) hide show
  1. package/README.md +21 -0
  2. package/package.json +5 -4
  3. package/src/compat/v2.js +127 -27
  4. package/src/compile/index.js +18 -4
  5. package/src/compile/options.js +3 -7
  6. package/src/compile/v-jsf-compiled.vue.ejs +1 -1
  7. package/src/components/fragments/help-message.vue +11 -1
  8. package/src/components/fragments/section-header.vue +1 -2
  9. package/src/components/fragments/select-item-icon.vue +1 -1
  10. package/src/components/fragments/select-selection.vue +2 -1
  11. package/src/components/fragments/selection-group.vue +101 -0
  12. package/src/components/fragments/text-field-menu.vue +5 -1
  13. package/src/components/node.vue +4 -3
  14. package/src/components/nodes/autocomplete.vue +9 -35
  15. package/src/components/nodes/checkbox-group.vue +36 -0
  16. package/src/components/nodes/checkbox.vue +6 -1
  17. package/src/components/nodes/color-picker.vue +2 -1
  18. package/src/components/nodes/date-picker.vue +1 -1
  19. package/src/components/nodes/expansion-panels.vue +22 -12
  20. package/src/components/nodes/list.vue +143 -88
  21. package/src/components/nodes/one-of-select.vue +42 -24
  22. package/src/components/nodes/radio-group.vue +50 -0
  23. package/src/components/nodes/select.vue +7 -27
  24. package/src/components/nodes/switch-group.vue +36 -0
  25. package/src/components/nodes/switch.vue +6 -3
  26. package/src/components/options.js +21 -5
  27. package/src/components/vjsf.vue +6 -0
  28. package/src/composables/use-dnd.js +1 -0
  29. package/src/composables/use-get-items.js +48 -0
  30. package/src/composables/use-vjsf.js +76 -40
  31. package/src/index.js +3 -0
  32. package/src/types.ts +21 -6
  33. package/src/utils/build.js +1 -1
  34. package/src/utils/index.js +0 -1
  35. package/src/utils/props.js +9 -25
  36. package/types/compat/v2.d.ts.map +1 -1
  37. package/types/compile/index.d.ts +2 -2
  38. package/types/compile/index.d.ts.map +1 -1
  39. package/types/compile/options.d.ts.map +1 -1
  40. package/types/components/fragments/selection-group.vue.d.ts +35 -0
  41. package/types/components/fragments/selection-group.vue.d.ts.map +1 -0
  42. package/types/components/fragments/text-field-menu.vue.d.ts.map +1 -1
  43. package/types/components/nodes/autocomplete.vue.d.ts.map +1 -1
  44. package/types/components/nodes/checkbox-group copy.vue.d.ts +27 -0
  45. package/types/components/nodes/checkbox-group copy.vue.d.ts.map +1 -0
  46. package/types/components/nodes/checkbox-group.vue.d.ts +27 -0
  47. package/types/components/nodes/checkbox-group.vue.d.ts.map +1 -0
  48. package/types/components/nodes/radio-group.vue.d.ts +27 -0
  49. package/types/components/nodes/radio-group.vue.d.ts.map +1 -0
  50. package/types/components/nodes/radio.vue.d.ts +10 -0
  51. package/types/components/nodes/radio.vue.d.ts.map +1 -0
  52. package/types/components/nodes/select.vue.d.ts.map +1 -1
  53. package/types/components/nodes/switch-group.vue.d.ts +27 -0
  54. package/types/components/nodes/switch-group.vue.d.ts.map +1 -0
  55. package/types/components/options.d.ts +1 -1
  56. package/types/components/options.d.ts.map +1 -1
  57. package/types/components/vjsf.vue.d.ts +2 -2
  58. package/types/composables/use-dnd.d.ts.map +1 -1
  59. package/types/composables/use-get-items.d.ts +13 -0
  60. package/types/composables/use-get-items.d.ts.map +1 -0
  61. package/types/composables/use-vjsf.d.ts.map +1 -1
  62. package/types/index.d.ts +2 -0
  63. package/types/index.d.ts.map +1 -1
  64. package/types/types.d.ts +20 -8
  65. package/types/types.d.ts.map +1 -1
  66. package/types/utils/build.d.ts +1 -1
  67. package/types/utils/index.d.ts +0 -1
  68. package/types/utils/props.d.ts +3 -4
  69. package/types/utils/props.d.ts.map +1 -1
  70. package/src/utils/global-register.js +0 -13
  71. package/types/components/global-register.d.ts +0 -8
  72. package/types/components/global-register.d.ts.map +0 -1
  73. package/types/components/nodes/markdown.vue.d.ts +0 -27
  74. package/types/components/nodes/markdown.vue.d.ts.map +0 -1
  75. package/types/components/nodes/text-field copy.vue.d.ts +0 -10
  76. package/types/components/nodes/text-field copy.vue.d.ts.map +0 -1
  77. package/types/components/types.d.ts +0 -91
  78. package/types/components/types.d.ts.map +0 -1
  79. package/types/components/v-jsf.vue.d.ts +0 -13
  80. package/types/components/v-jsf.vue.d.ts.map +0 -1
  81. package/types/utils/clone.d.ts +0 -3
  82. package/types/utils/clone.d.ts.map +0 -1
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # VJSF
2
+
3
+ *vuetify-json-schema-form* - *@koumoul/vjsf on npm*
4
+
5
+ Easily create beautiful forms that output valid data.
6
+
7
+ Based on [Vue.js](https://vuejs.org/) / [Vuetify](https://vuetifyjs.com/) / [JSON Schema](https://json-schema.org/) / [JSON Layout](https://github.com/json-layout/json-layout).
8
+
9
+ See [the documentation](https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/).
10
+
11
+ See [the documentation for deprecated v2](https://koumoul-dev.github.io/vuetify-jsonschema-form/2.x/).
12
+
13
+ ![](doc/static/vjsf.gif)
14
+
15
+ ## Bug reports
16
+
17
+ Bug reports are created using github issues. The examples in the documentation include codepen links, as much as possible please save a duplicate codepen with the minimal schema/config to reproduce your problem.
18
+
19
+ ## Contribute
20
+
21
+ See [CONTRIBUTE.md](./CONTRIBUTE.md).
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@koumoul/vjsf",
3
- "version": "3.0.0-beta.3",
3
+ "version": "3.0.0-beta.30",
4
4
  "description": "Generate forms for the vuetify UI library (vuejs) based on annotated JSON schemas.",
5
5
  "scripts": {
6
6
  "test": "vitest",
7
7
  "build": "vue-tsc",
8
- "watch:build": "vue-tsc --watch"
8
+ "watch:build": "vue-tsc --watch",
9
+ "prepublishOnly": "cp ../README.md README.md && cp ../LICENSE LICENSE"
9
10
  },
10
11
  "author": "Alban Mouton <alban.mouton@gmail.com>",
11
12
  "license": "MIT",
@@ -72,10 +73,10 @@
72
73
  },
73
74
  "peerDependencies": {
74
75
  "vue": "^3.4.3",
75
- "vuetify": "^3.4.9"
76
+ "vuetify": "^3.6.8"
76
77
  },
77
78
  "dependencies": {
78
- "@json-layout/core": "0.9.3",
79
+ "@json-layout/core": "0.25.0",
79
80
  "@vueuse/core": "^10.5.0",
80
81
  "debug": "^4.3.4",
81
82
  "ejs": "^3.1.9"
package/src/compat/v2.js CHANGED
@@ -1,23 +1,96 @@
1
1
  import ajvModule from 'ajv'
2
2
  import addFormats from 'ajv-formats'
3
- import { resolveRefs, clone } from '@json-layout/core'
3
+ import { resolveLocaleRefs, clone } from '@json-layout/core'
4
4
  import { isPartialGetItemsObj } from '@json-layout/vocabulary'
5
5
 
6
6
  // @ts-ignore
7
7
  const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
8
8
 
9
- const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
9
+ const expressionKeyMappings = {
10
+ modelRoot: 'rootData',
11
+ root: 'rootData',
12
+ model: 'data',
13
+ value: 'data'
14
+ }
15
+
16
+ /**
17
+ * @param {string} expression
18
+ * @returns {string}
19
+ */
20
+ const prefixExpression = (expression) => {
21
+ // looks like a simple expression missing the data. prefix
22
+ if (expression.match(/^[a-z.]*$/i) && !['data', 'context', 'rootData', 'parent'].some(key => expression.startsWith(key + '.'))) {
23
+ return 'rootData.' + expression
24
+ }
25
+ return expression
26
+ }
27
+
28
+ /**
29
+ *
30
+ * @param {string} expression
31
+ * @param {'js-eval' | 'js-tpl'} [type]
32
+ * @returns {{type: 'js-eval' | 'js-tpl', expr: string, pure: boolean}}
33
+ */
34
+ const fixExpression = (expression, type = 'js-eval') => {
35
+ let expr = expression
36
+ let pure = true
37
+
38
+ if (expr.includes('parent.value')) {
39
+ pure = false
40
+ expr = expr.replace(/parent\.value/g, 'parent.data')
41
+ }
42
+
43
+ for (const [key, value] of Object.entries(expressionKeyMappings)) {
44
+ expr = expr.replace(new RegExp(`${key}\\.`, 'g'), value + '.')
45
+ }
46
+ if (type === 'js-eval') {
47
+ expr = prefixExpression(expr)
48
+ }
49
+ if (type === 'js-tpl') {
50
+ for (const expressionMatch of expr.matchAll(/\{(.*?)\}/g)) {
51
+ if (expressionMatch[1] !== 'q') expr = expr.replace(expressionMatch[0], '${' + prefixExpression(expressionMatch[1]) + '}')
52
+ }
53
+ }
54
+
55
+ if (expr.includes('rootData')) {
56
+ pure = false
57
+ }
58
+
59
+ return { type, expr, pure }
60
+ }
61
+
62
+ /**
63
+ *
64
+ * @param {import("ajv").SchemaObject} schema
65
+ * @param {(schemaId: string, ref: string) => [any, string, string]} getJSONRef
66
+ * @param {string} schemaId
67
+ */
68
+ const processFragment = (schema, getJSONRef, schemaId) => {
69
+ if (schema.$ref) {
70
+ const [refFragment, refSchemaId] = getJSONRef(schemaId, schema.$ref)
71
+ processFragment(refFragment, getJSONRef, refSchemaId)
72
+ }
10
73
  if (!schema.layout) {
11
74
  /** @type import('@json-layout/vocabulary').PartialCompObject */
12
75
  const layout = {}
13
76
 
77
+ if (schema.separator || schema['x-separator']) {
78
+ layout.separator = schema.separator || schema['x-separator']
79
+ delete schema.separator
80
+ delete schema['x-separator']
81
+ }
82
+
14
83
  if (schema['x-display'] === 'icon' && (schema.enum || schema.items?.enum)) {
15
84
  layout.getItems = { itemIcon: schema['x-itemIcon'] || 'data.value' }
16
85
  delete schema['x-display']
17
86
  }
18
87
 
19
88
  if (schema['x-display']) {
20
- layout.comp = schema['x-display']
89
+ let display = schema['x-display']
90
+ if (display === 'radio') display = 'radio-group'
91
+ if (display === 'checkbox' && schema.type !== 'boolean') display = 'checkbox-group'
92
+ if (display === 'switch' && schema.type !== 'boolean') display = 'switch-group'
93
+ layout.comp = display
21
94
  delete schema['x-display']
22
95
  }
23
96
 
@@ -32,18 +105,20 @@ const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
32
105
  }
33
106
 
34
107
  if (schema['x-fromData']) {
35
- layout.comp = 'select'
36
- layout.getItems = { expr: schema['x-fromData'] }
108
+ layout.comp = layout.comp ?? 'select'
109
+ layout.getItems = fixExpression(schema['x-fromData'])
37
110
  delete schema['x-fromData']
38
111
  }
39
112
 
113
+ if (schema['x-if']) {
114
+ layout.if = fixExpression(schema['x-if'])
115
+ delete schema['x-if']
116
+ }
117
+
40
118
  if (schema['x-fromUrl']) {
41
119
  /** @type string */
42
- let url = schema['x-fromUrl']
43
- for (const expressionMatch of url.matchAll(/\{(.*?)\}/g)) {
44
- if (expressionMatch[1] !== 'q') url = url.replace(expressionMatch[0], '$' + expressionMatch[0])
45
- }
46
- layout.getItems = { url }
120
+ const url = schema['x-fromUrl']
121
+ layout.getItems = { url: fixExpression(url, 'js-tpl') }
47
122
  delete schema['x-fromUrl']
48
123
  }
49
124
  if (layout.getItems && isPartialGetItemsObj(layout.getItems)) {
@@ -58,6 +133,10 @@ const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
58
133
  delete schema['x-itemsProp']
59
134
  }
60
135
 
136
+ if (schema['x-cols']) {
137
+ layout.cols = schema['x-cols']
138
+ }
139
+
61
140
  // compact the layout keyword if possible
62
141
  if (Object.keys(layout).length === 1 && 'comp' in layout) {
63
142
  schema.layout = layout.comp
@@ -66,30 +145,51 @@ const processFragment = (/** @type {import("ajv").SchemaObject} */schema) => {
66
145
  }
67
146
  }
68
147
 
69
- if (schema.type === 'object') {
70
- if (schema.properties) {
71
- for (const propertyKey of Object.keys(schema.properties)) {
72
- processFragment(schema.properties[propertyKey])
73
- }
74
- }
75
- if (schema.allOf) {
76
- for (const item of schema.allOf) processFragment(item)
148
+ if (schema.properties) {
149
+ for (const propertyKey of Object.keys(schema.properties)) {
150
+ processFragment(schema.properties[propertyKey], getJSONRef, schemaId)
77
151
  }
78
- if (schema.oneOf) {
79
- for (const item of schema.oneOf) processFragment(item)
80
- }
81
- if (schema.anyOf) {
82
- for (const item of schema.anyOf) processFragment(item)
152
+ }
153
+
154
+ if (schema.allOf) {
155
+ for (const item of schema.allOf) processFragment(item, getJSONRef, schemaId)
156
+ }
157
+
158
+ if (schema.oneOf) {
159
+ if (!schema.oneOfLayout) {
160
+ const constPropertyKey = Object.keys(schema.oneOf[0]?.properties || {})
161
+ .find(key => !!schema.oneOf[0]?.properties[key].const)
162
+ if (constPropertyKey) {
163
+ const constProperty = schema.oneOf[0]?.properties[constPropertyKey]
164
+ if (constProperty?.title) schema.oneOfLayout = { label: constProperty.title }
165
+ if (schema.required && Array.isArray(schema.required)) {
166
+ schema.required = schema.required.filter(key => key !== constPropertyKey)
167
+ }
168
+ }
83
169
  }
170
+ for (const item of schema.oneOf) processFragment(item, getJSONRef, schemaId)
171
+ }
172
+
173
+ if (schema.anyOf) {
174
+ for (const item of schema.anyOf) processFragment(item, getJSONRef, schemaId)
84
175
  }
85
176
 
86
177
  if (schema.type === 'array' && schema.items) {
87
178
  if (Array.isArray(schema.items)) {
88
- for (const item of schema.items) processFragment(item)
179
+ for (const item of schema.items) processFragment(item, getJSONRef, schemaId)
89
180
  } else {
90
- processFragment(schema.items)
181
+ processFragment(schema.items, getJSONRef, schemaId)
91
182
  }
92
183
  }
184
+ if (schema.dependencies) {
185
+ for (const key of Object.keys(schema.dependencies)) {
186
+ processFragment(schema.dependencies[key], getJSONRef, schemaId)
187
+ }
188
+ }
189
+ if (schema.if) {
190
+ if (schema.then) processFragment(schema.then, getJSONRef, schemaId)
191
+ if (schema.else) processFragment(schema.else, getJSONRef, schemaId)
192
+ }
93
193
  }
94
194
 
95
195
  /**
@@ -111,7 +211,7 @@ export function v2compat (_schema, _ajv, lang = 'en') {
111
211
 
112
212
  const schema = /** @type {import("ajv").SchemaObject} */ (clone(_schema))
113
213
  schema.$id = schema.$id ?? '_jl'
114
- resolveRefs(schema, ajv, lang)
115
- processFragment(schema)
214
+ const getJSONRef = resolveLocaleRefs(schema, ajv, lang)
215
+ processFragment(schema, getJSONRef, schema.$id)
116
216
  return schema
117
217
  }
@@ -37,10 +37,17 @@ function listComps (comps, layout) {
37
37
  * @param {object} schema
38
38
  * @param {import('../types.js').PartialVjsfCompileOptions} [options]
39
39
  * @param {string} [baseImport]
40
- * @returns {string}
40
+ * @returns {Promise<string>}
41
41
  */
42
- export function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
42
+ export async function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
43
43
  const fullOptions = getFullOptions(options)
44
+ /** @type {Record<string, string>} */
45
+ const pluginsImportsByName = {}
46
+ for (const pluginImport of fullOptions.pluginsImports) {
47
+ const componentInfo = /** @type {import('@json-layout/vocabulary').ComponentInfo} */((await import(pluginImport + '/info.js')).default)
48
+ fullOptions.components[componentInfo.name] = componentInfo
49
+ pluginsImportsByName[componentInfo.name] = pluginImport
50
+ }
44
51
  const compiledLayout = compileLayout(schema, { ...fullOptions, code: true })
45
52
  const compiledLayoutCode = serializeCompiledLayout(compiledLayout)
46
53
  /** @type Set<string> */
@@ -50,9 +57,16 @@ export function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
50
57
  }
51
58
  comps.delete('none')
52
59
 
60
+ /** @type {Record<string, any>} */
61
+ const pluginsComponents = {}
62
+
53
63
  const compImports = [...comps].map(comp => {
54
64
  const compName = comp.replace(/-/g, '') + 'Node'
55
- const compImport = fullOptions.nodeComponentImports[comp] ?? `${baseImport}/components/nodes/${comp}.vue`
65
+ let compImport = `${baseImport}/components/nodes/${comp}.vue`
66
+ if (pluginsImportsByName[comp]) {
67
+ compImport = `${pluginsImportsByName[comp]}/node.vue`
68
+ pluginsComponents[comp] = fullOptions.components[comp]
69
+ }
56
70
  return {
57
71
  comp,
58
72
  compName,
@@ -60,6 +74,6 @@ export function compile (schema, options = {}, baseImport = '@koumoul/vjsf') {
60
74
  }
61
75
  })
62
76
 
63
- const code = ejs.render(template, { compiledLayoutCode, compImports, baseImport })
77
+ const code = ejs.render(template, { compiledLayoutCode, compImports, baseImport, pluginsComponents })
64
78
  return code
65
79
  }
@@ -1,8 +1,7 @@
1
1
  /** @type import("../types.js").PartialVjsfCompileOptions */
2
2
  export const defaultOptions = {
3
- nodeComponentImports: {
4
- markdown: '@koumoul/vjsf-markdown/components/nodes/markdown.vue'
5
- }
3
+ pluginsImports: ['@koumoul/vjsf-markdown'],
4
+ components: {}
6
5
  }
7
6
 
8
7
  /**
@@ -11,9 +10,6 @@ export const defaultOptions = {
11
10
  * @returns
12
11
  */
13
12
  export const getFullOptions = (options) => {
14
- const fullOptions = {
15
- ...defaultOptions,
16
- nodeComponentImports: { ...defaultOptions.nodeComponentImports, ...options.nodeComponentImports }
17
- }
13
+ const fullOptions = { ...defaultOptions }
18
14
  return /** @type import('../types.js').VjsfCompileOptions */ (fullOptions)
19
15
  }
@@ -39,7 +39,7 @@ const emit = defineEmits(emits)
39
39
  const { el, statefulLayout, stateTree } = useVjsf(
40
40
  null,
41
41
  computed(() => props.modelValue),
42
- computed(() => props.options),
42
+ computed(() => ({...props.options, components: <%- JSON.stringify(pluginsComponents) %>})),
43
43
  nodeComponents,
44
44
  emit,
45
45
  null,
@@ -4,15 +4,17 @@
4
4
  <v-alert
5
5
  v-show="show"
6
6
  color="info"
7
+ :density="node.options.density"
7
8
  >
8
9
  <div v-html="node.layout.help" />
9
10
  </v-alert>
10
11
  </v-slide-x-reverse-transition>
11
12
  <v-btn
12
13
  color="info"
13
- class="vjsf-help-message-toggle"
14
+ :class="`vjsf-help-message-toggle vjsf-help-message-toggle-${node.options.density}`"
14
15
  :icon="show ? 'mdi-close-circle' : 'mdi-information'"
15
16
  density="compact"
17
+ :size="node.options.density !== 'default' ? 'small' : 'default'"
16
18
  :title="show ? '' : node.messages.showHelp"
17
19
  @click="show = !show"
18
20
  />
@@ -45,5 +47,13 @@ const show = ref(false)
45
47
  right: -4px;
46
48
  z-index: 1;
47
49
  }
50
+ .vjsf-help-message-toggle-comfortable {
51
+ top: -4px;
52
+ right: -4px;
53
+ }
54
+ .vjsf-help-message-toggle-compact {
55
+ top: -4px;
56
+ right: -4px;
57
+ }
48
58
  </style>
49
59
  ../../../types.js
@@ -45,7 +45,7 @@ const titleClass = computed(() => {
45
45
  </p>
46
46
  <v-alert
47
47
  v-if="node.error && node.validated"
48
- v-bind="node.options.errorAlertProps"
48
+ type="error"
49
49
  :class="`mt-${titleDepthBase - node.options.titleDepth}`"
50
50
  :density="node.options.density"
51
51
  >
@@ -53,4 +53,3 @@ const titleClass = computed(() => {
53
53
  </v-alert>
54
54
  </div>
55
55
  </template>
56
- ../../../types.js
@@ -20,7 +20,7 @@ export default defineComponent({
20
20
  } else if (isSVG.value) {
21
21
  return h('div', { innerHTML: props.icon.replace('<svg ', '<svg class="v-icon__svg" '), class: 'v-icon' })
22
22
  } else {
23
- return h(VIcon, null, props.icon)
23
+ return h(VIcon, null, () => props.icon)
24
24
  }
25
25
  }
26
26
  }
@@ -18,6 +18,7 @@ defineProps({
18
18
  required: true
19
19
  }
20
20
  })
21
+
21
22
  </script>
22
23
 
23
24
  <template>
@@ -26,7 +27,7 @@ defineProps({
26
27
  v-if="item.icon"
27
28
  :icon="item.icon"
28
29
  />
29
- {{ item.title }}
30
+ {{ item.title ?? item.key ?? item.value }}
30
31
  <span
31
32
  v-if="multiple && !last"
32
33
  class="v-select__selection-comma"
@@ -0,0 +1,101 @@
1
+ <script>
2
+ import { VInput, VLabel, VCheckbox, VSwitch, VSkeletonLoader } from 'vuetify/components'
3
+ import { defineComponent, h, computed } from 'vue'
4
+ import { getInputProps, getCompSlots } from '../../utils/index.js'
5
+ import useGetItems from '../../composables/use-get-items.js'
6
+
7
+ export default defineComponent({
8
+ props: {
9
+ modelValue: {
10
+ /** @type import('vue').PropType<import('../../types.js').VjsfCheckboxGroupNode> */
11
+ type: Object,
12
+ required: true
13
+ },
14
+ statefulLayout: {
15
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
16
+ type: Object,
17
+ required: true
18
+ },
19
+ type: {
20
+ type: String,
21
+ required: true
22
+ }
23
+ },
24
+ setup (props) {
25
+ const getItems = useGetItems(props)
26
+
27
+ const fieldProps = computed(() => {
28
+ const fieldProps = getInputProps(props.modelValue, props.statefulLayout)
29
+ fieldProps.class.push('v-radio-group') // reuse some styles from radio-group
30
+ fieldProps.class.push('vjsf-selection-group')
31
+ return fieldProps
32
+ })
33
+
34
+ const fieldSlots = computed(() => {
35
+ const slots = getCompSlots(props.modelValue, props.statefulLayout)
36
+
37
+ if (!slots.default) {
38
+ slots.default = () => {
39
+ /** @type {import('vue').VNode[]} */
40
+ const children = [h(VLabel, { text: fieldProps.value.label })]
41
+ if (getItems.loading.value) {
42
+ children.push(h(VSkeletonLoader, { type: 'chip' }))
43
+ } else {
44
+ /** @type {import('vue').VNode[]} */
45
+ const checkboxes = []
46
+ for (const item of getItems.items.value) {
47
+ let modelValue = false
48
+ if (props.modelValue.layout.multiple) {
49
+ modelValue = props.modelValue.data?.includes(item.value)
50
+ } else {
51
+ modelValue = props.modelValue.data === item.value
52
+ }
53
+ checkboxes.push(h(props.type === 'switch' ? VSwitch : VCheckbox, {
54
+ label: item.title,
55
+ hideDetails: true,
56
+ density: props.modelValue.options?.density,
57
+ key: item.key,
58
+ modelValue,
59
+ onClick: () => {
60
+ let newValue
61
+ if (props.modelValue.layout.multiple) {
62
+ newValue = props.modelValue.data ? [...props.modelValue.data] : []
63
+ if (newValue.includes(item.value)) {
64
+ newValue = newValue.filter((/** @type {any} */v) => v !== item.value)
65
+ } else {
66
+ newValue.push(item.value)
67
+ }
68
+ } else {
69
+ if (props.modelValue.data === item.value) {
70
+ newValue = undefined
71
+ } else {
72
+ newValue = item.value
73
+ }
74
+ }
75
+ props.statefulLayout.input(props.modelValue, newValue)
76
+ }
77
+ }))
78
+ }
79
+ children.push(h('div', { class: 'v-selection-control-group' }, checkboxes))
80
+ }
81
+ return children
82
+ }
83
+ }
84
+
85
+ return slots
86
+ })
87
+
88
+ // @ts-ignore
89
+ return () => {
90
+ return h(VInput, fieldProps.value, fieldSlots.value)
91
+ }
92
+ }
93
+ })
94
+
95
+ </script>
96
+
97
+ <style>
98
+ .vjsf-selection-group .v-selection-control-group>.v-input .v-selection-control {
99
+ min-height: auto;
100
+ }
101
+ </style>
@@ -24,11 +24,15 @@ const props = defineProps({
24
24
  const fieldProps = computed(() => {
25
25
  const fieldProps = getInputProps(props.modelValue, props.statefulLayout, [], false)
26
26
  fieldProps.readonly = true
27
+ fieldProps.clearable = fieldProps.clearable ?? !props.modelValue.skeleton.required
28
+ fieldProps['onClick:clear'] = () => {
29
+ props.statefulLayout.input(props.modelValue, null)
30
+ }
27
31
  return fieldProps
28
32
  })
29
33
 
30
34
  const menuProps = computed(() => {
31
- const menuProps = getCompProps(props.modelValue, 'menu', false)
35
+ const menuProps = getCompProps(props.modelValue)
32
36
  menuProps.closeOnContentClick = false
33
37
  menuProps.disabled = true
34
38
  return menuProps
@@ -35,7 +35,7 @@ const nodeClasses = computed(() => {
35
35
  return classes
36
36
  })
37
37
 
38
- if (!props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp]) {
38
+ if (props.modelValue.layout.comp !== 'none' && !props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp]) {
39
39
  console.error(`vjsf: missing component to render vjsf node "${props.modelValue.layout.comp}", maybe you forgot to register a component from a plugin ?`)
40
40
  }
41
41
 
@@ -43,6 +43,7 @@ if (!props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp])
43
43
 
44
44
  <template>
45
45
  <v-col
46
+ v-if="modelValue.layout.comp !== 'none'"
46
47
  :cols="modelValue.cols"
47
48
  :class="nodeClasses"
48
49
  >
@@ -56,7 +57,7 @@ if (!props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp])
56
57
  />
57
58
 
58
59
  <help-message
59
- v-if="modelValue.layout.help"
60
+ v-if="modelValue.layout.help && !modelValue.options.summary"
60
61
  :node="modelValue"
61
62
  :class="beforeAfterClasses[modelValue.options.density]"
62
63
  />
@@ -69,7 +70,7 @@ if (!props.statefulLayout.options.nodeComponents[props.modelValue.layout.comp])
69
70
  />
70
71
  <component
71
72
  :is="props.statefulLayout.options.nodeComponents[modelValue.layout.comp]"
72
- v-else-if="modelValue.layout.comp !== 'none' "
73
+ v-else
73
74
  :model-value="modelValue"
74
75
  :stateful-layout="statefulLayout"
75
76
  />
@@ -1,7 +1,8 @@
1
1
  <script>
2
2
  import { VAutocomplete } from 'vuetify/components'
3
- import { defineComponent, computed, ref, shallowRef, h } from 'vue'
3
+ import { defineComponent, computed, h } from 'vue'
4
4
  import { getInputProps, getCompSlots } from '../../utils/index.js'
5
+ import useGetItems from '../../composables/use-get-items.js'
5
6
  import SelectItem from '../fragments/select-item.vue'
6
7
  import SelectSelection from '../fragments/select-selection.vue'
7
8
 
@@ -13,54 +14,27 @@ export default defineComponent({
13
14
  required: true
14
15
  },
15
16
  statefulLayout: {
16
- /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
17
+ /** @type import('vue').PropType<import('../../types.js').VjsfStatefulLayout> */
17
18
  type: Object,
18
19
  required: true
19
20
  }
20
21
  },
21
22
  setup (props) {
22
- /** @type import('vue').ShallowRef<import('@json-layout/vocabulary').SelectItems> */
23
- const items = shallowRef([])
24
- /** @type import('vue').Ref<boolean> */
25
- const loading = ref(false)
26
- /** @type import('vue').Ref<string> */
27
- const search = ref('')
23
+ const getItems = useGetItems(props)
28
24
 
29
25
  const fieldProps = computed(() => {
30
26
  const fieldProps = getInputProps(props.modelValue, props.statefulLayout, ['multiple'])
31
27
  if (props.modelValue.options.readOnly) fieldProps.menuProps = { modelValue: false }
32
28
  fieldProps.noFilter = true
33
29
  fieldProps['onUpdate:search'] = (/** @type string */searchValue) => {
34
- search.value = searchValue
35
- refresh()
30
+ getItems.search.value = searchValue
36
31
  }
37
- fieldProps['onUpdate:menu'] = refresh
38
- fieldProps.items = items.value
39
- fieldProps.loading = loading.value
32
+ fieldProps.items = getItems.items.value
33
+ fieldProps.loading = getItems.loading.value
34
+ fieldProps.clearable = fieldProps.clearable ?? !props.modelValue.skeleton.required
40
35
  return fieldProps
41
36
  })
42
37
 
43
- /** @type import('@json-layout/core').StateTree | null */
44
- let lastStateTree = null
45
- /** @type Record<string, any> | null */
46
- let lastContext = null
47
- /** @type string */
48
- let lastSearch = ''
49
-
50
- const refresh = async () => {
51
- if (props.statefulLayout.stateTree === lastStateTree && props.statefulLayout.options.context === lastContext && search.value === lastSearch) return
52
- loading.value = true
53
- items.value = await props.statefulLayout.getItems(props.modelValue, search.value)
54
- lastStateTree = props.statefulLayout.stateTree
55
- lastContext = props.statefulLayout.options.context ?? null
56
- lastSearch = search.value
57
- loading.value = false
58
- }
59
-
60
- if (!props.modelValue.layout.items) {
61
- refresh()
62
- }
63
-
64
38
  const fieldSlots = computed(() => {
65
39
  const slots = getCompSlots(props.modelValue, props.statefulLayout)
66
40
  if (!slots.item) {
@@ -74,7 +48,7 @@ export default defineComponent({
74
48
  slots.selection = (/** @type {any} */ context) => h(SelectSelection, {
75
49
  multiple: props.modelValue.layout.multiple,
76
50
  last: props.modelValue.layout.multiple && context.index === props.modelValue.data.length - 1,
77
- item: context.item.raw
51
+ item: getItems.prepareSelectedItem(context.item.raw, context.item.value)
78
52
  })
79
53
  }
80
54
  return slots