Skip to main content

Form events

Each form created by fab4m provides several events that can be used to be notified about the form lifecycle. We provide events for when data changes, When data is submitted, and when data is being validated.

Listen for all data changes

Each time any component in the form changes the onDataChange event is fired. All of the form data is passed to the form as an object.

import {
createForm,
textField,
integerField,
FormView,
textAreaWidget,
} from "@fab4m/fab4m";
import React, { useState } from "react";

const form = createForm({
name: textField({ label: "Name" }),
bio: textField({ label: "Bio", widget: textAreaWidget() }),
age: integerField({ label: "Age" }),
});

export function OnChangeExample() {
// We set up a state within this component with some defaults.
const [data, changeData] = useState({
name: "Fabian Sörqvist",
bio: "This is my bio",
age: 33,
});
// When our data changes, we update our state, so that we have the
// current fresh form data available.
form.onDataChange((newData) => {
// This could be written just as form.onDataChange(changeData)
changeData(newData);
});
// We render our form using the FormView component, and render the
// information that is changed by the form on the fly.
return (
<>
<FormView form={form} data={data} hideSubmit={true} />
<div>
<dl>
<dt>Name</dt>
<dd>{data.name}</dd>
<dt>Bio</dt>
<dd>{data.bio}</dd>
<dt>Age</dt>
<dd>{data.age}</dd>
</dl>
</div>
</>
);
}

Listen for changes in one specific component

You might not want to handle the whole state, and instead only listen for changes in certain components. This can be done using the onComponentChange event.

Example

import { createForm, integerField, StatefulFormView } from "@fab4m/fab4m";
import React, { useState } from "react";

const ageForm = createForm({
age: integerField({ label: "Age" }),
});

export function OnComponentChangeExample() {
// We store the age in this state.
const [age, changeAge] = useState(undefined);
// When the age component is updated, we update our state.
ageForm.onComponentChange((name, value) => {
if (name === "age") {
changeAge(value);
}
});
// Render our form, and print out our current age when it's available.
return (
<>
<StatefulFormView form={ageForm} hideSubmit={true} />
{age && <p style={{ fontWeight: "bold" }}>You are {age} years old</p>}
</>
);
}

Handling form submits

Use the onSubmit function to handle form submissions. This function is called whenever the form is submitted and it's only called if the form data is valid, so you can rely on your data being complete at this point.

Example

import {
createForm,
textField,
integerField,
textAreaWidget,
StatefulFormView,
} from "@fab4m/fab4m";
import React, { useState } from "react";

const form = createForm({
name: textField({ label: "Name" }),
bio: textField({ label: "Bio", widget: textAreaWidget() }),
age: integerField({ label: "Age" }),
});

export function OnSubmitExample() {
// This state will be updated with the data when we submit the form.
const [submitted, changeSubmittedData] = useState(undefined);
form.onSubmit((e, data) => {
// The event is a React FormEvent.
e.preventDefault();
// We update the state with our data.
changeSubmittedData(data);
});
return (
<>
<StatefulFormView form={form} />
{submitted && (
<p style={{ fontWeight: "bold" }}>Welcome {submitted.name}</p>
)}
</>
);
}

Using events for custom validation

Sometimes you need to add custom validation for your form, for instance, if you need to verify that an email doesn't exist, or any other validation that requires data. You can use the onPartValidate event for this. the onPartValidate function is called each timea form part is submitted.

the listener is asynchronous, so any required server validation can occur within.

Errors encountered should be returned as an array with the following structure:

[{
path: "/component" // <--- This is a JSON pointer,
message: "This is the message that will be shown in the form"
}];

See the validator documentation for more information about validating your data.

tip

You can also define custom component validators. This works well when you only need the data from one specific component, but if you require the data from the full form you're probably better off using this event instead. See the documentation on how to create your own component validators.

Example

The example below performs validation on each of the form parts:

  • On part 1, we ensure that the provided age is over 18 and that the name doesn't contain "sam"
  • On part 2, We encourage the user to switch profession, if the name provided in part 1 contains "Fabian" and the text "Computer science" is part of the bio.
import React, { useState } from "react";
import {
createForm,
textField,
integerField,
booleanField,
textAreaWidget,
StatefulFormView,
pageBreak,
} from "@fab4m/fab4m";

const partValidateForm = createForm({
name: textField({ label: "Name" }),
age: integerField({ label: "Age" }),
confirmAge: booleanField({
required: true,
label: "I confirm I'm at least 18 years of age",
}),
break: pageBreak({}),
bio: textField({ label: "Bio", widget: textAreaWidget() }),
});
export function OnPartValidateForm() {
const [completed, changeCompleted] = useState(false);
partValidateForm
.onPartValidate(async (part, data) => {
if (part === 0) {
const errors = [];
if (data.age < 18) {
errors.push({ path: "/age", message: "You are under age" });
}
if (data.name.toLowerCase().includes("sam")) {
errors.push({ path: "/name", message: "Sam is always under age" });
}
return errors;
}
if (
part === 1 &&
data.name.toLowerCase().includes("fabian") &&
data.bio.toLowerCase().includes("computer science")
) {
return [{ path: "/bio", message: "Consider a different profession" }];
}
})
.onSubmit((e) => {
e.preventDefault();
changeCompleted(true);
});
return completed ? (
<p>Welcome, you're the right person for the job!</p>
) : (
<StatefulFormView form={partValidateForm} />
);
}

Adding multiple form listeners to the same event

Fab4m forms support multiple event listeners for the same event. By default we override the registered events when we add any event listener, because you normally add these events inside of a React component, and we want to provide the current event handler function to avoid weird scope errors.

If you want to use multiple events you can do that by telling the form to append the event:

form.onChange(() => {
// Listener one.
}, true).onChange(() => {
// Listener two.
}, true)