@form-instant/react-input-mapping
v2.2.4
Published
<!-- Documentación empaquetada para consumo por IA. Generado desde README.md + views/*.md -->
Downloads
351
Maintainers
Readme
Quick start guide
1. Installation and dependency order
Install in this order (or all at once in a monorepo):
React (peer)
bun add react react-dom # or: npm i react react-domZod (for defining schemas)
bun add zod # or: npm i zod@form-instant/react-input-mapping (field → component mapping)
bun add @form-instant/react-input-mapping # or: npm i @form-instant/react-input-mapping@form-instant/react-resolver-zod (resolves Zod schema and provides
FormInstantProvider/FormInstantElement)bun add @form-instant/react-resolver-zod # or: npm i @form-instant/react-resolver-zodThis package has as peers:
react,@form-instant/react-input-mapping, andzod. Without the mapping installed first, the resolver cannot resolve fields.
Summary: react → zod → @form-instant/react-input-mapping → @form-instant/react-resolver-zod.
<span id="implementation">
Implementation in your project
2.1 Creating the Mapping
The Mapping is a dictionary that associates each fieldType (string) with a React component. You can use InputMapping or InputMappingStore (recommended: better support for granular re-renders).
- Default keys (
INPUT_COMPONENTS_KEYS):'checkbox' | 'date' | 'select' | 'radio' | 'switch' | 'textarea' | 'number' | 'file' | 'text' | 'fallback'. - Extra keys: you can extend with your own types (e.g.
email,password,customSelect). - Each component receives ParsedField:
name(current,history),fieldType,required,default,fieldConfig, and for selects/enumsoptionsas[value, label][]. - Important: Type your components as
FC<ParsedField<MyInputs['key']>>, notFC<FieldConfig<...>>. At runtime,ElementMappingpasses a fullParsedFieldobject.FieldConfigonly describes the config shape per field;ParsedFieldis what each component actually receives.
Minimal example (in a file like providers/input-mapping.tsx):
import {
InputMappingStore,
createFormInstantContainer,
type ParsedField,
type FieldConfig,
} from '@form-instant/react-input-mapping';
import type { FC } from 'react';
export type MyInputs = {
text: FieldConfig<{ placeholder?: string; label?: string }>;
number: FieldConfig<{ placeholder?: string; label?: string; min?: number; max?: number }>;
textarea: FieldConfig<{ placeholder?: string; label?: string }>;
date: FieldConfig;
email: FieldConfig<{ placeholder?: string; label?: string }>;
password: FieldConfig<{ placeholder?: string; label?: string }>;
checkbox: FieldConfig;
select: FieldConfig;
fallback: FieldConfig;
};
const TextInput: FC<ParsedField<MyInputs['text']>> = ({ name, fieldConfig, required, ...props }) => (
<div>
<label htmlFor={name.current}>{(fieldConfig as any)?.label ?? name.current}{required && ' *'}</label>
<input id={name.current} name={name.history} type="text" required={required} {...props} />
</div>
);
// Define the rest: NumberInput, TextareaInput, DateInput, EmailInput, PasswordInput,
// CheckboxInput, SelectInput (uses props.options as [value, label][]), FallbackInput...
export const inputMapping = new InputMappingStore<MyInputs>({
text: TextInput,
number: NumberInput,
textarea: TextareaInput,
date: DateInput,
email: EmailInput,
password: PasswordInput,
checkbox: CheckboxInput,
select: SelectInput,
fallback: FallbackInput,
});
export const { FormInstantInputsProvider, useInputMapping } =
createFormInstantContainer<MyInputs>(inputMapping);- Select: the component receives
options?: [string, string][]; you can populate them from an API (data fetch) and keep using the same mapping.
2.2 Setting up providers
createFormInstantContainer<MyInputs>(inputMapping)returns:FormInstantInputsProvider: provider that should wrap your app (or at least the area where forms are used).useInputMapping: hook to access the mapping (e.g. for custom components that useElementMapping).
Place it at the root (e.g. App.tsx or layout.tsx):
import { FormInstantInputsProvider } from './providers/input-mapping';
function App() {
return (
<FormInstantInputsProvider>
{/* routes, forms, etc. */}
</FormInstantInputsProvider>
);
}2.3 Zod schema and fieldConfig
- Define the schema with Zod. With @form-instant/react-resolver-zod the package already extends Zod with
.fieldConfig()on the types used by the parser. .fieldConfig(...)is used to specifyfieldTypeand extra props (placeholder, label, min, max, etc.) that reach the component viafieldConfig.
Example:
import { z } from 'zod';
// If you use @form-instant/react-resolver-zod, you don't need to call extendZodWithFieldConfig:
// the module applies the extension on import.
const formSchema = z.object({
name: z.string().min(2).fieldConfig({ fieldType: 'text', placeholder: 'Name', label: 'Name' }),
email: z.email().fieldConfig({ fieldType: 'email', placeholder: '[email protected]' }),
age: z.number().min(18).max(100).fieldConfig({ fieldType: 'number', min: 18, max: 100 }),
bio: z.string().min(10).fieldConfig({ fieldType: 'textarea' }),
role: z.enum(['admin', 'user']).fieldConfig({ fieldType: 'select' }),
});
export type FormSchemaType = z.infer<typeof formSchema>;- If you omit
fieldConfig, the resolver infersfieldTypefrom the Zod type (string → text, number → number, etc.).
2.4 Rendering forms
- Wrap the form in
FormInstantProviderwith the schema. Pass a stable schema reference (e.g. defined outside the component or memoized withuseMemo) so the provider does not re-parse the schema on every parent re-render. - Use
FormInstantElement<FormSchemaType> name="..."for each path in the schema you want to render:- Primitive field:
name="name"→ a single input. - Nested object:
name="personalData"→ all fields ofpersonalDataare rendered (the provider parses the schema and exposesfield.schema;FormInstantElementiterates and usesElementMappingfor each child).
- Primitive field:
Minimal example:
import { FormInstantProvider, FormInstantElement } from '@form-instant/react-resolver-zod';
import { formSchema, type FormSchemaType } from './schema';
export function MyForm() {
return (
<form onSubmit={...}>
<FormInstantProvider schema={formSchema}>
<FormInstantElement<FormSchemaType> name="name" />
<FormInstantElement<FormSchemaType> name="email" />
<FormInstantElement<FormSchemaType> name="age" />
<FormInstantElement<FormSchemaType> name="bio" />
<FormInstantElement<FormSchemaType> name="role" />
</FormInstantProvider>
</form>
);
}For nested objects, one FormInstantElement per object:
<FormInstantProvider schema={objectFormSchema}>
<FormInstantElement<ObjectFormType> name="personalData" />
<FormInstantElement<ObjectFormType> name="address" />
</FormInstantProvider><span id="form-types">
Form types
3.1 Static form
Flat schema, fixed fields. You only need FormInstantProvider plus several FormInstantElement (one per field or per object group).
const schema = z.object({
name: z.string().min(2),
email: z.email(),
age: z.number().min(18).max(100),
});
// ...
<FormInstantProvider schema={schema}>
<FormInstantElement<FormType> name="name" />
<FormInstantElement<FormType> name="email" />
<FormInstantElement<FormType> name="age" />
</FormInstantProvider>3.2 Inputs with data fetch (select / autocomplete)
- Select: in the schema use
z.enum([...])or a type the parser turns intooptions; the mapping component receivesoptionsand can also receive data loaded from an API (store options in state and pass them via context or props to the component that renders the field). - Autocomplete: you can define your own
fieldType(e.g.autocomplete), map it to a component that usesuseFieldsand internally fetches and shows suggestions; the final value is still bound to the samename.
The select mapping already uses options?: [string, string][]; populating options from an API is compatible with the same flow.
3.3 Dynamic form with discriminatedUnion
Schema that changes based on a discriminator (e.g. user type). Steps:
- Define the schema with
z.discriminatedUnion('status', [ z.object({ status: z.literal('ok'), ... }), z.object({ status: z.literal('not'), ... }) ]). - Use
useSchemafrom@form-instant/react-resolver-zod: it receives a callback that returns the schema and a dependencies object; when the reference of the dependencies object changes, the schema and initial values are recalculated. Pass a stable dependencies object (e.g. from state oruseMemo) to avoid unnecessary recalculations on parent re-renders. - Keep dependencies in sync with the current discriminator value in the form (e.g. with
form.watch('data.status')if you use react-hook-form, or your own state). - The first field in the union is the discriminator; you can map that field to a select in your mapping (by
fieldTypeor by the discriminator key) so the user can switch the variant.
Structure example (submit and form provider depend on your form library):
const formSchema = z.object({
data: z.discriminatedUnion('status', [
z.object({ status: z.literal('ok'), code: z.string() }),
z.object({ status: z.literal('not'), birthday: z.coerce.date() }),
]),
});
const [dependencies, setDependencies] = useState({ status: 'ok' });
const { schema } = useSchema(() => formSchema, dependencies);
// In a useEffect or in the same flow as the form: when data.status changes,
// update setDependencies({ status: newStatus }) so useSchema returns
// the correct schema and FormInstantElement shows the fields for that variant.
<FormInstantProvider schema={schema}>
<FormInstantElement<FormType> name="data" />
</FormInstantProvider>3.4 Array with dynamic input creation
For arrays of objects (e.g. list of items or skills):
- Schema with
z.array(z.object({ ... }))(optionally.min()/.max()). - In the array component:
useFields({ key: 'items' })(or the array field name) to get the parsed field.useInputArray(field)(from@form-instant/react-input-mapping): returnsinputs,append,remove,fieldConfig, etc.
- Render each item: for each entry in
inputs, iterate over properties and useElementMapping formProps={...}for each. +/- buttons that callappend()andremove(index).
Condensed example:
const arrayFormSchema = z.object({
items: z.array(z.object({
name: z.string().min(2),
quantity: z.number().min(1),
price: z.number().min(0),
})).min(1).max(10),
});
function ArrayFieldComponent({ name }: { name: 'items' }) {
const field = useFields({ key: name });
const { inputs, append, remove, fieldConfig } = useInputArray(field);
const id = useId();
return (
<div>
{inputs.map((inputFields, index) => (
<div key={`${id}-${index}`}>
{Object.entries(inputFields).map(([key, value]) => (
<ElementMapping key={key} formProps={value} />
))}
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={append}>+ Add</button>
</div>
);
}
<FormInstantProvider schema={arrayFormSchema}>
<ArrayFieldComponent name="items" />
</FormInstantProvider>fieldConfig?.min / fieldConfig?.max can come from the schema (e.g. via .fieldConfig({ min: 1, max: 10 })) to disable buttons based on limits.
3.5 Nested object
Schema with z.object({ group: z.object({ a: z.string(), b: z.number() }) }). No custom component needed: a single FormInstantElement<FormType> name="group" makes the resolver render all fields of group (the parser fills field.schema and FormInstantElement walks those children with ElementMapping).
const objectFormSchema = z.object({
personalData: z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
email: z.string().email(),
}),
address: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string(),
}),
});
<FormInstantProvider schema={objectFormSchema}>
<FormInstantElement<ObjectFormType> name="personalData" />
<FormInstantElement<ObjectFormType> name="address" />
</FormInstantProvider><span id="full-example">
Full example: React Hook Form + Shadcn UI + Zod
This example wires react-hook-form, @hookform/resolvers (Zod resolver), Zod, and shadcn/ui (or any similar UI primitives) with Form Instant. Validation runs via zodResolver; inputs are rendered from your schema and mapping.
Dependencies
bun add react-hook-form @hookform/resolvers zod
# or: npm i react-hook-form @hookform/resolvers zodAdd shadcn/ui with your stack (e.g. npx shadcn@latest init) and install the components you need (e.g. Input, Label, Button). You can also use plain HTML elements or your own design system.
1. Zod schema
// lib/schemas/profile.ts
import { z } from 'zod';
export const profileSchema = z.object({
username: z.string().min(2, 'At least 2 characters').fieldConfig({ fieldType: 'text', placeholder: 'Username', label: 'Username' }),
email: z.string().email('Invalid email').fieldConfig({ fieldType: 'email', placeholder: '[email protected]', label: 'Email' }),
age: z.number().min(18, 'Must be 18+').max(120).fieldConfig({ fieldType: 'number', label: 'Age', min: 18, max: 120 }),
bio: z.string().min(10, 'At least 10 characters').fieldConfig({ fieldType: 'textarea', label: 'Bio', placeholder: 'Tell us about yourself' }),
});
export type ProfileFormValues = z.infer<typeof profileSchema>;2. Input mapping (with react-hook-form registration)
Type each mapping component as FC<ParsedField<MyInputs['key']>>—they receive a full ParsedField, not FieldConfig alone.
For react-hook-form to validate and submit correctly, each rendered input must be registered. Use useFormContext() inside your mapping components and spread register(name.history) (and optionally show errors from formState.errors).
// components/input-mapping.tsx
import {
InputMappingStore,
createFormInstantContainer,
type ParsedField,
type FieldConfig,
} from '@form-instant/react-input-mapping';
import { useFormContext } from 'react-hook-form';
import type { FC } from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
export type MyInputs = {
text: FieldConfig<{ placeholder?: string; label?: string }>;
number: FieldConfig<{ placeholder?: string; label?: string; min?: number; max?: number }>;
textarea: FieldConfig<{ placeholder?: string; label?: string }>;
email: FieldConfig<{ placeholder?: string; label?: string }>;
fallback: FieldConfig;
};
const TextInput: FC<ParsedField<MyInputs['text']>> = ({ name, fieldConfig, required, ...props }) => {
const { register, formState: { errors } } = useFormContext();
const label = (fieldConfig as any)?.label ?? name.current;
const placeholder = (fieldConfig as any)?.placeholder ?? '';
return (
<div className="space-y-2">
<Label htmlFor={name.current}>{label}{required && ' *'}</Label>
<Input
id={name.current}
type="text"
placeholder={placeholder}
aria-invalid={!!errors[name.history]}
{...register(name.history)}
{...props}
/>
{errors[name.history]?.message && (
<p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
)}
</div>
);
};
const NumberInput: FC<ParsedField<MyInputs['number']>> = ({ name, fieldConfig, required, ...props }) => {
const { register, formState: { errors } } = useFormContext();
const label = (fieldConfig as any)?.label ?? name.current;
return (
<div className="space-y-2">
<Label htmlFor={name.current}>{label}{required && ' *'}</Label>
<Input
id={name.current}
type="number"
min={fieldConfig?.min}
max={fieldConfig?.max}
aria-invalid={!!errors[name.history]}
{...register(name.history, { valueAsNumber: true })}
{...props}
/>
{errors[name.history]?.message && (
<p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
)}
</div>
);
};
const TextareaInput: FC<ParsedField<MyInputs['textarea']>> = ({ name, fieldConfig, required, ...props }) => {
const { register, formState: { errors } } = useFormContext();
const label = (fieldConfig as any)?.label ?? name.current;
const placeholder = (fieldConfig as any)?.placeholder ?? '';
return (
<div className="space-y-2">
<Label htmlFor={name.current}>{label}{required && ' *'}</Label>
<Textarea
id={name.current}
placeholder={placeholder}
aria-invalid={!!errors[name.history]}
{...register(name.history)}
{...props}
/>
{errors[name.history]?.message && (
<p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
)}
</div>
);
};
const EmailInput: FC<ParsedField<MyInputs['email']>> = ({ name, fieldConfig, required, ...props }) => {
const { register, formState: { errors } } = useFormContext();
const label = (fieldConfig as any)?.label ?? name.current;
const placeholder = (fieldConfig as any)?.placeholder ?? '';
return (
<div className="space-y-2">
<Label htmlFor={name.current}>{label}{required && ' *'}</Label>
<Input
id={name.current}
type="email"
placeholder={placeholder}
aria-invalid={!!errors[name.history]}
{...register(name.history)}
{...props}
/>
{errors[name.history]?.message && (
<p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
)}
</div>
);
};
const FallbackInput: FC<ParsedField<MyInputs['fallback']>> = ({ name, fieldConfig, required, ...props }) => {
const { register, formState: { errors } } = useFormContext();
const label = (fieldConfig as any)?.label ?? name.current;
return (
<div className="space-y-2">
<Label htmlFor={name.current}>{label}{required && ' *'}</Label>
<Input
id={name.current}
type="text"
aria-invalid={!!errors[name.history]}
{...register(name.history)}
{...props}
/>
{errors[name.history]?.message && (
<p className="text-sm text-destructive">{String(errors[name.history]?.message)}</p>
)}
</div>
);
};
export const inputMapping = new InputMappingStore<MyInputs>({
text: TextInput,
number: NumberInput,
textarea: TextareaInput,
email: EmailInput,
fallback: FallbackInput,
});
export const { FormInstantInputsProvider, useInputMapping } =
createFormInstantContainer<MyInputs>(inputMapping);3. Form component
Use FormProvider from react-hook-form, zodResolver with your schema, and FormInstantProvider + FormInstantElement so fields are rendered from the schema and your mapping.
// components/profile-form.tsx
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { FormInstantProvider, FormInstantElement } from '@form-instant/react-resolver-zod';
import { profileSchema, type ProfileFormValues } from '@/lib/schemas/profile';
import { Button } from '@/components/ui/button';
export function ProfileForm() {
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileSchema),
defaultValues: {
username: '',
email: '',
age: undefined,
bio: '',
},
});
const onSubmit = (data: ProfileFormValues) => {
console.log(data);
};
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormInstantProvider schema={profileSchema}>
<FormInstantElement<ProfileFormValues> name="username" />
<FormInstantElement<ProfileFormValues> name="email" />
<FormInstantElement<ProfileFormValues> name="age" />
<FormInstantElement<ProfileFormValues> name="bio" />
</FormInstantProvider>
<Button type="submit">Submit</Button>
</form>
</FormProvider>
);
}4. App root
Ensure FormInstantInputsProvider wraps the tree where forms use the mapping (e.g. root layout or App).
// App.tsx
import { FormInstantInputsProvider } from '@/components/input-mapping';
import { ProfileForm } from '@/components/profile-form';
export default function App() {
return (
<FormInstantInputsProvider>
<ProfileForm />
</FormInstantInputsProvider>
);
}Summary: Use Zod for the schema, zodResolver in useForm, FormProvider (react-hook-form) around the form, and FormInstantProvider + FormInstantElement to render fields from the schema. In your mapping components, call useFormContext() and register(name.history) (and optionally formState.errors) so react-hook-form controls and validates the inputs.
<span id="api-reference">
API reference (minimal)
| Concept | Package | Description |
| -------------------------------------------------------------- | -------------------------------- | --------------------------------------------------------------------- |
| InputMapping / InputMappingStore | react-input-mapping | Maps fieldType → React component. |
| createFormInstantContainer | react-input-mapping | Creates FormInstantInputsProvider and useInputMapping. |
| ParsedField, FieldConfig | react-input-mapping | Props types for mapping components. |
| ElementMapping | react-input-mapping | Renders a single field from formProps (fieldType, name, etc.). |
| useInputArray | react-input-mapping | For arrays:inputs, append, remove, fieldConfig. |
| FormInstantProvider, FormInstantElement, useFields | react-resolver-zod | Schema provider, element per path, hook for a field. |
| useSchema | react-resolver-zod | Reactive schema (e.g. for discriminatedUnion) and initial values. Recalculates when the reference of the dependencies object changes; pass a stable object to avoid unnecessary recalculations. |
| .fieldConfig(...) | react-resolver-zod (extends Zod) | Associate fieldType and props with a schema field. |
