Migrate to 0.11
0.11 is the last breaking release before 1.0. Run the grep list below first, then work through the table. checkDefinition() reports every definition that 0.11 rejects, with a JSON pointer to the node.
bash
npm i @formhaus/core@^0.11 @formhaus/react@^0.11
npm i @formhaus/core@^0.11 @formhaus/vue@^0.11Find usages
Search the code base for each pattern. Every hit needs a change from the table.
bash
grep -rnE "nextStep\(|prevStep\(|skipStep\(|nextStepAsync|prevStepAsync|skipStepAsync|submitAsync|validateDefinition\(|isStepVisible|validateField\(field|shouldApplyExternalErrors|createCloudController|initialCloudState|focusProblem|form_abandoned|ActionsComponent|ProgressComponent|onStepChange|stepChange|step-change|submitHandler|submit-handler|update:value|\.direction|FormField\b|FieldComponentMap|engine\.(values|errors|currentStepIndex)\s*=|\"action\":|reveal_secret|notify_email" srcFor definitions stored as JSON, run the check on each file:
ts
import { checkDefinition } from '@formhaus/core';
const errors = checkDefinition(json).filter((issue) => issue.severity === 'error');
for (const issue of errors) console.log(issue.message);Changes
| Area | Old (0.10) | New (0.11) | Action |
|---|---|---|---|
| Install | @formhaus/core installed as a dependency of the adapter | @formhaus/core is a peer dependency | npm i @formhaus/core @formhaus/react or @formhaus/vue |
| Install | React >=18.0.0 | ^18.0.0 || ^19.0.0 | Use React 18 or 19 |
| Install | Vue >=3.3.0 | ^3.3.3 | Upgrade Vue to 3.3.3 or later |
| Install | Node 18 | Node 20 for @formhaus/core, react and vue | Upgrade Node |
| Definition | { field } without an operator, or with two | Exactly one operator | Keep one of eq, neq, in, notIn, notEmpty, gt, gte, lt, lte |
| Definition | Unknown condition keys such as equals matched always | Constructor throws | Rename to an operator |
| Definition | evaluateCondition() returned true without a known operator | Returns false | Give every condition one operator |
| Definition | show, showAny, route and disabled conditions saw all values without routes | They see the active projection: earlier steps on the path plus the same step | Move a referenced field to the same or an earlier step |
| Definition | Step condition on a field of the same step worked without routes | Constructor throws | Move the field to an earlier step |
| Definition | Condition or matchField on a key that is not a field: warning | Constructor throws | Add the field, or remove the condition |
| Definition | Same field key twice: warning | Constructor throws, unless the steps are route branches that never meet | Rename one field |
| Definition | Invalid pattern, condition cycle, minLength > maxLength, min > max, missing submit: warnings | Constructor throws | Fix the reported path |
| Definition | Missing id, step id, field key or type, wrong property types | Constructor throws | Fix the reported path |
| Definition | FormAction.action | Removed from the type and the JSON Schema | Delete action from actions |
| Definition | NaN, Infinity or "5" in a number field passed | "Enter a number" | Store numbers |
| Definition | Empty field with matchField passed | Fails when the matched field has a value | Add required to the confirmation field for "This field is required" |
| Definition | engine.validate() did not check email, dates, booleans or options | Same type checks as validateSubmission() | Store valid values |
| Definition | setValue() on a hidden field kept the value without routes | Cleared at once | Read values only while the field is visible |
| Definition | Validators and hooks got all values without routes | They get the active projection | Read other context from your own state |
| Checks | validateDefinition(definition): string[] | checkDefinition(definition): DefinitionIssue[] | checkDefinition(d).filter((i) => i.severity === 'error') |
| Checks | checkDefinition() from /server returned { errors, warnings } | DefinitionIssue[] | Filter by severity |
| Checks | The FormEngine constructor threw Error with joined messages | FormDefinitionError with issues | catch (e) { if (e instanceof FormDefinitionError) e.issues } |
| Checks | [FormEngine] warnings on console.warn | [formhaus] warnings, or the onWarning option | Pass onWarning to collect them |
| Checks | validateSubmission() validated broken definitions | Throws FormDefinitionError | Run checkDefinition() before you store a definition |
| Engine | engine.nextStep(), nextStepAsync() | await engine.next() | Await it |
| Engine | engine.prevStep(), prevStepAsync() | await engine.back() | Await it |
| Engine | engine.skipStep(), skipStepAsync(submit) | await engine.skip(submit) | Await it |
| Engine | engine.skip() skipped steps without skip | Returns false | Add skip: { label } or call next() |
| Engine | engine.submitAsync(handler) | engine.submit(handler) | Rename |
| Engine | Actions moved even while another was pending: next() | Return false while an action is pending | Call cancelPendingActions() first |
| Engine | engine.values.x = v, assigning values, errors, currentStepIndex | Read-only getters | setValue(), setErrors() |
| Engine | StepChangeContext.direction | reason | context.reason === 'back' |
| Engine | SubmitFn with optional skippedSteps | Required string[] | Pass skipped step ids when you call it |
| Engine | onStepValidate and hooks with all values | Active projection | See the projection row |
| Engine | validateField(field, value, values, validators) standalone | validateFieldValue() | Rename. engine.validateField(key) is unchanged |
| Engine | isStepVisible() | isVisible() | Rename |
| Engine | getDefaultMessage(rule: string) | getDefaultMessage(rule: ValidationRule) | Pass a rule name |
| Engine | shouldApplyExternalErrors from @formhaus/core | From @formhaus/core/internal | Change the import |
| Cloud | createCloudController, initialCloudState, focusProblem from /cloud | From @formhaus/core/internal | Change the import, or use FormhausForm |
| Cloud | CloudCallbacks, CloudState types from /cloud | From @formhaus/core/internal | Change the import |
| Cloud | @formhaus/core/cloud loaded GET /f/{id}/definition | GET /f/{id}/form?core=<version> | Allow the path in proxies and CSP |
| Cloud | FormhausForm ignored the form's success message and redirect URL | Shows the message and opens the URL after a submission | Pass success (React) or the #success slot (Vue) to keep your own handling |
| Analytics | form_abandoned event type | Removed | Delete handlers |
| React | ActionsComponent, ProgressComponent | actionsComponent, progressComponent | Rename the props |
| React | onStepChange(stepId, direction) | onAfterStepChange(context) | Read context.toStepId, context.reason |
| React, Vue | <h3> step title | <h2>, headingLevel prop | Pass headingLevel={3} to keep <h3> |
| React, Vue | Engine recreated only when definition.id changed | Recreated when the definition object changes | Pass a stable object |
| React, Vue | submit.disabled read engine.values | Reads getSubmitValues() | Nothing, unless you relied on off-path values |
| React, Vue | Field ids email (React), fh-field-email (Vue) | <form id>-<key>, sanitized | Select by name or label |
| React, Vue | Option ids <key>-<value> | <field id>-<index> | Select options by label |
| React, Vue | Helper and error ids <key>-helper | <field id>-helper, <field id>-error | Read aria-describedby |
| Vue | @submit notification after submitHandler | @submit is the awaited handler, (values, skippedSteps) | Move :submit-handler="save" to @submit="save" |
| Vue | stepChange event | onAfterStepChange prop | Read context.toStepId |
| Vue | Custom field value prop and update:value event | modelValue and update:modelValue | Rename, v-model works |
| Vue | FormField component | FormhausField | Change the import |
| Vue | FieldComponentMap = Record<FieldType, Component> | Partial<Record<FieldType, Component>> | Nothing for maps you pass |
| Vue | FormActions action event | Removed | Delete @action |
| MCP | validate_definition returned { valid, errors, warnings } | { valid, issues } | Read issues |
| MCP | simulate_path validation.errors | validation.issues | Read issues |
| MCP | update_form_settings with reveal_secret | reveal_webhook_secret tool | Call the new tool |
| MCP | update_form_settings with notify_email | notify_mode | "milestones" or "off" |
Definitions
One operator per condition
Before:
json
{ "key": "detail", "type": "text", "label": "Detail", "show": [{ "field": "kind" }] }After:
json
{ "key": "detail", "type": "text", "label": "Detail", "show": [{ "field": "kind", "notEmpty": true }] }Conditions on later steps
Before, without routes, a field on step 1 could depend on step 2:
json
{
"steps": [
{ "id": "one", "title": "One", "fields": [{ "key": "x", "type": "text", "label": "X", "show": [{ "field": "y", "eq": "yes" }] }] },
{ "id": "two", "title": "Two", "fields": [{ "key": "y", "type": "text", "label": "Y" }] }
]
}After, the field it depends on comes first:
json
{
"steps": [
{ "id": "one", "title": "One", "fields": [{ "key": "y", "type": "text", "label": "Y" }] },
{ "id": "two", "title": "Two", "fields": [{ "key": "x", "type": "text", "label": "X", "show": [{ "field": "y", "eq": "yes" }] }] }
]
}Comparisons
New in 0.11. Scores from radio options compare as numbers:
json
{ "key": "problem", "type": "textarea", "label": "What went wrong?", "show": [{ "field": "score", "lte": 6 }] }Actions
Remove action from submit, cancel, next, back and skip:
json
{ "submit": { "label": "Send", "action": "send" } }json
{ "submit": { "label": "Send" } }Code
Engine actions
ts
// 0.10
engine.nextStep();
await engine.nextStepAsync();
engine.prevStep();
await engine.submitAsync(save);ts
// 0.11
await engine.next();
await engine.back();
await engine.submit(save);Read-only state
ts
// 0.10
engine.values.email = '[email protected]';
engine.errors = { email: 'Taken' };ts
// 0.11
engine.setValue('email', '[email protected]');
engine.setErrors({ email: 'Taken' });Definition errors
ts
// 0.10
const problems = validateDefinition(json);
try { new FormEngine(json); } catch (error) { console.log(String(error).split('\n')); }ts
// 0.11
import { checkDefinition, FormDefinitionError, parseDefinition } from '@formhaus/core';
const errors = checkDefinition(json).filter((issue) => issue.severity === 'error');
try {
const definition = parseDefinition(json);
} catch (error) {
if (error instanceof FormDefinitionError) console.log(error.issues);
}React props
tsx
// 0.10
<FormRenderer definition={definition} onSubmit={save} ActionsComponent={Actions} onStepChange={(id) => track(id)} />tsx
// 0.11
<FormRenderer definition={definition} onSubmit={save} actionsComponent={Actions} onAfterStepChange={({ toStepId }) => track(toStepId)} />Vue submit and custom fields
vue
<!-- 0.10 -->
<FormRenderer :definition="definition" :submit-handler="save" @step-change="track" />vue
<!-- 0.11 -->
<FormRenderer :definition="definition" @submit="save" :on-after-step-change="({ toStepId }) => track(toStepId)" />vue
<!-- 0.11 custom field -->
<script setup lang="ts">
import type { FormFieldProps } from '@formhaus/vue';
const props = defineProps<FormFieldProps>();
const emit = defineEmits<{ (e: 'update:modelValue', value: unknown): void }>();
</script>
<template>
<input :id="props.id" :value="props.modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" />
</template>Tests that select by id
ts
// 0.10
document.querySelector('#email');ts
// 0.11
document.querySelector('[name="email"]');
screen.getByLabelText('Email');