@djangocfg/ui-tools 2.1.320 → 2.1.322
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/JsonSchemaForm-OSPUUUHM.cjs +13 -0
- package/dist/{JsonSchemaForm-IIYKSH6X.cjs.map → JsonSchemaForm-OSPUUUHM.cjs.map} +1 -1
- package/dist/JsonSchemaForm-TSLX2GRO.mjs +4 -0
- package/dist/{JsonSchemaForm-RN3XWSWX.mjs.map → JsonSchemaForm-TSLX2GRO.mjs.map} +1 -1
- package/dist/{chunk-L37FZYJU.cjs → chunk-4IW7GZFQ.cjs} +189 -74
- package/dist/chunk-4IW7GZFQ.cjs.map +1 -0
- package/dist/{chunk-JUGQNNDC.mjs → chunk-EXGXUK2N.mjs} +190 -76
- package/dist/chunk-EXGXUK2N.mjs.map +1 -0
- package/dist/index.cjs +28 -24
- package/dist/index.d.cts +240 -206
- package/dist/index.d.ts +240 -206
- package/dist/index.mjs +2 -2
- package/package.json +6 -6
- package/src/index.ts +15 -0
- package/src/tools/JsonForm/JsonForm.story.tsx +217 -1
- package/src/tools/JsonForm/JsonSchemaForm.tsx +15 -4
- package/src/tools/JsonForm/README.md +268 -0
- package/src/tools/JsonForm/index.ts +22 -1
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +28 -5
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +110 -3
- package/src/tools/JsonForm/types.ts +37 -5
- package/src/tools/JsonForm/utils.ts +25 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +6 -11
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +20 -12
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +9 -5
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +6 -10
- package/src/tools/JsonForm/widgets/_useWidgetEnv.ts +43 -0
- package/dist/JsonSchemaForm-IIYKSH6X.cjs +0 -13
- package/dist/JsonSchemaForm-RN3XWSWX.mjs +0 -4
- package/dist/chunk-JUGQNNDC.mjs.map +0 -1
- package/dist/chunk-L37FZYJU.cjs.map +0 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { JsonSchemaForm } from './index';
|
|
3
4
|
|
|
4
5
|
export default defineStory({
|
|
5
6
|
title: 'Tools/Json Schema Form',
|
|
6
7
|
component: JsonSchemaForm,
|
|
7
|
-
description:
|
|
8
|
+
description:
|
|
9
|
+
'Dynamic form generator from JSON Schema using react-jsonschema-form. Supports density (compact), conditional disable via `ui:disabledWhen`, and collapsible sub-sections via `ui:groups`.',
|
|
8
10
|
});
|
|
9
11
|
|
|
10
12
|
const SCHEMAS = {
|
|
@@ -77,6 +79,13 @@ export const Interactive = () => {
|
|
|
77
79
|
description: 'Select form schema',
|
|
78
80
|
});
|
|
79
81
|
|
|
82
|
+
const [density] = useSelect('density', {
|
|
83
|
+
options: ['comfortable', 'compact'] as const,
|
|
84
|
+
defaultValue: 'comfortable',
|
|
85
|
+
label: 'Density',
|
|
86
|
+
description: '`compact` shrinks rows and moves description into label tooltip.',
|
|
87
|
+
});
|
|
88
|
+
|
|
80
89
|
const [liveValidate] = useBoolean('liveValidate', {
|
|
81
90
|
defaultValue: false,
|
|
82
91
|
label: 'Live Validate',
|
|
@@ -91,6 +100,7 @@ export const Interactive = () => {
|
|
|
91
100
|
schema={config.schema}
|
|
92
101
|
uiSchema={config.uiSchema}
|
|
93
102
|
liveValidate={liveValidate}
|
|
103
|
+
density={density}
|
|
94
104
|
onSubmit={(data) => console.log('Submitted:', data.formData)}
|
|
95
105
|
/>
|
|
96
106
|
</div>
|
|
@@ -132,3 +142,209 @@ export const WithDefaultValues = () => (
|
|
|
132
142
|
/>
|
|
133
143
|
</div>
|
|
134
144
|
);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Compact density — shrinks rows, moves description into a tooltip on the
|
|
148
|
+
* label. Pair with `ui:groups` for dense sidebar / playground configurators.
|
|
149
|
+
*/
|
|
150
|
+
export const Compact = () => (
|
|
151
|
+
<div className="max-w-md">
|
|
152
|
+
<JsonSchemaForm
|
|
153
|
+
schema={SCHEMAS.simple.schema}
|
|
154
|
+
density="compact"
|
|
155
|
+
showSubmitButton={false}
|
|
156
|
+
onSubmit={(data) => console.log('Submitted:', data.formData)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `ui:disabledWhen` greys out a field based on another field's value.
|
|
163
|
+
* Here `phone` is disabled until `subscribe` is checked, and `email` is
|
|
164
|
+
* disabled when `firstName` is empty.
|
|
165
|
+
*/
|
|
166
|
+
export const ConditionalDisable = () => {
|
|
167
|
+
const [data, setData] = useState<Record<string, unknown>>({
|
|
168
|
+
firstName: '',
|
|
169
|
+
subscribe: false,
|
|
170
|
+
});
|
|
171
|
+
return (
|
|
172
|
+
<div className="max-w-lg">
|
|
173
|
+
<JsonSchemaForm
|
|
174
|
+
schema={SCHEMAS.contact.schema}
|
|
175
|
+
uiSchema={{
|
|
176
|
+
...SCHEMAS.contact.uiSchema,
|
|
177
|
+
email: {
|
|
178
|
+
'ui:disabledWhen': { path: 'firstName', falsy: true },
|
|
179
|
+
},
|
|
180
|
+
phone: {
|
|
181
|
+
'ui:disabledWhen': { path: 'subscribe', falsy: true },
|
|
182
|
+
},
|
|
183
|
+
subscribe: { 'ui:widget': 'switch' },
|
|
184
|
+
}}
|
|
185
|
+
formData={data}
|
|
186
|
+
onChange={(e) => setData(e.formData ?? {})}
|
|
187
|
+
showSubmitButton={false}
|
|
188
|
+
/>
|
|
189
|
+
<pre className="mt-3 text-[11px] text-muted-foreground">
|
|
190
|
+
{JSON.stringify(data, null, 2)}
|
|
191
|
+
</pre>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `ui:groups` splits a flat object into collapsible sub-sections without
|
|
198
|
+
* restructuring the schema. Fields not listed in any group render flat above
|
|
199
|
+
* the groups.
|
|
200
|
+
*/
|
|
201
|
+
export const CollapsibleGroups = () => (
|
|
202
|
+
<div className="max-w-lg">
|
|
203
|
+
<JsonSchemaForm
|
|
204
|
+
schema={{
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
identity: { type: 'string', title: 'Display name' },
|
|
208
|
+
firstName: { type: 'string', title: 'First name' },
|
|
209
|
+
lastName: { type: 'string', title: 'Last name' },
|
|
210
|
+
email: { type: 'string', title: 'Email', format: 'email' },
|
|
211
|
+
phone: { type: 'string', title: 'Phone' },
|
|
212
|
+
newsletter: { type: 'boolean', title: 'Newsletter' },
|
|
213
|
+
marketing: { type: 'boolean', title: 'Marketing emails' },
|
|
214
|
+
tracking: { type: 'boolean', title: 'Usage analytics' },
|
|
215
|
+
},
|
|
216
|
+
}}
|
|
217
|
+
uiSchema={{
|
|
218
|
+
'ui:groups': [
|
|
219
|
+
{
|
|
220
|
+
title: 'Personal',
|
|
221
|
+
fields: ['firstName', 'lastName'],
|
|
222
|
+
defaultOpen: true,
|
|
223
|
+
},
|
|
224
|
+
{ title: 'Contact', fields: ['email', 'phone'], defaultOpen: false },
|
|
225
|
+
{
|
|
226
|
+
title: 'Preferences',
|
|
227
|
+
fields: ['newsletter', 'marketing', 'tracking'],
|
|
228
|
+
defaultOpen: false,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
newsletter: { 'ui:widget': 'switch' },
|
|
232
|
+
marketing: { 'ui:widget': 'switch' },
|
|
233
|
+
tracking: { 'ui:widget': 'switch' },
|
|
234
|
+
}}
|
|
235
|
+
density="compact"
|
|
236
|
+
showSubmitButton={false}
|
|
237
|
+
onChange={(e) => console.log('changed:', e.formData)}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Empty-string enum values are now allowed — Radix `<Select.Item value="" />`
|
|
244
|
+
* is reserved, so the ui-core `Select` wrapper substitutes a sentinel
|
|
245
|
+
* internally. From the schema's perspective `''` is just another option.
|
|
246
|
+
*/
|
|
247
|
+
export const EmptyStringSelect = () => {
|
|
248
|
+
const [data, setData] = useState<Record<string, unknown>>({ plan: '' });
|
|
249
|
+
return (
|
|
250
|
+
<div className="max-w-md">
|
|
251
|
+
<JsonSchemaForm
|
|
252
|
+
schema={{
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
plan: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
title: 'Plan',
|
|
258
|
+
enum: ['', 'Free', 'Pro', 'Max', 'Enterprise'],
|
|
259
|
+
description: 'An empty string is a valid enum value here.',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
}}
|
|
263
|
+
uiSchema={{
|
|
264
|
+
plan: {
|
|
265
|
+
'ui:enumNames': ['— none —', 'Free', 'Pro', 'Max', 'Enterprise'],
|
|
266
|
+
},
|
|
267
|
+
}}
|
|
268
|
+
formData={data}
|
|
269
|
+
onChange={(e) => setData(e.formData ?? {})}
|
|
270
|
+
showSubmitButton={false}
|
|
271
|
+
/>
|
|
272
|
+
<pre className="mt-3 text-[11px] text-muted-foreground">
|
|
273
|
+
{JSON.stringify(data, null, 2)}
|
|
274
|
+
</pre>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Live demo of all extensions wired together — compact density, collapsible
|
|
281
|
+
* groups, conditional disable, slider with unit. Mirrors what the layouts
|
|
282
|
+
* playground sidebar uses.
|
|
283
|
+
*/
|
|
284
|
+
export const PlaygroundStyle = () => {
|
|
285
|
+
const [data, setData] = useState<Record<string, unknown>>({
|
|
286
|
+
shell: { variant: 'boxed', inset: 12, radius: '2xl', border: true },
|
|
287
|
+
sidebar: {
|
|
288
|
+
density: 'default',
|
|
289
|
+
activeIndicator: 'background',
|
|
290
|
+
collapsibleGroups: false,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
return (
|
|
294
|
+
<div className="max-w-md">
|
|
295
|
+
<JsonSchemaForm
|
|
296
|
+
schema={{
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
shell: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
title: 'Shell',
|
|
302
|
+
properties: {
|
|
303
|
+
variant: { type: 'string', title: 'Variant', enum: ['full-bleed', 'boxed'] },
|
|
304
|
+
inset: { type: 'integer', title: 'Inset', minimum: 0, maximum: 32 },
|
|
305
|
+
radius: { type: 'string', title: 'Radius', enum: ['sm', 'md', 'lg', 'xl', '2xl', '3xl'] },
|
|
306
|
+
border: { type: 'boolean', title: 'Border' },
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
sidebar: {
|
|
310
|
+
type: 'object',
|
|
311
|
+
title: 'Sidebar',
|
|
312
|
+
properties: {
|
|
313
|
+
density: { type: 'string', title: 'Density', enum: ['sparse', 'default', 'dense'] },
|
|
314
|
+
activeIndicator: {
|
|
315
|
+
type: 'string',
|
|
316
|
+
title: 'Active indicator',
|
|
317
|
+
enum: ['background', 'rail', 'both'],
|
|
318
|
+
},
|
|
319
|
+
collapsibleGroups: { type: 'boolean', title: 'Collapsible groups' },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
}}
|
|
324
|
+
uiSchema={{
|
|
325
|
+
shell: {
|
|
326
|
+
'ui:collapsible': true,
|
|
327
|
+
inset: {
|
|
328
|
+
'ui:widget': 'slider',
|
|
329
|
+
'ui:options': { unit: 'px', step: 2, showInput: false },
|
|
330
|
+
'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
|
|
331
|
+
},
|
|
332
|
+
radius: { 'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' } },
|
|
333
|
+
border: {
|
|
334
|
+
'ui:widget': 'switch',
|
|
335
|
+
'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
sidebar: {
|
|
339
|
+
'ui:collapsible': true,
|
|
340
|
+
collapsibleGroups: { 'ui:widget': 'switch' },
|
|
341
|
+
},
|
|
342
|
+
}}
|
|
343
|
+
formData={data}
|
|
344
|
+
onChange={(e) => setData(e.formData ?? {})}
|
|
345
|
+
density="compact"
|
|
346
|
+
showSubmitButton={false}
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
};
|
|
@@ -7,14 +7,14 @@ import React, { useCallback, useMemo } from 'react';
|
|
|
7
7
|
|
|
8
8
|
import { Alert, AlertDescription, Button } from '@djangocfg/ui-core/components';
|
|
9
9
|
import Form from '@rjsf/core';
|
|
10
|
-
import { RegistryWidgetsType } from '@rjsf/utils';
|
|
10
|
+
import { RegistryWidgetsType, UiSchema } from '@rjsf/utils';
|
|
11
11
|
import validator from '@rjsf/validator-ajv8';
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
ArrayFieldItemTemplate, ArrayFieldTemplate, BaseInputTemplate, ErrorListTemplate, FieldTemplate,
|
|
15
15
|
ObjectFieldTemplate
|
|
16
16
|
} from './templates';
|
|
17
|
-
import { JsonSchemaFormProps } from './types';
|
|
17
|
+
import { JsonFormContext, JsonSchemaFormProps } from './types';
|
|
18
18
|
import { normalizeFormData, validateSchema } from './utils';
|
|
19
19
|
import {
|
|
20
20
|
CheckboxWidget, ColorWidget, NumberWidget, SelectWidget, SliderWidget, SwitchWidget, TextWidget
|
|
@@ -60,6 +60,8 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
|
|
|
60
60
|
className,
|
|
61
61
|
showSubmitButton = true,
|
|
62
62
|
submitButtonText = 'Submit',
|
|
63
|
+
density = 'comfortable',
|
|
64
|
+
formContext: callerFormContext,
|
|
63
65
|
...restProps
|
|
64
66
|
} = props;
|
|
65
67
|
|
|
@@ -167,15 +169,24 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
|
|
|
167
169
|
);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
// formContext threads the latest formData + density into widgets/templates so
|
|
173
|
+
// they can react to global form state (conditional disable, density CSS).
|
|
174
|
+
const formContext = useMemo<JsonFormContext & Record<string, unknown>>(() => ({
|
|
175
|
+
...(callerFormContext as object | undefined),
|
|
176
|
+
density,
|
|
177
|
+
formData: normalizedFormData,
|
|
178
|
+
}), [callerFormContext, density, normalizedFormData]);
|
|
179
|
+
|
|
170
180
|
return (
|
|
171
|
-
<div className={className}>
|
|
181
|
+
<div className={className} data-jsonform-density={density}>
|
|
172
182
|
<Form
|
|
173
183
|
schema={validatedSchema}
|
|
174
|
-
uiSchema={uiSchema}
|
|
184
|
+
uiSchema={uiSchema as UiSchema | undefined}
|
|
175
185
|
formData={normalizedFormData}
|
|
176
186
|
validator={validator}
|
|
177
187
|
widgets={widgets}
|
|
178
188
|
templates={templates}
|
|
189
|
+
formContext={formContext}
|
|
179
190
|
onSubmit={handleSubmit}
|
|
180
191
|
onChange={handleChange}
|
|
181
192
|
onError={handleError}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# JsonForm
|
|
2
|
+
|
|
3
|
+
Schema-driven forms on top of [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) (RJSF) with `@djangocfg/ui` widgets, AJV8 validation, and a few custom extensions for compact playground-style UIs.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { JsonSchemaForm } from '@djangocfg/ui-tools/json-form';
|
|
7
|
+
// or, for code-splitting:
|
|
8
|
+
import { LazyJsonSchemaForm } from '@djangocfg/ui-tools';
|
|
9
|
+
|
|
10
|
+
const schema = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
name: { type: 'string', title: 'Name' },
|
|
14
|
+
age: { type: 'integer', title: 'Age', minimum: 0 },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
<JsonSchemaForm schema={schema} onSubmit={(e) => console.log(e.formData)} />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Components
|
|
24
|
+
|
|
25
|
+
| Export | Use |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `JsonSchemaForm` | Direct (synchronous) form. |
|
|
28
|
+
| `LazyJsonSchemaForm` | Same API, lazy-loaded with a `<CardLoadingFallback>` (RJSF is ~300KB). |
|
|
29
|
+
|
|
30
|
+
Both accept the same `JsonSchemaFormProps`:
|
|
31
|
+
|
|
32
|
+
| Prop | Type | Notes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `schema` | `RJSFSchema` | Required. JSON Schema 7. |
|
|
35
|
+
| `uiSchema` | `UiSchema` | Optional. Widget overrides + custom extensions (see below). |
|
|
36
|
+
| `formData` | `T` | Initial / controlled data. |
|
|
37
|
+
| `onChange` | `(e) => void` | Live changes. |
|
|
38
|
+
| `onSubmit` | `(e) => void` | Submit (enter or button). |
|
|
39
|
+
| `onError` | `(errors) => void` | Validation errors. |
|
|
40
|
+
| `density` | `'comfortable' \| 'compact'` | Default `'comfortable'`. See **Density** below. |
|
|
41
|
+
| `liveValidate` | `boolean` | Default `false`. |
|
|
42
|
+
| `disabled` / `readonly` | `boolean` | Form-wide. |
|
|
43
|
+
| `showSubmitButton` | `boolean` | Default `true`. |
|
|
44
|
+
| `submitButtonText` | `string` | Default `'Submit'`. |
|
|
45
|
+
| `showErrorList` | `false \| 'top' \| 'bottom'` | Default `'top'`. |
|
|
46
|
+
| `className` | `string` | Wrapper class. |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Built-in widgets
|
|
51
|
+
|
|
52
|
+
Mapped via `widgets={{ ... }}` and registered under both PascalCase and lowercase aliases — use whichever matches your `uiSchema`.
|
|
53
|
+
|
|
54
|
+
| `ui:widget` | Widget | Built on |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `text` | `TextWidget` | `<Input />` |
|
|
57
|
+
| `number` | `NumberWidget` | `<Input type="number" />` |
|
|
58
|
+
| `checkbox` | `CheckboxWidget` | `<Checkbox />` |
|
|
59
|
+
| `switch` | `SwitchWidget` | `<Switch />` |
|
|
60
|
+
| `select` | `SelectWidget` | `<Select />` (auto-falls back to `<input type="text">` when no enum) |
|
|
61
|
+
| `slider` / `range` | `SliderWidget` | `<Slider />` (+ optional inline numeric input, supports unit suffix) |
|
|
62
|
+
| `color` | `ColorWidget` | Native colour picker. |
|
|
63
|
+
|
|
64
|
+
### Slider options
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
'ui:widget': 'slider',
|
|
68
|
+
'ui:options': {
|
|
69
|
+
unit: 'px', // appended to the displayed value: "12px"
|
|
70
|
+
step: 2,
|
|
71
|
+
showInput: false, // hide the editable text input next to the slider
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Templates
|
|
78
|
+
|
|
79
|
+
Custom RJSF templates live in `./templates/`:
|
|
80
|
+
|
|
81
|
+
- `FieldTemplate` — label + body + errors. Honors `density`.
|
|
82
|
+
- `ObjectFieldTemplate` — object body. Supports `ui:collapsible`, `ui:grid`, `ui:className`, and **`ui:groups`** (see below).
|
|
83
|
+
- `ArrayFieldTemplate`, `ArrayFieldItemTemplate` — array editing.
|
|
84
|
+
- `ErrorListTemplate` — top/bottom error block.
|
|
85
|
+
- `BaseInputTemplate` — fallback input wrapper.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Custom extensions
|
|
90
|
+
|
|
91
|
+
These are extra `uiSchema` keys this fork understands on top of stock RJSF.
|
|
92
|
+
|
|
93
|
+
### `density`
|
|
94
|
+
|
|
95
|
+
Form-wide compact mode. Drives label sizing, row spacing, and tooltip placement.
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<JsonSchemaForm density="compact" ... />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
In compact mode:
|
|
102
|
+
- Labels: 10px uppercase muted, no description paragraph
|
|
103
|
+
- Description (`schema.description` or `ui:description`) becomes a `title=` tooltip on the label
|
|
104
|
+
- Tighter row spacing (`space-y-1` vs `space-y-2`)
|
|
105
|
+
- Slimmer select trigger (`h-7`) and inputs
|
|
106
|
+
|
|
107
|
+
Use this for sidebars, settings drawers, dense playground configurators.
|
|
108
|
+
|
|
109
|
+
### `ui:disabledWhen`
|
|
110
|
+
|
|
111
|
+
Declarative conditional disable, evaluated against the current `formData`. Avoids the JSON Schema `dependencies` boilerplate when you just want to grey a field out.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const uiSchema = {
|
|
115
|
+
shell: {
|
|
116
|
+
inset: {
|
|
117
|
+
'ui:widget': 'slider',
|
|
118
|
+
'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Supported rule shapes:
|
|
125
|
+
|
|
126
|
+
| Shape | Disabled when |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `{ path, eq: value }` | field at `path` equals `value` |
|
|
129
|
+
| `{ path, notEq: value }` | field at `path` does not equal `value` |
|
|
130
|
+
| `{ path, in: [...] }` | field is in array |
|
|
131
|
+
| `{ path, notIn: [...] }` | field is not in array |
|
|
132
|
+
| `{ path, truthy: true }` | field is truthy |
|
|
133
|
+
| `{ path, falsy: true }` | field is falsy / empty / `false` / `0` |
|
|
134
|
+
|
|
135
|
+
`path` is dot-separated against `formData` root (e.g. `'shell.variant'`, `'navbar.controls.showThemeSwitcher'`).
|
|
136
|
+
|
|
137
|
+
The runtime `evaluateDisabledWhen(rule, formData)` is exported for tests / reuse.
|
|
138
|
+
|
|
139
|
+
### `ui:groups` (in `ObjectFieldTemplate`)
|
|
140
|
+
|
|
141
|
+
Splits a flat object into collapsible sub-sections without restructuring the schema.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
sidebar: {
|
|
145
|
+
'ui:groups': [
|
|
146
|
+
{ title: 'Density', fields: ['density'], defaultOpen: true },
|
|
147
|
+
{ title: 'Slots', fields: ['showMenuStart', 'showMenuEnd'], defaultOpen: false },
|
|
148
|
+
{ title: 'Visual', fields: ['activeIndicator', 'groupLabelStyle'] },
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Fields not listed in any group render flat above the groups. Each group is a `<Collapsible>` with a chevron-right header.
|
|
154
|
+
|
|
155
|
+
### `ui:collapsible` / `ui:collapsed` (existing)
|
|
156
|
+
|
|
157
|
+
Wraps the entire object in a single collapsible card. Independent of `ui:groups` — use one or the other on a given object.
|
|
158
|
+
|
|
159
|
+
### `ui:grid` (existing)
|
|
160
|
+
|
|
161
|
+
`'ui:grid': 2` switches the object body to a 2-column grid (`gap-4`). Default is vertical stack.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Example — playground sidebar
|
|
166
|
+
|
|
167
|
+
The `apps/demo` layouts playground uses the stack end-to-end:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// schema
|
|
171
|
+
const privateSchema: RJSFSchema = {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
shell: { type: 'object', properties: { variant: { type: 'string', enum: ['full-bleed', 'boxed'] }, inset: { type: 'integer', minimum: 0, maximum: 32 } } },
|
|
175
|
+
sidebar: { type: 'object', properties: { /* ... */ } },
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// uiSchema
|
|
180
|
+
const privateUiSchema: UiSchema = {
|
|
181
|
+
shell: {
|
|
182
|
+
'ui:collapsible': true,
|
|
183
|
+
inset: {
|
|
184
|
+
'ui:widget': 'slider',
|
|
185
|
+
'ui:options': { unit: 'px', step: 2, showInput: false },
|
|
186
|
+
'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
sidebar: {
|
|
190
|
+
'ui:collapsible': true,
|
|
191
|
+
'ui:groups': [
|
|
192
|
+
{ title: 'Density', fields: ['density'], defaultOpen: true },
|
|
193
|
+
{ title: 'Visual', fields: ['activeIndicator', 'groupLabelStyle'] },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// component
|
|
199
|
+
<LazyJsonSchemaForm
|
|
200
|
+
schema={privateSchema}
|
|
201
|
+
uiSchema={privateUiSchema}
|
|
202
|
+
formData={config}
|
|
203
|
+
onChange={(e) => setConfig(e.formData)}
|
|
204
|
+
density="compact"
|
|
205
|
+
showSubmitButton={false}
|
|
206
|
+
liveValidate={false}
|
|
207
|
+
/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
See `apps/demo/.../layouts/sidebar/{private,public}/Sidebar.tsx` for the full integration.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Internals
|
|
215
|
+
|
|
216
|
+
| File | Role |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `JsonSchemaForm.tsx` | Top-level component. Validates schema, normalises `formData`, threads `formContext = { density, formData }` to widgets/templates. |
|
|
219
|
+
| `widgets/_useWidgetEnv.ts` | Internal hook reading density + `ui:disabledWhen` from `formContext`. Used by `Switch`, `Select`, `Slider`, `Checkbox`. |
|
|
220
|
+
| `templates/ObjectFieldTemplate.tsx` | Implements `ui:collapsible`, `ui:grid`, `ui:groups`. |
|
|
221
|
+
| `templates/FieldTemplate.tsx` | Density-aware label / description / errors layout. |
|
|
222
|
+
| `utils.ts` | `validateSchema`, `normalizeFormData`, `evaluateDisabledWhen`, plus default-merging helpers. |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Re-exported types
|
|
227
|
+
|
|
228
|
+
For consumers that don't have `@rjsf/utils` as a direct dependency:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import type {
|
|
232
|
+
RJSFSchema,
|
|
233
|
+
UiSchema,
|
|
234
|
+
JsonSchemaFormProps,
|
|
235
|
+
JsonFormDensity,
|
|
236
|
+
DisabledWhenRule,
|
|
237
|
+
UiGroup,
|
|
238
|
+
JsonFormContext,
|
|
239
|
+
} from '@djangocfg/ui-tools';
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Portable schema types (`CustomJson*`)
|
|
243
|
+
|
|
244
|
+
For packages that **ship a configurator schema** without taking a runtime
|
|
245
|
+
dependency on RJSF — typically library packages like `@djangocfg/layouts/configurator`
|
|
246
|
+
— use the portable types from `@djangocfg/ui-core/lib`:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import type {
|
|
250
|
+
CustomJsonSchema7,
|
|
251
|
+
CustomJsonSchema7Type,
|
|
252
|
+
CustomJsonUiSchema7,
|
|
253
|
+
CustomJsonUiGroup,
|
|
254
|
+
CustomJsonUiDisabledWhenRule,
|
|
255
|
+
} from '@djangocfg/ui-core/lib';
|
|
256
|
+
|
|
257
|
+
// also re-exported from `@djangocfg/ui-tools` for convenience.
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
These are our own Draft 7-shaped subset (not a copy of RJSF's types) and
|
|
261
|
+
`<JsonSchemaForm>` accepts them directly via union with `RJSFSchema`. Library
|
|
262
|
+
packages typing their schemas with `CustomJsonSchema7` stay decoupled from
|
|
263
|
+
the form framework — if RJSF is ever swapped out, these schemas don't need
|
|
264
|
+
to change.
|
|
265
|
+
|
|
266
|
+
The aliases `DisabledWhenRule` and `UiGroup` exported from `@djangocfg/ui-tools`
|
|
267
|
+
point at `CustomJsonUiDisabledWhenRule` / `CustomJsonUiGroup` respectively
|
|
268
|
+
(kept for back-compat with existing consumers).
|
|
@@ -34,7 +34,28 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
export { JsonSchemaForm } from './JsonSchemaForm';
|
|
37
|
-
export type {
|
|
37
|
+
export type {
|
|
38
|
+
JsonSchemaFormProps,
|
|
39
|
+
JsonFormDensity,
|
|
40
|
+
DisabledWhenRule,
|
|
41
|
+
UiGroup,
|
|
42
|
+
JsonFormContext,
|
|
43
|
+
} from './types';
|
|
44
|
+
export { evaluateDisabledWhen } from './utils';
|
|
45
|
+
|
|
46
|
+
// Re-export RJSF schema types so consumers don't need to install `@rjsf/utils`
|
|
47
|
+
// directly. Keeps ui-tools the single dependency for schema-driven forms.
|
|
48
|
+
export type { RJSFSchema, UiSchema } from '@rjsf/utils';
|
|
49
|
+
|
|
50
|
+
// Re-export the portable schema types from ui-core for convenience: most
|
|
51
|
+
// configurator authors only need these (no RJSF coupling).
|
|
52
|
+
export type {
|
|
53
|
+
CustomJsonSchema7,
|
|
54
|
+
CustomJsonSchema7Type,
|
|
55
|
+
CustomJsonUiSchema7,
|
|
56
|
+
CustomJsonUiGroup,
|
|
57
|
+
CustomJsonUiDisabledWhenRule,
|
|
58
|
+
} from '@djangocfg/ui-core/lib';
|
|
38
59
|
|
|
39
60
|
// Export widgets for custom usage
|
|
40
61
|
export * from './widgets';
|
|
@@ -6,6 +6,8 @@ import { Label } from '@djangocfg/ui-core/components';
|
|
|
6
6
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
7
|
import { FieldTemplateProps } from '@rjsf/utils';
|
|
8
8
|
|
|
9
|
+
import type { JsonFormDensity } from '../types';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Field template for JSON Schema Form
|
|
11
13
|
* Controls the layout and styling of individual form fields
|
|
@@ -25,36 +27,57 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|
|
25
27
|
hidden,
|
|
26
28
|
rawErrors,
|
|
27
29
|
} = props;
|
|
30
|
+
const formContext = (props as { formContext?: { density?: JsonFormDensity } }).formContext;
|
|
28
31
|
|
|
29
32
|
if (hidden) {
|
|
30
33
|
return <div className="hidden">{children}</div>;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
const hasError = rawErrors && rawErrors.length > 0;
|
|
37
|
+
const density = (formContext?.density as JsonFormDensity | undefined) ?? 'comfortable';
|
|
38
|
+
const compact = density === 'compact';
|
|
39
|
+
|
|
40
|
+
// In compact mode the description is suppressed in the body and forwarded to
|
|
41
|
+
// the label `title=` tooltip — keeps rows dense without losing context.
|
|
42
|
+
const descriptionText =
|
|
43
|
+
typeof description === 'string'
|
|
44
|
+
? description
|
|
45
|
+
: undefined;
|
|
46
|
+
const labelTitle = compact ? descriptionText : undefined;
|
|
47
|
+
const showDescription = !compact && Boolean(description);
|
|
34
48
|
|
|
35
49
|
return (
|
|
36
50
|
<div
|
|
37
|
-
className={cn('space-y-2', classNames)}
|
|
51
|
+
className={cn(compact ? 'space-y-1' : 'space-y-2', classNames)}
|
|
38
52
|
style={style}
|
|
39
53
|
>
|
|
40
54
|
{displayLabel && label && (
|
|
41
|
-
<Label
|
|
55
|
+
<Label
|
|
56
|
+
htmlFor={id}
|
|
57
|
+
className={cn(
|
|
58
|
+
hasError && 'text-destructive',
|
|
59
|
+
compact && 'text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70',
|
|
60
|
+
)}
|
|
61
|
+
title={labelTitle}
|
|
62
|
+
>
|
|
42
63
|
{label}
|
|
43
64
|
{required && <span className="text-destructive ml-1">*</span>}
|
|
44
65
|
</Label>
|
|
45
66
|
)}
|
|
46
67
|
|
|
47
|
-
{
|
|
68
|
+
{showDescription && (
|
|
48
69
|
<div className="text-sm text-muted-foreground">{description}</div>
|
|
49
70
|
)}
|
|
50
71
|
|
|
51
72
|
<div>{children}</div>
|
|
52
73
|
|
|
53
74
|
{errors && (
|
|
54
|
-
<div className=
|
|
75
|
+
<div className={compact ? 'text-xs text-destructive' : 'text-sm text-destructive'}>
|
|
76
|
+
{errors}
|
|
77
|
+
</div>
|
|
55
78
|
)}
|
|
56
79
|
|
|
57
|
-
{help && (
|
|
80
|
+
{help && !compact && (
|
|
58
81
|
<div className="text-sm text-muted-foreground">{help}</div>
|
|
59
82
|
)}
|
|
60
83
|
</div>
|