Building a Multi-Step Form with Vue

Alexandre Olival
Engineering Samlino
7 min readNov 7, 2019
Man filling out a paper form.
Photo by Helloquence on Unsplash

Recently at Samlino.dk the engineering team was tasked with creating a form that would allow users to request personal loan offers from different banks.

While we did have a similar feature before in production, it relied on an outdated front-end, rendered as a “widget” of sorts, that required us to import the code via a <script> tag.

We are in the process of splitting a huge, “one size fits all”, monolithic codebase (the challenges of which will be the subject of another blog post). So this feature was the perfect step (pun intended) in our journey to centralize and modernize our front-end code.

Disclaimer: the point of this article is to show a strategy, not to be a tutorial.

Where to begin?

So how do we tackle this? First, let us look through the technical requirements:

  • It should be easy to extend and change.
  • It should feel like a desktop wizard (e.g. no page reloads in between steps).
  • It should have an ergonomic user experience (e.g. persisting data between each step).
  • It should have three steps, but optionally a fourth depending on user input.

Sounds like we need something similar to a small-scale Single Page Application, limited to a component on a static page served by our back-end.

We also need a way to store data in between steps so the user doesn’t lose their progress in case they move backwards to change something.

This sounds like a job for a Flux Architectural Pattern. In simple terms, different components (our form “steps”) share a single source of truth, the store, for their data. But why?

Normally developers fall into this trap when first presented with a similar set of requirements: they tend to underestimate the complexity and size of the data that they will be dealing with, and try to handle something like a multi-step form by conditionally hiding the steps or aggregating data manually from each of the components. This quickly becomes unmanageable.

However, for a really small form, using the Flux pattern may also be overcomplicating. In the words of the creator of Redux (an implementation of this pattern in React):

Let’s get started!

We’ll be using our weapons of choice: Vue.js, Vuex and the Vue Router.

Let’s start by creating our components. Given that we need four, we’ll name them FirstStep, SecondStep and so on:

Code for the FirstStep.vue component file.
FirstStep.vue

But uh-oh: we’re still using local component data here!

In order to take advantage of Vuex, and the Flux pattern, we need to declare a store.

A store is basically where we declare all the data that will be shared by our components. It’s also where we declare getters, mutators, actions and all the other ways to manipulate our data. Our components can then access and modify that data, rather than the one they have locally.
Let’s declare our store then:

Code for the store.js file, the Vuex store.
store.js

A little note on mutations: we could pass the value directly as the second parameter for the mutation rather than a “payload” object. This would allow us to include the mutations in our components in a much more concise way. However, we prefer this way because it allows us to better visualize the data using the Vue Developer Tools in our browser. And we’re all about software quality!

Now that we have our single source of truth, let’s modify the First Step component to use our store rather than its local data:

FirstStep.vue code now modified to include store mutations.
FirstStep.vue — now with more Vuex!

There we go! Now our component works the same with reactivity, except data will be stored and retrieved from a centralized location. And if you want, you can still have local data in your component.

Declaring our router

It’s now time to go through the painful task of declaring all the routes for all our different step components:

Code for the router.js file, the Vue router module.
router.js

Well… that wasn’t too bad. Moving on.

Navigating between steps

So far we tackled two out of four requirements. Using a framework and a suitable pattern (both properly) makes our code easy to understand, learn and extend.

We also have a single source of truth, and data is persisted between components such that no data is lost while the user navigates.

However, what good is it to have different steps and routes for each if we can’t navigate between them? Let’s change that. How about if we create a small component to handle this? Let’s set some goals:

  • Have “previous” and “next” buttons (duh…).
  • Prevent the “previous” button from being clickable when on the first step.
  • Change the “next” button text, depending on the fourth step being active.

Let’s declare our component then:

Code for the Navigation.vue component file.
Navigation.vue

Yikes, that’s a lot of code. Let’s unpack this:

We declare two buttons on our template. The “previous” button can be hidden using a v-show directive. We pass a method to it (isFirstStep) that determines, you guessed it, if it is the first step of our form. That method simply checks with the router if our current step is named after the first.

Next we declare our “next” button. But because its text will depend on whether we have a fourth step, we use a method to render it. That method, aptly named nextButtonText, checks with the router and our store if we are both in the third step and with the hasFourthStep variable set to “true” on our store. If that is the case, we return “Next” as the text to use. Otherwise, “Finish”.

The last two methods, navigateNext and navigatePrevious should be easy to understand now. The former uses a similar strategy to nextButtonText to decide if it should render the next route, or call a stub submit method.

Now all we have to do is include this component in all our step components:

Code for FirstStep.vue with the Navigation component included.
FirstStep.vue now with more Navigation!

Protecting our optional route

We have now tackled all of our requirements, but we can do better with our final one. Although we prevent the user from accessing the optional step with the button, nothing stops them from manually accessing the route by typing “/4” and hitting enter.

That guy who tries this. What a Hacker. Photo by NeONBRAND on Unsplash

Luckily, the Vue Router has a way to protect this. We can intercept the “request” when changing routes and decide the behavior, by using a “Navigation Guard”:

Code for the navigation guard.
app.js — alternatively, whatever file you declared your router in

Now, every time the user attempts to access the “/4” route, and the hasFourthStep variable has a “false-y” value, nothing will happen.

Some of you might be screaming at the screen right now looking at that else, but don’t get wise here! It needs to be like that. From the Vue Router documentation:

Make sure that the next function is called exactly once in any given pass through the navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors.

And with that, we have made sure that the last optional step isn’t illegally accessed.

Bonus: loading saved data from the back-end

For every visitor on our form, we assign a unique identifier on the URL we call Journey ID.

This allows us to save the data the user inputs as they complete the form. In case a user leaves accidentally (or just feels like completing the form later) we can then fetch their progress by matching that ID with the one from the cache.

So how could we reload the data onto our store?

As soon as the static page loads, we set a variable on our global window instance like so:

Next, we define a mutator on our store to change all of its values:

Object.assign() is a cool ES6 function that does exactly what it says. It will assign the values of the incoming object to our state object, while leaving the ones that don’t get replaced untouched.

Now all we need is to call this mutation upon the root Vue component render function, and just like that, we can restore the user’s progress!

Thank you for reading if you made it this far!

Feel free to reach me on Twitter or GitHub. I’ll be happy to answer any questions you have. Until next time!

--

--

Alexandre Olival
Engineering Samlino

Developer with a tendency to multiply database queries way past the intended amount.