Few interactions on the web cause as much displeasure for a user as being confronted with a large, intimidating form. Multi-step forms can alleviate this pain by breaking a large form into smaller approachable steps — but they can also be complicated to build.
In this guide, we'll walk through building a multi-step form with FormKit and see how we can provide an elevated user experience with minimal code. Let's get started!
Let's begin by laying out the requirements for our multi-part form:
First, let's create a basic form without steps so we have content to work with. Our example will be a pretend application to receive a grant, so we'll organize the form into 3 sections — "Contact Info", "Organization Info", and "Application". These will become the full form steps later.
We'll include a mix of validation rules for each input, and limit each section to 1 question for now until we have the full structure in place. Lastly, for the purposes of this guide, we'll output the collected form data at the bottom of each example:
Now that we have a defined structure, let's break the form into distinct sections.
To start, let's wrap each section of inputs with a group (<FormKit type="group" />
) so we can validate each group independently. FormKit groups are powerful because they are aware of the validation state of all their descendants without affecting the markup of your form.
A group itself becomes valid when all its children (and their children) are valid:
In our case, we're also going to want wrapping HTML. Let's put each group into a "step" section which we can conditionally show and hide:
Next, let's introduce some navigation UI so we can toggle between each step:
Here's what it looks like put together:
It's starting to look like a real multi-step form! There's more work to be done though as we've got a few issues:
Let's address the first issue.
FormKit already tracks group
validity out-of-the-box. We'll just need to capture this data so we can use it in our UI.
One important concept to remember about FormKit is that every <FormKit>
component has a matching core node, which itself has a reactive node.context
object. This context
object tracks the validity of the node in context.state.valid
. As mentioned above, a group
becomes valid when all its descendants are valid. With that in mind, let's build up an object that stores the reactive validity of each of the groups.
We'll leverage FormKit's plugin functionality to do this job. While the term "plugin" may sound intimidating, plugins in FormKit are just setup functions that are called when a node is created. Plugins are inherited by all descendants (such as children within a group).
Here's our custom plugin, called stepPlugin
:
The resulting steps
reactive object from our plugin above looks like this:
To use our plugin, we'll add it to our root form <FormKit type="form" />
. This means that every top-level group in our form will inherit the plugin:
Now that our template has real-time access to each group's validity state via our plugin, let's write the UI to show this data in the step navigation bar.
We also no longer need to manually define our steps since our plugin is dynamically storing the name of all groups in the steps
object. Let's add a data-step-valid="true"
attribute to each step if it's valid so we can target with CSS:
With these updates, our form is now capable of informing a user when they have correctly filled out all of the fields in a given step!
We'll also make a few other improvements:
activeStep
.Showing errors is more nuanced. Though the user may not be aware, there are actually 2 types of errors we need to handle and communicate to the user:
messages
of type validation
)messages
of type error
)FormKit uses its message store to track both of these types of errors/messages.
With our plugin already in place, it's relatively simple to add tracking for both:
messages
of type validation
), and errors (messages
of type error
).
Let's update our example to show both types of errors with the following requirements:
Since "blurring a group" doesn't exist in HTML, we'll introduce it in our plugin with an array called visitedSteps
. Here's the relevant code:
You might be wondering why we are walking all of the descendants of a given step (node.walk()
) and creating messages with a key of submitted
and value of true
? When a user attempts to submit a form, this is how FormKit informs itself that all inputs are in a submitted
state. In this state, FormKit forces any blocking validation messages to appear. We are manually triggering the same thing in our "group blur" event.
We'll use the same UI for both types of errors since end-users don't really care about the distinction. Here's our updated step HTML, which outputs a red bubble with the sum total of the errors errorCount
+ blockingCount
:
We are almost to the finish line! Here's our current form — which can now tell a user when they have properly or improperly filled out each step:
The last piece of the puzzle is submitting the form and handling any errors we receive from the backend server. We'll fake the backend for the purposes of this guide.
We submit the form by adding an @submit
handler to the <FormKit type="form">
:
And here's our submit handler:
Notice that FormKit passes our submit handler 2 helpful arguments: the form's data in a single request-ready object (which we're calling formData
), and the form's underlying core node
, which we can use to clear errors or set any returned errors using the node.clearErrors()
and node.setErrors()
helpers, respectively.
setErrors()
takes 2 arguments: form-level errors and field-specific errors. Our fake backend returns the err
response which we use to set any errors.
So, what happens if the user is on step 3 (Application) when they submit, and there are field-level errors on a hidden step? Thankfully, so long as the nodes exist the DOM, FormKit is able place these errors appropriately. This is why we used a v-show
for the steps instead of v-if
— The DOM node needs to exist in order to have errors set on the corresponding FormKit node.
And Voilà! 🎉 We are finished! In addition to our submit handler, we've added some more UI and UX flourishes to this final form to make it feel more real:
utils.js
that returns errors.valid
state.Here it is — a fully functioning multi-step form:
Of course, there are always ways to improve anything, and this form is no exception. Here are a few ideas:
window.localStorage
so a user's form state is maintained even if they accidentally leave.We've covered a lot of topics in this guide and hope you've learned more about FormKit and how to use it to make multi-step forms easier!