@formhaus/core
v0.16.0
Published
JSON form definition engine: validation, conditional visibility, multi-step and branching routes. Zero dependencies, works in React, Vue, Svelte or vanilla.
Downloads
3,660
Maintainers
Readme
@formhaus/core
@formhaus/core runs a JSON form definition: field state, validation, conditional visibility, multi-step navigation and branching routes. It has zero runtime dependencies and no UI, so React, Vue, Svelte or vanilla JS can render on top of it.
Part of Formhaus. For React, use @formhaus/react. For Vue, use @formhaus/vue.
Install
npm install @formhaus/coreRequires Node ≥18. ESM-only.
Usage
import { FormEngine, type FormDefinition } from '@formhaus/core';
const definition: FormDefinition = {
id: 'contact',
title: 'Contact Us',
submit: { label: 'Send' },
fields: [
{ key: 'name', type: 'text', label: 'Name', validation: { required: true } },
{ key: 'email', type: 'email', label: 'Email', validation: { required: true } },
],
};
const engine = new FormEngine(definition);
engine.setValue('name', 'Jane');
const errors = engine.validate();
// { email: 'This field is required' }
engine.setValue('email', '[email protected]');
engine.validate();
// {}
engine.getSubmitValues();
// { name: 'Jane', email: '[email protected]' }Generating a definition
Write the JSON by hand, or use the /formhaus:formhaus-create-form Claude Code skill to generate it from a text description, a CSV table, or a screenshot of an existing form.
Navigation and submission
await engine.next(), back(), skip() and submit(handler) validate and run the async before/after hooks, which can cancel the action. See step navigation and lifecycle.
Step routes choose an ordered forward path from answers. Back, progress, validation and submission follow that path. See branching and retained answers.
Server-side validation
validateSubmission() checks a submitted payload against the definition without a browser. It drops hidden, skipped and unknown keys and checks value types. Custom validators run only when you pass them in validators. Strings over 10,000 characters or the field's maxLength fail before any rule runs. pattern rules still run on untrusted input, so bound the execution time of the call.
import { validateSubmission } from '@formhaus/core/server';
const { values, errors } = validateSubmission(definition, body.values, { skippedSteps: body.skippedSteps });checkDefinition() returns a list of issues with a code, a severity (error or warning), a JSON pointer path and a message. FormEngine throws a FormDefinitionError on errors.
import { checkDefinition } from '@formhaus/core';
const errors = checkDefinition(definition).filter((issue) => issue.severity === 'error');@formhaus/core/server.iife.js exposes both functions on a Formhaus global for embedded JS runtimes.
Optional baseline styles
The default React and Vue field components render unstyled HTML with fh-* class names. For a sensible starting look (padding, focus states, error colour, button styles), import the optional stylesheet:
import '@formhaus/core/style.css';Theme via CSS custom properties (--fh-color-primary, --fh-radius, --fh-gap, etc.) — no need to fork the file. If you're using a UI kit through the components prop, the stylesheet doesn't affect those.
What it covers
- Form state with reactive
subscribe()/getSnapshot()for adapters - Per-field and structure subscriptions for renderers that need isolated updates
- Field validation (required, min/max, pattern, matchField, custom validators)
- Visibility conditions (
show/showAny) with cascade-clearing of hidden values - Multi-step forms with conditional steps and per-step validation
- Async step validation via
onStepValidate+next() - Definition validation (duplicate keys, invalid regex, circular show conditions)
- Translations through
@formhaus/core/i18n: message and label catalogs, overlays and translation keys. See translations.
Granular subscriptions
subscribe() notifies on every engine update. Renderers can subscribe more narrowly so a change to one field does not update every field component:
const unsubscribe = engine.subscribeField('email', () => {
const value = engine.values.email;
const error = engine.errors.email;
renderEmail({ value, error });
});
engine.getFieldSnapshot('email');Use subscribeStructure() and getStructureSnapshot() for visible field lists, visible steps, and navigation. Every subscribe method returns an unsubscribe function.
Docs
- Full guide and API reference: https://formhaus.dev
- Live playground: https://formhaus.dev/playground.html
- Source and issues: https://github.com/ignsm/formhaus
License
MIT
