diff --git a/MIGRATION.md b/MIGRATION.md index f21c331cb4..b90e78f1e9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,6 +2,18 @@ ## Migration to JSON Forms 3.9 +### JSON Schema generation supports non-object root values + +`generateJsonSchema` now generates a schema matching the root value's actual type. In particular, arrays produce an array schema with an `items` schema, primitives produce their corresponding primitive schema, and `undefined` produces an empty schema. + +Previously all root values were handled as objects. For example, `generateJsonSchema([1, 2])` produced an object schema with properties named `"0"` and `"1"`; it now produces an array schema with an integer `items` schema. If you depend on the previous object-shaped output, adapt the generated schema before consuming it. + +### Default UI schema generation always returns a layout + +`generateDefaultUISchema` previously returned `null` when no UI schema elements could be generated. It now returns an empty layout of the requested type instead. + +If you call `generateDefaultUISchema` directly, remove any `null` handling and check the returned layout's `elements` when you need to know whether controls were generated. + ### Data update paths treat all segments literally Data updates (e.g. dispatched `update` actions) previously wrote to the form data via lodash's `set`/`unset`, which interpret bracket notation and array indices in paths. diff --git a/packages/core/src/generators/Generate.ts b/packages/core/src/generators/Generate.ts index ad1b17354d..4a8d9adebe 100644 --- a/packages/core/src/generators/Generate.ts +++ b/packages/core/src/generators/Generate.ts @@ -28,9 +28,7 @@ import { generateJsonSchema } from './schema'; import { createControlElement, generateDefaultUISchema } from './uischema'; export const Generate: { - // TODO fix @typescript-eslint/ban-types - // eslint-disable-next-line @typescript-eslint/ban-types - jsonSchema(instance: Object, options?: any): JsonSchema; + jsonSchema(instance: unknown, options?: any): JsonSchema; uiSchema( jsonSchema: JsonSchema, layoutType?: string, diff --git a/packages/core/src/generators/schema.ts b/packages/core/src/generators/schema.ts index f3b1d2324b..623966e97f 100644 --- a/packages/core/src/generators/schema.ts +++ b/packages/core/src/generators/schema.ts @@ -52,9 +52,7 @@ class Gen { private findOption: (props: Properties) => (optionName: string) => any ) {} - // TODO fix @typescript-eslint/ban-types - // eslint-disable-next-line @typescript-eslint/ban-types - schemaObject = (data: Object): JsonSchema4 => { + schemaObject = (data: Record): JsonSchema4 => { const props: Properties = this.properties(data); const schema: JsonSchema4 = { type: 'object', @@ -140,14 +138,12 @@ class Gen { /** * Generate a JSON schema based on the given data and any additional options. - * @param {Object} instance the data to create a JSON schema for + * @param {unknown} instance the data to create a JSON schema for * @param {any} options any additional options that may alter the generated JSON schema * @returns {JsonSchema} the generated schema */ export const generateJsonSchema = ( - // TODO fix @typescript-eslint/ban-types - // eslint-disable-next-line @typescript-eslint/ban-types - instance: Object, + instance: unknown, options: any = {} ): JsonSchema4 => { const findOption = @@ -177,5 +173,5 @@ export const generateJsonSchema = ( const gen = new Gen(findOption); - return gen.schemaObject(instance); + return gen.property(instance); }; diff --git a/packages/core/src/generators/uischema.ts b/packages/core/src/generators/uischema.ts index bb8fdec2f9..ff91662841 100644 --- a/packages/core/src/generators/uischema.ts +++ b/packages/core/src/generators/uischema.ts @@ -225,4 +225,4 @@ export const generateDefaultUISchema = ( wrapInLayoutIfNecessary( generateUISchema(jsonSchema, [], prefix, '', layoutType, rootSchema), layoutType - ); + ) ?? createLayout(layoutType); diff --git a/packages/core/test/generators/schema.test.ts b/packages/core/test/generators/schema.test.ts index 679defefa3..13ecf543b9 100644 --- a/packages/core/test/generators/schema.test.ts +++ b/packages/core/test/generators/schema.test.ts @@ -26,6 +26,38 @@ import test from 'ava'; import { generateJsonSchema } from '../../src/generators/schema'; +test('default schema generation root primitive types', (t) => { + t.deepEqual(generateJsonSchema(undefined), {}); + t.deepEqual(generateJsonSchema('hello'), { + type: 'string', + }); + t.deepEqual(generateJsonSchema(42), { + type: 'integer', + }); + t.deepEqual(generateJsonSchema(3.14), { + type: 'number', + }); + t.deepEqual(generateJsonSchema(true), { + type: 'boolean', + }); + t.deepEqual(generateJsonSchema(null), { + type: 'null', + }); +}); + +test('default schema generation root array types', (t) => { + t.deepEqual(generateJsonSchema([]), { + type: 'array', + items: {}, + }); + t.deepEqual(generateJsonSchema([1, 2]), { + type: 'array', + items: { + type: 'integer', + }, + }); +}); + test('default schema generation basic types', (t) => { const instance: any = { boolean: false, diff --git a/packages/core/test/generators/uischema.test.ts b/packages/core/test/generators/uischema.test.ts index 904f3b70b4..024c1e177f 100644 --- a/packages/core/test/generators/uischema.test.ts +++ b/packages/core/test/generators/uischema.test.ts @@ -496,19 +496,28 @@ test('generate unnamed array control w/o type', (t) => { test('generate for empty schema', (t) => { const schema: JsonSchema = {}; - const uischema: Layout = null; + const uischema: Layout = { + type: 'VerticalLayout', + elements: [], + }; t.deepEqual(generateDefaultUISchema(schema), uischema); }); test('generate for null schema', (t) => { const schema: JsonSchema = null; - const uischema: Layout = null; + const uischema: Layout = { + type: 'VerticalLayout', + elements: [], + }; t.deepEqual(generateDefaultUISchema(schema), uischema); }); test('generate for undefined schema', (t) => { const schema: JsonSchema = undefined; - const uischema: Layout = null; + const uischema: Layout = { + type: 'VerticalLayout', + elements: [], + }; t.deepEqual(generateDefaultUISchema(schema), uischema); }); diff --git a/packages/material-renderers/src/complex/CombinatorProperties.tsx b/packages/material-renderers/src/complex/CombinatorProperties.tsx index 74ec5e5aca..c7d5d4fee7 100644 --- a/packages/material-renderers/src/complex/CombinatorProperties.tsx +++ b/packages/material-renderers/src/complex/CombinatorProperties.tsx @@ -58,10 +58,8 @@ export class CombinatorProperties extends React.Component< undefined, rootSchema ); - let isLayoutWithElements = false; - if (foundUISchema !== null && isLayout(foundUISchema)) { - isLayoutWithElements = foundUISchema.elements.length > 0; - } + const isLayoutWithElements = + isLayout(foundUISchema) && foundUISchema.elements.length > 0; if (isLayoutWithElements) { return ( diff --git a/packages/vue-vanilla/src/complex/components/CombinatorProperties.vue b/packages/vue-vanilla/src/complex/components/CombinatorProperties.vue index 39849b724e..bd6309a680 100644 --- a/packages/vue-vanilla/src/complex/components/CombinatorProperties.vue +++ b/packages/vue-vanilla/src/complex/components/CombinatorProperties.vue @@ -59,10 +59,8 @@ export default defineComponent({ const isLayout = (uischema: UISchemaElement): uischema is Layout => Object.prototype.hasOwnProperty.call(uischema, 'elements'); - let isLayoutWithElements = false; - if (foundUISchema !== null && isLayout(foundUISchema)) { - isLayoutWithElements = foundUISchema.elements.length > 0; - } + const isLayoutWithElements = + isLayout(foundUISchema) && foundUISchema.elements.length > 0; return { otherProps, diff --git a/packages/vue-vuetify/dev/components/ExampleForm.vue b/packages/vue-vuetify/dev/components/ExampleForm.vue index 25a2e4f6bc..4036503c4d 100644 --- a/packages/vue-vuetify/dev/components/ExampleForm.vue +++ b/packages/vue-vuetify/dev/components/ExampleForm.vue @@ -50,12 +50,16 @@ const resolvedSchema = shallowReactive({ error: undefined, }); -const emits = defineEmits(['jsfchange']); +const emits = defineEmits(['jsfchange', 'update:data']); const onChange = (event: JsonFormsChangeEvent): void => { emits('jsfchange', event); }; +const onUpdateData = (data: any): void => { + emits('update:data', data); +}; + watch( () => props.state.schema, (schema) => { @@ -101,6 +105,7 @@ const properties = computed(() => ({ v-if="resolvedSchema.resolved && resolvedSchema.error === undefined" v-bind="properties" @change="onChange" + @update:data="onUpdateData" > { monaco.Uri.parse(toDataUri(props.example.name)), event.data !== undefined ? JSON.stringify(event.data, null, 2) : '', ); - state.data = event.data; } errors.value = event.errors; }; +const onDataChange = (data: any): void => { + state.data = data; +}; + const reloadMonacoSchema = () => { const example = find( examples, @@ -346,7 +349,11 @@ const handleAction = (action: Action) => { - + @@ -392,7 +399,12 @@ const handleAction = (action: Action) => { - + @@ -505,7 +517,11 @@ const handleAction = (action: Action) => {
- +
diff --git a/packages/vue-vuetify/src/complex/components/CombinatorProperties.vue b/packages/vue-vuetify/src/complex/components/CombinatorProperties.vue index 32c92723cf..4803ce351d 100644 --- a/packages/vue-vuetify/src/complex/components/CombinatorProperties.vue +++ b/packages/vue-vuetify/src/complex/components/CombinatorProperties.vue @@ -64,10 +64,8 @@ export default defineComponent({ const isLayout = (uischema: UISchemaElement): uischema is Layout => Object.prototype.hasOwnProperty.call(uischema, 'elements'); - let isLayoutWithElements = false; - if (foundUISchema !== null && isLayout(foundUISchema)) { - isLayoutWithElements = foundUISchema.elements.length > 0; - } + const isLayoutWithElements = + isLayout(foundUISchema) && foundUISchema.elements.length > 0; return { otherProps, diff --git a/packages/vue/README.md b/packages/vue/README.md index a2b4bf10fc..9f4933b6cb 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -18,7 +18,7 @@ Use the `json-forms` component for each form you want to render. Mandatory props: -- `data: any` - the data to show +- `data: any` - the controlled data to show. Use `v-model:data` to keep the parent state synchronized with form edits. When binding with `:data` instead, update the bound value from `update:data` or `change`; otherwise later prop updates can restore stale data. - `renderers: JsonFormsRendererRegistryEntry[]` - the Vue renderer set to use Optional props: @@ -37,12 +37,13 @@ Optional props: Events: - `change: {data: any; errors: AJVError[]}` - Whenever data and/or errors change this event is emitted. +- `update:data: any` - Emits the current data alongside `change`, enabling `v-model:data`. Example: ```html