Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,28 @@ jobs:
check:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20, 22]

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: ${{ matrix.node-version }}
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm build

- run: pnpm lint:packages

- run: pnpm check:types

- run: pnpm --filter @formhaus/core --filter @formhaus/react run test:resolve

- run: pnpm test
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## 0.3.1 - 2026-04-10

### Breaking

- `FormSchema` type renamed to `FormDefinition`. `validateSchema()` renamed to `validateDefinition()`.
- `FormRenderer` prop `schema` renamed to `definition` (React and Vue).

### `@formhaus/core`

- New `onStepValidate` option runs an async validator between steps. `nextStepAsync()` awaits it. Exposes `stepValidating` and the `StepValidateFn` type.

### `@formhaus/react`

- `FormRenderer` accepts `onStepValidate`. Continue button shows a loading state while the validator runs.

### `@formhaus/vue`

- `FormRenderer` accepts `onStepValidate`. `useFormEngine` now returns `stepValidating`.

### Packaging

- **Breaking:** ESM-only. Dropped legacy `main` and `module` fields. Minimum Node is 18.
- Fixed `@formhaus/vue` type resolution under `moduleResolution: "node16"` and `"nodenext"`.
- `@formhaus/core` and `@formhaus/react` down to 2.87 KB and 3.15 KB gzipped.
- `sideEffects: false` on core, react, and vue.

## 0.3.0 - 2026-04-07

### Docs
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ npm install @formhaus/core @formhaus/react

```tsx
import { FormRenderer } from '@formhaus/react';
import schema from './contact-schema.json';
import definition from './contact-form.json';

function ContactPage() {
async function handleSubmit(values: Record<string, unknown>) {
Expand All @@ -96,7 +96,7 @@ function ContactPage() {
});
}

return <FormRenderer schema={schema} onSubmit={handleSubmit} />;
return <FormRenderer definition={definition} onSubmit={handleSubmit} />;
}
```

Expand All @@ -109,7 +109,7 @@ npm install @formhaus/core @formhaus/vue
```vue
<script setup lang="ts">
import { FormRenderer } from '@formhaus/vue';
import schema from './contact-schema.json';
import definition from './contact-form.json';

async function handleSubmit(values: Record<string, unknown>) {
await fetch('/api/contact', {
Expand All @@ -120,7 +120,7 @@ async function handleSubmit(values: Record<string, unknown>) {
</script>

<template>
<FormRenderer :schema="schema" @submit="handleSubmit" />
<FormRenderer :definition="definition" @submit="handleSubmit" />
</template>
```

Expand Down Expand Up @@ -156,7 +156,7 @@ const components: FieldComponentMap = {
email: MyTextInput,
};

<FormRenderer schema={schema} onSubmit={handleSubmit} components={components} />;
<FormRenderer definition={definition} onSubmit={handleSubmit} components={components} />;
```

### Vue
Expand All @@ -180,7 +180,7 @@ import MyTextInput from './MyTextInput.vue';

<template>
<FormRenderer
:schema="schema"
:definition="definition"
:components="{ text: MyTextInput, email: MyTextInput }"
@submit="handleSubmit"
/>
Expand Down
22 changes: 11 additions & 11 deletions docs/.vitepress/components/PlaygroundExamples.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ const selectedVue = ref('Basic Form');
const selectedSvelte = ref('Basic Form');

function reactFiles(fixture: string) {
const schema = JSON.stringify(fixtures[fixture], null, 2);
const definition = JSON.stringify(fixtures[fixture], null, 2);
return {
'App.tsx': `import { FormRenderer } from "@formhaus/react";

const schema = ${schema};
const definition = ${definition};

