Skip to content

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.11

Find 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" src

For 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 ​

AreaOld (0.10)New (0.11)Action
Install@formhaus/core installed as a dependency of the adapter@formhaus/core is a peer dependencynpm i @formhaus/core @formhaus/react or @formhaus/vue
InstallReact >=18.0.0^18.0.0 || ^19.0.0Use React 18 or 19
InstallVue >=3.3.0^3.3.3Upgrade Vue to 3.3.3 or later
InstallNode 18Node 20 for @formhaus/core, react and vueUpgrade Node
Definition{ field } without an operator, or with twoExactly one operatorKeep one of eq, neq, in, notIn, notEmpty, gt, gte, lt, lte
DefinitionUnknown condition keys such as equals matched alwaysConstructor throwsRename to an operator
DefinitionevaluateCondition() returned true without a known operatorReturns falseGive every condition one operator
Definitionshow, showAny, route and disabled conditions saw all values without routesThey see the active projection: earlier steps on the path plus the same stepMove a referenced field to the same or an earlier step
DefinitionStep condition on a field of the same step worked without routesConstructor throwsMove the field to an earlier step
DefinitionCondition or matchField on a key that is not a field: warningConstructor throwsAdd the field, or remove the condition
DefinitionSame field key twice: warningConstructor throws, unless the steps are route branches that never meetRename one field
DefinitionInvalid pattern, condition cycle, minLength > maxLength, min > max, missing submit: warningsConstructor throwsFix the reported path
DefinitionMissing id, step id, field key or type, wrong property typesConstructor throwsFix the reported path
DefinitionFormAction.actionRemoved from the type and the JSON SchemaDelete action from actions
DefinitionNaN, Infinity or "5" in a number field passed"Enter a number"Store numbers
DefinitionEmpty field with matchField passedFails when the matched field has a valueAdd required to the confirmation field for "This field is required"
Definitionengine.validate() did not check email, dates, booleans or optionsSame type checks as validateSubmission()Store valid values
DefinitionsetValue() on a hidden field kept the value without routesCleared at onceRead values only while the field is visible
DefinitionValidators and hooks got all values without routesThey get the active projectionRead other context from your own state
ChecksvalidateDefinition(definition): string[]checkDefinition(definition): DefinitionIssue[]checkDefinition(d).filter((i) => i.severity === 'error')
CheckscheckDefinition() from /server returned { errors, warnings }DefinitionIssue[]Filter by severity
ChecksThe FormEngine constructor threw Error with joined messagesFormDefinitionError with issuescatch (e) { if (e instanceof FormDefinitionError) e.issues }
Checks[FormEngine] warnings on console.warn[formhaus] warnings, or the onWarning optionPass onWarning to collect them
ChecksvalidateSubmission() validated broken definitionsThrows FormDefinitionErrorRun checkDefinition() before you store a definition
Engineengine.nextStep(), nextStepAsync()await engine.next()Await it
Engineengine.prevStep(), prevStepAsync()await engine.back()Await it
Engineengine.skipStep(), skipStepAsync(submit)await engine.skip(submit)Await it
Engineengine.skip() skipped steps without skipReturns falseAdd skip: { label } or call next()
Engineengine.submitAsync(handler)engine.submit(handler)Rename
EngineActions moved even while another was pending: next()Return false while an action is pendingCall cancelPendingActions() first
Engineengine.values.x = v, assigning values, errors, currentStepIndexRead-only getterssetValue(), setErrors()
EngineStepChangeContext.directionreasoncontext.reason === 'back'
EngineSubmitFn with optional skippedStepsRequired string[]Pass skipped step ids when you call it
EngineonStepValidate and hooks with all valuesActive projectionSee the projection row
EnginevalidateField(field, value, values, validators) standalonevalidateFieldValue()Rename. engine.validateField(key) is unchanged
EngineisStepVisible()isVisible()Rename
EnginegetDefaultMessage(rule: string)getDefaultMessage(rule: ValidationRule)Pass a rule name
EngineshouldApplyExternalErrors from @formhaus/coreFrom @formhaus/core/internalChange the import
CloudcreateCloudController, initialCloudState, focusProblem from /cloudFrom @formhaus/core/internalChange the import, or use FormhausForm
CloudCloudCallbacks, CloudState types from /cloudFrom @formhaus/core/internalChange the import
Cloud@formhaus/core/cloud loaded GET /f/{id}/definitionGET /f/{id}/form?core=<version>Allow the path in proxies and CSP
CloudFormhausForm ignored the form's success message and redirect URLShows the message and opens the URL after a submissionPass success (React) or the #success slot (Vue) to keep your own handling
Analyticsform_abandoned event typeRemovedDelete handlers
ReactActionsComponent, ProgressComponentactionsComponent, progressComponentRename the props
ReactonStepChange(stepId, direction)onAfterStepChange(context)Read context.toStepId, context.reason
React, Vue<h3> step title<h2>, headingLevel propPass headingLevel={3} to keep <h3>
React, VueEngine recreated only when definition.id changedRecreated when the definition object changesPass a stable object
React, Vuesubmit.disabled read engine.valuesReads getSubmitValues()Nothing, unless you relied on off-path values
React, VueField ids email (React), fh-field-email (Vue)<form id>-<key>, sanitizedSelect by name or label
React, VueOption ids <key>-<value><field id>-<index>Select options by label
React, VueHelper and error ids <key>-helper<field id>-helper, <field id>-errorRead aria-describedby
Vue@submit notification after submitHandler@submit is the awaited handler, (values, skippedSteps)Move :submit-handler="save" to @submit="save"
VuestepChange eventonAfterStepChange propRead context.toStepId
VueCustom field value prop and update:value eventmodelValue and update:modelValueRename, v-model works
VueFormField componentFormhausFieldChange the import
VueFieldComponentMap = Record<FieldType, Component>Partial<Record<FieldType, Component>>Nothing for maps you pass
VueFormActions action eventRemovedDelete @action
MCPvalidate_definition returned { valid, errors, warnings }{ valid, issues }Read issues
MCPsimulate_path validation.errorsvalidation.issuesRead issues
MCPupdate_form_settings with reveal_secretreveal_webhook_secret toolCall the new tool
MCPupdate_form_settings with notify_emailnotify_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');

Formhaus is MIT licensed. Legal