npm.io
1.4.0 • Published yesterday

mui-schema-form-builder

Licence
MIT
Version
1.4.0
Deps
0
Size
339 kB
Vulns
0
Weekly
0
Stars
1

mui-schema-form-builder

Schema-driven, type-safe form builder for MUI + React Hook Form + Zod

Generate complex, production-ready forms from a plain JSON config. No boilerplate. No manual register calls. Full TypeScript inference from your Zod schema through to your onSubmit handler.


Features

  • Zero-config forms — one fields array, one schema, done
  • Type-safe submitonSubmit data is fully typed from your Zod schema
  • MUI-native — built on @mui/material v9, not bolted on
  • Password inputFIELD_TYPE.PASSWORD with a built-in show/hide toggle (no icon library needed)
  • Input adornmentsstartAdornment / endAdornment on TEXT and NUMBER fields for prefixes, suffixes, and icons
  • Form title — optional heading with alignment (titleAlign) and placement (titlePosition) control
  • Custom action buttons — replace Submit/Cancel/Reset with your own layout via renderActions
  • Multi-step wizardFormWizard with per-step validation, completed-step navigation, and submit-error navigation
  • Async autocomplete — debounced fetch with built-in stale-response protection
  • Conditional fields — hide/show fields based on other field values
  • Performance — fields without visibleIf never re-render on sibling changes
  • Accessible — proper <label htmlFor>, aria-required, aria-invalid, aria-describedby
  • Virtualization — optional react-window support for 50+ field forms

Installation

npm install mui-schema-form-builder

