@apart-tech/jsonforms-kit
v2.0.0
Published
A headless, bring-your-own-components library for rendering JSON Forms with any React component library
Readme
@apart-tech/jsonforms-kit
A headless, bring-your-own-components library for rendering JSON Forms with any React component library. The library handles all the heavy lifting (renderer registration, tester logic, layout dispatching, i18n integration, conditional visibility) while allowing consumers to provide their own UI components.
Features
- Headless: No CSS or styling dependencies - bring your own components
- Type-safe: Full TypeScript support with component contracts
- Flexible: Works with shadcn/ui, MUI, Chakra UI, or any component library
- Comprehensive: Supports all common form controls out of the box
- Extensible: Easy to add custom controls and testers
Installation
npm install @apart-tech/jsonforms-kit @jsonforms/core @jsonforms/reactQuick Start
1. Create your component map
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
const componentMap = {
// Required components
Input: ({ value, onChange, type, placeholder, disabled, className, ...props }) => (
<Input
type={type}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={className}
{...props}
/>
),
Label: ({ children, className, htmlFor }) => (
<Label className={className} htmlFor={htmlFor}>{children}</Label>
),
// Optional components
Select: ({ value, onChange, options, placeholder, disabled, className }) => (
<Select value={value} onValueChange={onChange} disabled={disabled}>
<SelectTrigger className={className}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
),
Checkbox: ({ checked, onChange, disabled, className }) => (
<Checkbox
checked={checked}
onCheckedChange={onChange}
disabled={disabled}
className={className}
/>
),
// Layout components
VerticalLayout: ({ children, className }) => (
<div className={className || 'space-y-6'}>{children}</div>
),
HorizontalLayout: ({ children, className }) => (
<div className={className || 'flex gap-4 items-end'}>{children}</div>
),
Group: ({ label, children, className }) => (
<fieldset className={className || 'space-y-4 border rounded-lg p-4'}>
{label && <legend className="text-lg font-semibold">{label}</legend>}
{children}
</fieldset>
),
// Error display
ErrorMessage: ({ message, className }) => (
<p className={className || 'text-sm text-red-600 mt-1'}>{message}</p>
),
RequiredIndicator: ({ className }) => (
<span className={className || 'text-red-500 ml-1'}>*</span>
),
};2. Create renderers
import { createRenderers } from '@apart-tech/jsonforms-kit';
const renderers = createRenderers({
components: componentMap,
classNames: {
field: 'space-y-1',
label: 'text-sm font-medium',
error: 'text-sm text-red-600 mt-1',
required: 'text-red-500 ml-1',
layouts: {
vertical: 'space-y-6',
horizontal: 'flex gap-4 items-end',
group: 'border rounded-lg p-4',
},
},
});3. Use with JsonForms
import { useState } from 'react';
import { JsonForms } from '@jsonforms/react';
import { FormProvider } from '@apart-tech/jsonforms-kit';
function MyForm() {
const [data, setData] = useState({});
const schema = {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
email: { type: 'string', format: 'email', title: 'Email' },
role: {
type: 'string',
title: 'Role',
enum: ['admin', 'user', 'guest'],
},
},
required: ['name', 'email'],
};
return (
<FormProvider components={componentMap}>
<JsonForms
schema={schema}
data={data}
renderers={renderers}
onChange={({ data }) => setData(data || {})}
/>
</FormProvider>
);
}Component Contracts
Each component type has a defined prop interface:
Input
interface InputProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
placeholder?: string;
type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'number';
className?: string;
}Select
interface SelectProps {
value: string;
onChange: (value: string) => void;
options: Array<{ value: string; label: string }>;
placeholder?: string;
disabled?: boolean;
className?: string;
}Checkbox
interface CheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}See the types documentation for all component contracts.
Supported Controls
| Control | Schema Match | Component |
|---------|--------------|-----------|
| String | type: "string" | Input |
| Email | format: "email" | Input (type="email") |
| Password | format: "password" | Input (type="password") |
| URL | format: "uri" | Input (type="url") |
| Phone | format: "phone" or scope ends with phone | Input (type="tel") |
| Number | type: "number" or type: "integer" | Input (type="number") |
| Boolean | type: "boolean" | Checkbox |
| Toggle | type: "boolean" + options.format: "toggle" | Toggle |
| Select | enum or oneOf | Select |
| Radio | enum/boolean + options.format: "radio" | RadioGroup |
| Combobox | enum + options.format: "combobox" | Combobox |
| Date | format: "date" | DatePicker |
| DateTime | format: "date-time" | DateTimePicker |
| Textarea | string + options.multi: true | Textarea |
| Slider | number + options.format: "slider" | Slider |
| Currency | format: "currency" | Input (with formatting) |
| File | format: "file" or contentMediaType | FileUpload |
i18n Support
import { createI18n } from '@apart-tech/jsonforms-kit';
import { useTranslations } from 'next-intl';
function useFormI18n(namespace: string) {
const t = useTranslations(namespace);
const tErrors = useTranslations('formErrors');
return createI18n({
translate: (key, defaultValue) => {
const translated = t(key);
return translated !== key ? translated : defaultValue;
},
translateError: (error) => {
const translated = tErrors(error.keyword);
return translated !== error.keyword ? translated : error.message;
},
locale: 'en',
});
}
// Usage
function MyForm() {
const i18n = useFormI18n('forms.personalInfo');
return (
<JsonForms
// ...
i18n={i18n}
/>
);
}Custom Testers
Override default tester ranks or add custom testers:
import { createRenderers, rankWith, scopeEndsWith } from '@apart-tech/jsonforms-kit';
const renderers = createRenderers({
components: componentMap,
testers: {
// Override default ranks
ranks: {
email: 10, // Higher priority for email fields
select: 4,
},
// Add custom testers
custom: [
{
name: 'specialPhone',
tester: rankWith(10, scopeEndsWith('mobileNumber')),
renderer: 'Input',
props: { type: 'tel' },
},
],
},
});API Reference
createRenderers(config)
Creates JSON Forms renderer registry entries.
createI18n(config)
Creates a JSON Forms i18n configuration object.
FormProvider
React context provider for component map and class names.
Hooks
useComponents()- Access the component mapuseClassNames()- Access class name configurationuseFieldWrapper(props)- Common field wrapper logicuseHasComponent(name)- Check if a component is available
License
MIT
