General Form
The createForm factory generates components strictly typed against your Zod schema. If you try to render <UserForm.Field name="typo">, the TypeScript compiler throws an error. This is the recommended approach for forms with static, known shapes.
However, sometimes you need to build forms dynamically based on server responses, JSON schemas, or user configuration. In these cases, you don't know the field names at compile time.
For this, AIR Form provides the general-purpose, untyped <Form> and <Field> components.
The Form Component
The general <Form> component takes the schema and initial value as props. It creates the reactive form state and provides it to all child fields.
import { setup, render, mutable } from '@anchorlib/react';
import { z } from 'zod';
import { Form, Field, TextInput, FormSubmit } from '@airlib/react-form';
// The schema might be generated dynamically
const dynamicSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
});
export const DynamicForm = setup(() => {
const profile = mutable({ firstName: '', lastName: '' });
return render(() => (
<Form
schema={dynamicSchema}
value={profile}
onSubmit={(data, changes) => console.log('Saved:', changes)}
>
<Field name="firstName" label="First Name">
<TextInput />
</Field>
<Field name="lastName" label="Last Name">
<TextInput />
</Field>
<FormSubmit>Save</FormSubmit>
</Form>
));
});import { setup, mutable } from '@anchorlib/solid';
import { z } from 'zod';
import { Form, Field, TextInput, FormSubmit } from '@airlib/solid-form';
// The schema might be generated dynamically
const dynamicSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
});
export const DynamicForm = setup(() => {
const profile = mutable({ firstName: '', lastName: '' });
return (
<Form
schema={dynamicSchema}
value={profile}
onSubmit={(data, changes) => console.log('Saved:', changes)}
>
<Field name="firstName" label="First Name">
<TextInput />
</Field>
<Field name="lastName" label="Last Name">
<TextInput />
</Field>
<FormSubmit>Save</FormSubmit>
</Form>
);
});The Field Component
The general <Field> component accepts any string as its name prop. It functions exactly like a typed field — providing label association, error rendering, and context for nested inputs.
import { setup, render } from '@anchorlib/react';
// A generic field renderer for dynamic forms
const RenderDynamicField = setup<{ name: string; label: string; type: string }>((props) => {
return render(() => (
<Field name={props.name} label={props.label}>
{props.type === 'email' ? <EmailInput /> : <TextInput />}
</Field>
));
});
// Rendering a list of fields driven by data
export const DynamicFormRenderer = setup<{ fields: any[]; schema: any; initialValue: any }>((props) => {
return render(() => (
<Form schema={props.schema} value={props.initialValue}>
{props.fields.map(f => (
<RenderDynamicField key={f.name} {...f} />
))}
<FormSubmit>Submit</FormSubmit>
</Form>
));
});import { setup } from '@anchorlib/solid';
import { For } from 'solid-js';
import { Form, Field, TextInput, EmailInput, FormSubmit } from '@airlib/solid-form';
// A generic field renderer for dynamic forms
const RenderDynamicField = setup<{ name: string; label: string; type: string }>((props) => {
return (
<Field name={props.name} label={props.label}>
{props.type === 'email' ? <EmailInput /> : <TextInput />}
</Field>
);
});
// Rendering a list of fields driven by data
export const DynamicFormRenderer = setup<{ fields: any[]; schema: any; initialValue: any }>((props) => {
return (
<Form schema={props.schema} value={props.initialValue}>
<For each={props.fields}>
{(f) => <RenderDynamicField {...f} />}
</For>
<FormSubmit>Submit</FormSubmit>
</Form>
);
});Trade-offs
When you use the general <Form> instead of createForm, you trade compile-time safety for runtime flexibility.
- No Autocomplete — Typing
name="in your editor won't suggest valid field paths. - Runtime Errors — If you misspell a field name, TypeScript won't warn you. The input simply won't connect to the expected schema path.
- Submit Typing — The
onSubmithandler will inferdatabased on the schema prop, but if the schema isZodType<any>, the data will beany.
For 90% of forms, use createForm. Reserve the general <Form> for form builders, CMS interfaces, and heavily data-driven views.