export default function App() {
return (
<div style={{ maxWidth: 480, margin: "24px auto", fontFamily: "sans-serif" }}>
<FormRenderer
schema={schema}
definition={definition}
onSubmit={(values) => console.log("Submitted:", values)}
/>
</div>
Expand All @@ -34,12 +34,12 @@ export default function App() {
}

function vueFiles(fixture: string) {
const schema = JSON.stringify(fixtures[fixture], null, 2);
const definition = JSON.stringify(fixtures[fixture], null, 2);
return {
'src/App.vue': `<` + `script setup>
import { FormRenderer } from "@formhaus/vue";

const schema = ${schema};
const definition = ${definition};

function onSubmit(values) {
console.log("Submitted:", values);
Expand All @@ -48,23 +48,23 @@ function onSubmit(values) {

<template>
<div style="max-width: 480px; margin: 24px auto; font-family: sans-serif">
<FormRenderer :schema="schema" @submit="onSubmit" />
<FormRenderer :definition="definition" @submit="onSubmit" />
</div>
</template>`,
};
}

function svelteFiles(fixture: string) {
const schema = JSON.stringify(fixtures[fixture], null, 2);
const definition = JSON.stringify(fixtures[fixture], null, 2);
return {
'App.svelte': `<` + `script>
// No adapter needed. This shows how to use @formhaus/core directly.
// The engine handles validation, visibility, and multi-step navigation.
// You only write the rendering.
import { FormEngine } from "@formhaus/core";

const schema = ${schema};
const engine = new FormEngine(schema);
const definition = ${definition};
const engine = new FormEngine(definition);

let fields = engine.visibleFields.slice();
let vals = Object.assign({}, engine.values);
Expand Down Expand Up @@ -115,7 +115,7 @@ function svelteFiles(fixture: string) {
</` + `script>

<div style="max-width: 480px; margin: 24px auto; font-family: sans-serif">
<h2>{schema.title}</h2>
<h2>{definition.title}</h2>
{#if multi && step}
<p style="color: #666; margin: 0 0 12px">{step.title}{step.description ? ' — ' + step.description : ''}</p>
{/if}
Expand Down Expand Up @@ -161,7 +161,7 @@ function svelteFiles(fixture: string) {
{#if multi && !first}
<button on:click={back}>Back</button>
{/if}
<button on:click={submit}>{multi && !last ? 'Continue' : schema.submit.label}</button>
<button on:click={submit}>{multi && !last ? 'Continue' : definition.submit.label}</button>
</div>
</div>`,
};
Expand Down
6 changes: 3 additions & 3 deletions docs/api/definition.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Definition Reference

Every form is a JSON object following the `FormSchema` type.
Every form is a JSON object following the `FormDefinition` type.

## FormSchema
## FormDefinition

Top-level structure. Use `fields` (single-step) or `steps` (multi-step), never both.

```ts
interface FormSchema {
interface FormDefinition {
id: string; // Unique form identifier
title: string; // Form title (rendered by the adapter or parent)
submit: FormAction; // Submit button config
Expand Down
10 changes: 5 additions & 5 deletions docs/guide/async-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function validateStep(stepId: string, values: Record<string, unknown>) {
function SignupForm() {
return (
<FormRenderer
schema={schema}
definition={definition}
onStepValidate={validateStep}
onSubmit={handleSubmit}
/>
Expand Down Expand Up @@ -61,7 +61,7 @@ async function validateStep(stepId, values) {

<template>
<FormRenderer
:schema="schema"
:definition="definition"
:on-step-validate="validateStep"
@submit="onSubmit"
/>
Expand Down Expand Up @@ -131,11 +131,11 @@ type StepValidateFn = (
```

The callback receives:
- `stepId` - the `id` of the step being validated (from your schema)
- `stepId` - the `id` of the step being validated (from your definition)
- `values` - all current field values (not just the current step's fields)

Return:
- `{ key: message }` - errors to show. Keys match field keys in your schema.
- `{ key: message }` - errors to show. Keys match field keys in your definition.
- `null` or `undefined` - no errors, advance to next step.

## Using the engine directly
Expand All @@ -145,7 +145,7 @@ If you use `FormEngine` without the React/Vue adapter, call `nextStepAsync()` in
```ts
import { FormEngine } from '@formhaus/core';

const engine = new FormEngine(schema, initialValues, {
const engine = new FormEngine(definition, initialValues, {
validators: { ... },
onStepValidate: async (stepId, values) => {
// your server call
Expand Down
16 changes: 8 additions & 8 deletions docs/guide/custom-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import MyFormActions from './MyFormActions.vue'

<template>
<FormRenderer
:schema="schema"
:definition="definition"
:actions-component="MyFormActions"
@submit="onSubmit"
/>
Expand All @@ -27,7 +27,7 @@ import { FormRenderer } from '@formhaus/react'
import { MyFormActions } from './MyFormActions'

<FormRenderer
schema={schema}
definition={definition}
ActionsComponent={MyFormActions}
onSubmit={handleSubmit}
/>
Expand Down Expand Up @@ -110,7 +110,7 @@ Pass your component via the `progressComponent` (Vue) or `ProgressComponent` (Re
```vue [Vue]
<template>
<FormRenderer
:schema="schema"
:definition="definition"
:progress-component="MyStepProgress"
@submit="onSubmit"
/>
Expand All @@ -119,7 +119,7 @@ Pass your component via the `progressComponent` (Vue) or `ProgressComponent` (Re

```tsx [React]
<FormRenderer
schema={schema}
definition={definition}
ProgressComponent={MyStepProgress}
onSubmit={handleSubmit}
/>
Expand All @@ -145,7 +145,7 @@ Use all custom component props together:
```vue [Vue]
<template>
<FormRenderer
:schema="schema"
:definition="definition"
:components="{ phone: CustomPhoneInput }"
:actions-component="MyFormActions"
:progress-component="MyStepProgress"
Expand All @@ -156,7 +156,7 @@ Use all custom component props together:

```tsx [React]
<FormRenderer
schema={schema}
definition={definition}
components={{ phone: CustomPhoneInput }}
ActionsComponent={MyFormActions}
ProgressComponent={MyStepProgress}
Expand Down Expand Up @@ -186,15 +186,15 @@ Track form interactions via `onAnalyticsEvent` (React) or `@analyticsEvent` (Vue
::: code-group
```tsx [React]
<FormRenderer
schema={schema}
definition={definition}
onAnalyticsEvent={(event) => analytics.track(event.type, event)}
onSubmit={handleSubmit}
/>
```

```vue [Vue]
<FormRenderer
:schema="schema"
:definition="definition"
@analytics-event="(event) => analytics.track(event.type, event)"
@submit="onSubmit"
/>
Expand Down
12 changes: 6 additions & 6 deletions docs/guide/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function onSubmit(values) {

<template>
<FormRenderer
:schema="schema"
:definition="definition"
:errors="serverErrors"
@submit="onSubmit"
/>
Expand All @@ -47,7 +47,7 @@ function MyForm() {

return (
<FormRenderer
schema={schema}
definition={definition}
errors={errors}
onSubmit={handleSubmit}
/>
Expand Down Expand Up @@ -86,7 +86,7 @@ Show a loading indicator on the submit button while the request is pending:
```vue [Vue]
<template>
<FormRenderer
:schema="schema"
:definition="definition"
:loading="isSubmitting"
@submit="onSubmit"
/>
Expand All @@ -95,7 +95,7 @@ Show a loading indicator on the submit button while the request is pending:

```tsx [React]
<FormRenderer
schema={schema}
definition={definition}
loading={isSubmitting}
onSubmit={handleSubmit}
/>
Expand All @@ -113,7 +113,7 @@ Some fields load data asynchronously (e.g. looking up a city by zip code). Use `
<script setup>
import { useFormEngine } from '@formhaus/vue';

const { engine } = useFormEngine(schema);
const { engine } = useFormEngine(definition);

async function onFieldChange(key, value) {
if (key === 'zipCode' && value.length === 5) {
Expand All @@ -127,7 +127,7 @@ async function onFieldChange(key, value) {
```

```tsx [React]
const engine = useFormEngine(schema);
const engine = useFormEngine(definition);

function onFieldChange(key, value) {
if (key === 'zipCode' && value.length === 5) {
Expand Down
Loading
Loading