JSPM

@apart-tech/jsonforms-kit

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 14
  • Score
    100M100P100Q61363F
  • License MIT

A headless, bring-your-own-components library for rendering JSON Forms with any React component library

Package Exports

  • @apart-tech/jsonforms-kit
  • @apart-tech/jsonforms-kit/renderers

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/react

Quick 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 map
  • useClassNames() - Access class name configuration
  • useFieldWrapper(props) - Common field wrapper logic
  • useHasComponent(name) - Check if a component is available

License

MIT