@ahrowe/form-validation
v0.2.0
Published
Framework-agnostic form validation core (FormValidator, FormValidatorGroup, validators) — the validation engine behind @ahrowe/ui
Readme
@ahrowe/form-validation
Framework-agnostic form validation core — FormValidator, FormValidatorGroup, built-in Validators, and the useFormValidator / useFormValidatorGroup hooks. This is the validation engine behind @ahrowe/ui; it has no dependency on any UI component library and can be used standalone.
npm install @ahrowe/form-validationimport { FormValidator, Validators } from '@ahrowe/form-validation';
const nameValidator = new FormValidator('', [Validators.required(), Validators.minLength(2)]);FormValidatorGroup
import { FormValidator, FormValidatorGroup, Validators } from '@ahrowe/form-validation';
const userForm = new FormValidatorGroup(
{
name: new FormValidator('', [Validators.required()]),
email: new FormValidator('', [Validators.required(), Validators.email()]),
},
null, // class component instance, or null when not using class components
{
initialValues: { name: 'Jane' },
onSubmit: async (values) => {
await api.saveUser(values); // values: { name: string; email: string }
},
}
);
userForm.group.name.set('Jane Doe');
await userForm.submit(); // validates all fields, then calls onSubmit if there are no errorsuseFormValidator
import { FormValidator, Validators, useFormValidator } from '@ahrowe/form-validation';
import { useRef } from 'react';
function EmailField() {
const email = useFormValidator(
useRef(new FormValidator('', [Validators.required(), Validators.email()])).current
);
return (
<div>
{/* {...email.bind()} spreads name/value/onChange/onBlur directly onto the input */}
<input {...email.bind()} />
{email.touched && email.hasError() && <span>{email.getCurrentErrorMessage()}</span>}
</div>
);
}bind() (on FormValidator) returns props spreadable directly onto a native input/textarea/select, react-hook-form-register-style: name, onChange, onBlur, and either value or — for boolean fields such as checkboxes — checked. It also coerces numeric fields (FormValidator<number>) and reads <select multiple> as string[]. It doesn't support radio groups (each option needs its own checked, derived by equality) — wire those by hand.
const agreedToTerms = new FormValidator(false);
<input type="checkbox" {...agreedToTerms.bind('agreedToTerms')} />
// checked=false, value omitted; name is required here since agreedToTerms
// isn't in a FormValidatorGroup (which is what defaults `label`, and
// therefore `bind()`'s default name, to the group key)
const email = new FormValidator('');
<input {...email.bind('email')} />
// value='', checked omitted; name defaults to `label` if omitteduseFormValidatorGroup
import { FormValidator, Validators, useFormValidatorGroup } from '@ahrowe/form-validation';
function SignupForm() {
const userForm = useFormValidatorGroup(
{
name: new FormValidator('', [Validators.required()]),
email: new FormValidator('', [Validators.required(), Validators.email()]),
},
{
onSubmit: async (values) => {
await api.saveUser(values);
},
}
);
return (
<form onSubmit={userForm.submit}>
<input
value={userForm.group.name.value}
onChange={(e) => userForm.group.name.set(e.target.value)}
/>
<input
value={userForm.group.email.value}
onChange={(e) => userForm.group.email.set(e.target.value)}
/>
<button type="submit" disabled={userForm.isSubmitting}>Submit</button>
</form>
);
}See CLAUDE.md for conventions and src/index.ts for the full public API.
Scripts
npm test # single run
npm run test:watch
npm run build # emits dist/ (esm + cjs + d.ts)
npm run lintRelationship to @ahrowe/ui
@ahrowe/ui re-exports everything here plus a thin adapter layer (ValidatableComponent, a toast-and-.Form-wired FormValidatorGroup) that depends on its own UI components (Tooltip, showToast).