Peer dependencies (install these if you don't have them):

npm install react react-dom @mui/material @emotion/react @emotion/styled \
            react-hook-form @hookform/resolvers zod

Optional (only needed when virtualize={true}):

npm install react-window

Quick Start

import { z } from 'zod';
import { FormBuilder, FIELD_TYPE } from 'mui-schema-form-builder';
import { ThemeProvider, createTheme } from '@mui/material';

const schema = z.object({
  name: z.string().min(2, 'Name is too short'),
  email: z.string().email('Invalid email'),
  age: z.number().min(18, 'Must be 18+'),
});

const fields = [
  { name: 'name', label: 'Full Name', type: FIELD_TYPE.TEXT, required: true },
  { name: 'email', label: 'Email Address', type: FIELD_TYPE.TEXT, required: true },
  { name: 'age', label: 'Age', type: FIELD_TYPE.NUMBER, required: true },
];

export default function App() {
  return (
    <ThemeProvider theme={createTheme()}>
      <FormBuilder
        fields={fields}
        schema={schema}
        onSubmit={(data) => {
          // data.name  → string  (TypeScript inferred from schema)
          // data.email → string
          // data.age   → number
          console.log(data);
        }}
      />
    </ThemeProvider>
  );
}

Field Schema Reference

Property Type Required Description
name string Field name — must match a key in your Zod schema
label string Display label
type FieldType See field types below
defaultValue unknown Initial value
placeholder string Input placeholder
required boolean Shows asterisk, sets aria-required
disabled boolean Disables the field
options Option[] For SELECT, RADIO, CHECKBOX
multiple boolean Multi-select for SELECT and AUTOCOMPLETE
grid GridConfig MUI Grid size — e.g. { xs: 12, sm: 6 }
size 'small' | 'medium' MUI component size
fullWidth boolean Full-width input (default true)
min number Min value — NUMBER only; also sets HTML min
max number Max value — NUMBER only; also sets HTML max
step number Step — NUMBER only; also sets HTML step
rows number Visible text rows — TEXTAREA only (default 4)
startAdornment React.ReactNode Prefix node inside the input (TEXT, NUMBER). E.g. "$", icon
endAdornment React.ReactNode Suffix node inside the input (TEXT, NUMBER). E.g. "kg"
fetchOptions (query: string) => Promise<Option[]> Async options for AUTOCOMPLETE
visibleIf (values: FieldValues) => boolean Hides field when returns false
muiProps Record<string, any> Extra props forwarded to the underlying MUI component
section string Groups consecutive same-section fields under a shared header
Field Types
import { FIELD_TYPE } from 'mui-schema-form-builder';

FIELD_TYPE.TEXT;         // <input type="text">
FIELD_TYPE.TEXTAREA;     // <textarea> (multiline)
FIELD_TYPE.NUMBER;       // <input type="number">
FIELD_TYPE.DATE;         // <input type="date">
FIELD_TYPE.PASSWORD;     // Password input with show/hide toggle
FIELD_TYPE.SELECT;       // <Select> single or multi
FIELD_TYPE.AUTOCOMPLETE; // <Autocomplete> static or async
FIELD_TYPE.RADIO;        // <RadioGroup>
FIELD_TYPE.CHECKBOX;     // Boolean or checkbox group
FIELD_TYPE.ARRAY;        // Dynamic list with add/remove (useFieldArray)
FIELD_TYPE.DATE_PICKER;  // MUI DatePicker — register via createDatePickerInput

Password Input

Use FIELD_TYPE.PASSWORD for a text input with a built-in show/hide toggle. The toggle uses an inline SVG icon — no @mui/icons-material dependency needed.

{
  name: 'password',
  label: 'Password',
  type: FIELD_TYPE.PASSWORD,
  required: true,
}

Input Adornments

Add a prefix or suffix decoration to TEXT and NUMBER fields via startAdornment and endAdornment. Pass any React node — a string, an icon, or an interactive element.

[
  {
    name: 'price',
    label: 'Price',
    type: FIELD_TYPE.NUMBER,
    startAdornment: '

Note: PASSWORD fields have their own fixed end adornment (the visibility toggle). Setting endAdornment on a PASSWORD field has no effect.


Form Title

Add a heading to FormBuilder or FormWizard with the title, titleAlign, and titlePosition props.

<FormBuilder
  title="Edit Profile"
  titleAlign="left"        // 'left' | 'center' | 'right' — default: 'left'
  titlePosition="inside"   // 'inside' | 'above' — default: 'inside'
  ...
/>
  • titlePosition="inside" — the heading renders inside the Paper container above the fields.
  • titlePosition="above" — the heading renders outside the Paper, useful when you control the container styling.

Custom Action Buttons

Replace the default Submit / Cancel / Reset buttons with your own layout via renderActions.

FormBuilder
import type { FormBuilderActionsParams } from 'mui-schema-form-builder';

<FormBuilder
  onSubmit={fn}
  onCancel={cancelFn}
  onReset={resetFn}
  renderActions={({ isSubmitting, submit, cancel, reset }: FormBuilderActionsParams) => (
    <Stack direction="row" spacing={1} justifyContent="flex-end">
      {cancel && <Button onClick={cancel}>Discard</Button>}
      <Button variant="contained" onClick={submit} disabled={isSubmitting}>
        Save Changes
      </Button>
    </Stack>
  )}
/>
FormWizard
import type { FormWizardActionsParams } from 'mui-schema-form-builder';

<FormWizard
  steps={steps}
  schema={schema}
  onSubmit={fn}
  renderActions={({
    isSubmitting, isFirstStep, isLastStep, next, back, submit,
  }: FormWizardActionsParams) => (
    <Stack direction="row" spacing={2} justifyContent="space-between" width="100%">
      <Button onClick={back} disabled={isFirstStep}>← Back</Button>
      {isLastStep
        ? <Button variant="contained" onClick={submit}>Finish</Button>
        : <Button variant="contained" onClick={next}>Continue →</Button>
      }
    </Stack>
  )}
/>

Validation

Pass any Zod schema. The library uses @hookform/resolvers/zod internally:

const schema = z
  .object({
    password: z.string().min(8).regex(/[A-Z]/, 'Needs uppercase'),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: 'Passwords must match',
    path: ['confirm'],
  });

Control when validation runs:

<FormBuilder
  validationMode="onChange"  // 'onChange' | 'onBlur' | 'onTouched' | 'onSubmit'
  ...
/>

Conditional Fields

Only fields with visibleIf subscribe to form state changes. All other fields are isolated — typing in one field does not re-render its siblings.

const fields = [
  {
    name: 'status',
    label: 'Status',
    type: FIELD_TYPE.SELECT,
    options: [
      { label: 'Employed', value: 'employed' },
      { label: 'Student', value: 'student' },
    ],
  },
  {
    name: 'company',
    label: 'Company',
    type: FIELD_TYPE.TEXT,
    visibleIf: (values) => values['status'] === 'employed',
  },
];

Async Autocomplete

Built-in 300ms debounce and stale-response protection. If a later search resolves before an earlier one, the earlier response is discarded.

{
  name: 'country',
  label: 'Country',
  type: FIELD_TYPE.AUTOCOMPLETE,
  fetchOptions: async (query) => {
    const res = await fetch(`/api/countries?q=${query}`);
    const data = await res.json();
    return data.map((c: Country) => ({ label: c.name, value: c.code }));
  },
}

FormBuilder Props

Prop Type Default Description
fields FieldConfig[] required Field configuration
schema z.ZodType required* Zod validation schema (*or resolver)
resolver Resolver react-hook-form resolver (alternative to schema)
onSubmit (data: z.infer<TSchema>) => void | Promise<void> required Typed submit handler
onCancel () => void Renders Cancel button when provided
onReset () => void Renders Reset button when provided
onChange (values: FieldValues) => void Called on every field value change
onFieldChange (name: string, value: unknown) => void Called when a single field changes
submitText string 'Submit' Submit button label
cancelText string 'Cancel' Cancel button label
resetText string 'Reset' Reset button label
title string Optional form heading
titleAlign 'left' | 'center' | 'right' 'left' Horizontal alignment of the title
titlePosition 'inside' | 'above' 'inside' Whether the title is inside or above the Paper container
renderActions (params: FormBuilderActionsParams) => ReactNode Replace default buttons with a custom render
readOnly boolean false Render all fields as display text
labels FormBuilderLabels Override built-in UI strings
spacing number 2 MUI Grid spacing between fields
virtualize boolean false Enable react-window for large forms
validationMode ValidationMode 'onTouched' When validation triggers
sx SxProps MUI sx prop for the outer Paper

TypeScript Tips

The generic propagates from schema → onSubmit automatically:

const schema = z.object({ name: z.string(), age: z.number() });

<FormBuilder
  schema={schema}
  fields={fields}
  onSubmit={(data) => {
    // data.name → string ✓
    // data.age  → number ✓
  }}
/>;

Memoize your fields array to prevent unnecessary recomputation of default values:

const fields = useMemo<FieldConfig[]>(
  () => [{ name: 'name', label: 'Name', type: FIELD_TYPE.TEXT }],
  [],
);

Accessibility

  • Every input has a proper <label htmlFor> association — clicking the label focuses the input
  • Required asterisk is aria-hidden (visual cue only)
  • Inputs have aria-required, aria-invalid, aria-describedby linked to error messages
  • Error messages have role="alert" for screen reader announcement
  • Radio groups and checkbox groups use <fieldset> + <legend> (WCAG 1.3.1)

License

MIT Arjun Prakash

, endAdornment: 'USD', }, { name: 'username', label: 'Username', type: FIELD_TYPE.TEXT, startAdornment: '@', }, ]

Note: PASSWORD fields have their own fixed end adornment (the visibility toggle). Setting __INLINE_CODE_79__ on a PASSWORD field has no effect.


Form Title

Add a heading to __INLINE_CODE_80__ or __INLINE_CODE_81__ with the __INLINE_CODE_82__, __INLINE_CODE_83__, and __INLINE_CODE_84__ props.

__CODE_BLOCK_7__
  • __INLINE_CODE_85__ — the heading renders inside the Paper container above the fields.
  • __INLINE_CODE_86__ — the heading renders outside the Paper, useful when you control the container styling.

Custom Action Buttons

Replace the default Submit / Cancel / Reset buttons with your own layout via __INLINE_CODE_87__.

FormBuilder
__CODE_BLOCK_8__
FormWizard
__CODE_BLOCK_9__

Validation

Pass any Zod schema. The library uses __INLINE_CODE_88__ internally:

__CODE_BLOCK_10__

Control when validation runs:

__CODE_BLOCK_11__

Conditional Fields

Only fields with __INLINE_CODE_89__ subscribe to form state changes. All other fields are isolated — typing in one field does not re-render its siblings.

__CODE_BLOCK_12__

Async Autocomplete

Built-in 300ms debounce and stale-response protection. If a later search resolves before an earlier one, the earlier response is discarded.

__CODE_BLOCK_13__

FormBuilder Props

Prop Type Default Description
__INLINE_CODE_90__ __INLINE_CODE_91__ required Field configuration
__INLINE_CODE_92__ __INLINE_CODE_93__ required* Zod validation schema (*or __INLINE_CODE_94__)
__INLINE_CODE_95__ __INLINE_CODE_96__ react-hook-form resolver (alternative to __INLINE_CODE_97__)
__INLINE_CODE_98__ __INLINE_CODE_99__ required Typed submit handler
__INLINE_CODE_100__ __INLINE_CODE_101__ Renders Cancel button when provided
__INLINE_CODE_102__ __INLINE_CODE_103__ Renders Reset button when provided
__INLINE_CODE_104__ __INLINE_CODE_105__ Called on every field value change
__INLINE_CODE_106__ __INLINE_CODE_107__ Called when a single field changes
__INLINE_CODE_108__ __INLINE_CODE_109__ __INLINE_CODE_110__ Submit button label
__INLINE_CODE_111__ __INLINE_CODE_112__ __INLINE_CODE_113__ Cancel button label
__INLINE_CODE_114__ __INLINE_CODE_115__ __INLINE_CODE_116__ Reset button label
__INLINE_CODE_117__ __INLINE_CODE_118__ Optional form heading
__INLINE_CODE_119__ __INLINE_CODE_120__ __INLINE_CODE_121__ Horizontal alignment of the title
__INLINE_CODE_122__ __INLINE_CODE_123__ __INLINE_CODE_124__ Whether the title is inside or above the Paper container
__INLINE_CODE_125__ __INLINE_CODE_126__ Replace default buttons with a custom render
__INLINE_CODE_127__ __INLINE_CODE_128__ __INLINE_CODE_129__ Render all fields as display text
__INLINE_CODE_130__ __INLINE_CODE_131__ Override built-in UI strings
__INLINE_CODE_132__ __INLINE_CODE_133__ __INLINE_CODE_134__ MUI Grid spacing between fields
__INLINE_CODE_135__ __INLINE_CODE_136__ __INLINE_CODE_137__ Enable react-window for large forms
__INLINE_CODE_138__ __INLINE_CODE_139__ __INLINE_CODE_140__ When validation triggers
__INLINE_CODE_141__ __INLINE_CODE_142__ MUI sx prop for the outer Paper

TypeScript Tips

The generic propagates from schema → onSubmit automatically:

__CODE_BLOCK_14__

Memoize your __INLINE_CODE_143__ array to prevent unnecessary recomputation of default values:

__CODE_BLOCK_15__

Accessibility

  • Every input has a proper __INLINE_CODE_144__ association — clicking the label focuses the input
  • Required asterisk is __INLINE_CODE_145__ (visual cue only)
  • Inputs have __INLINE_CODE_146__, __INLINE_CODE_147__, __INLINE_CODE_148__ linked to error messages
  • Error messages have __INLINE_CODE_149__ for screen reader announcement
  • Radio groups and checkbox groups use __INLINE_CODE_150__ + __INLINE_CODE_151__ (WCAG 1.3.1)

License

MIT Arjun Prakash

Keywords