@smart-forms/core
v0.2.0
Published
Framework-agnostic schema, validation, and state for smart-forms
Maintainers
Readme
smart-forms
Schema-driven React form generator. Describe fields as a config array — get inputs, Zod validation, errors, conditional visibility, and submit handling with zero manual JSX wiring for the common case.
Packages
| Package | Description |
|---------|-------------|
| @smart-forms/core | Framework-agnostic schema, normalize, validation, state, layout |
| @smart-forms/react | React bindings: <SmartForm>, useSmartForm, field components |
| @smart-forms/theme-default | Default CSS theme |
Install
pnpm add @smart-forms/react @smart-forms/theme-default
# peer: react, react-domimport { SmartForm } from '@smart-forms/react';
import '@smart-forms/theme-default/styles.css';Three API tiers (same engine)
1. Declarative <SmartForm>
import { SmartForm } from '@smart-forms/react';
import '@smart-forms/theme-default/styles.css';
const fields = [
{ type: 'email' },
{ type: 'password' },
];
export function Login() {
return (
<SmartForm
fields={fields}
submitLabel="Sign in"
onSubmit={(values) => console.log(values)}
/>
);
}2. Headless useSmartForm({ fields })
import { useSmartForm } from '@smart-forms/react';
export function CustomLogin() {
const form = useSmartForm({
fields: [{ type: 'email' }, { type: 'password' }],
});
return (
<form onSubmit={form.handleSubmit((values) => console.log(values))}>
<input
value={String(form.values.email ?? '')}
onChange={(e) => form.setValue('email', e.target.value)}
aria-invalid={!!form.errors.email}
/>
{form.errors.email && <span role="alert">{form.errors.email}</span>}
<button type="submit" disabled={!form.isValid}>
Submit
</button>
</form>
);
}3. Raw Zod schema useSmartForm({ schema })
import { z } from 'zod';
import { useSmartForm } from '@smart-forms/react';
const schema = z.object({
age: z.number().min(18),
nickname: z.string().min(2),
});
export function Advanced() {
const form = useSmartForm({
schema,
defaultValues: { age: 18, nickname: '' },
onSubmit: (values) => console.log(values),
});
return (
<form onSubmit={form.handleSubmit()}>
{/* your own JSX */}
</form>
);
}Field types
string · email · password · textarea · tel · url · number · checkbox · select · radio · date · file
Shared options: name (optional for email/password/tel/url), label, placeholder, required, disabled, readonly, style, crossValidate, showWhen / hideWhen / enableWhen / disableWhen, asyncValidate, errorDisplay, a11y, customValidator.
Form-level defaults (e.g. { required: true }) apply after type presets and before per-field overrides.
Conditional visibility
{
type: 'string',
name: 'companyName',
required: true,
showWhen: { field: 'isEmployed', operator: 'equals', value: true },
}Hidden fields are excluded from validation via a single getActiveFields filter shared by the validator and renderer.
Cross-field validation
{
type: 'password',
name: 'confirmPassword',
required: true,
crossValidate: [
{ rule: 'matches', field: 'password', message: 'Passwords must match' },
],
}Rules: matches | greaterThan | lessThan | notEquals | custom.
Async validation
{
type: 'string',
name: 'username',
required: true,
asyncValidate: {
debounceMs: 400,
validate: async (value) => {
const ok = await checkAvailable(String(value));
return ok || 'Username taken';
},
},
}Async errors merge into the same error map as sync validation.
Layout
<SmartForm
fields={fields}
layout={{ columns: 2, gap: '1rem' }}
onSubmit={...}
/>Monorepo development
pnpm install
pnpm build
pnpm testExamples:
pnpm --filter login-form-js dev # plain JavaScript
pnpm --filter signup-form-ts dev # TypeScript
pnpm --filter job-application-ts dev # full-featuredArchitecture
@smart-forms/corehas zero React dependency.@smart-forms/reactnever re-implements validation, normalization, or condition evaluation — it only calls intocore.- Defaults (
label,placeholder,errorDisplay,a11y) and type presets (email→ name/required/…) are filled once innormalizeField(preset → form defaults → user). - Conditions use one
evaluateConditionfor show/hide/enable/disable.
