Developing a UI with Vue, Part 5: The Multi-Step Form

Developing a UI with Vue, Part 5: The Multi-Step Form

In my last post , we developed the Login page by building custom input and button components. Now, we're going to focus on building a multi-step registration form to sign up new users.

The first time I built one of these I thought it was the coolest. I was also intimidated by the process, but after a few attempts and an understanding of what was happening, it became quite fun and simple to pull off.

Defining data properties

We're going to start by defining what fields we'll need to capture, and how many steps the user will have to go through:

  • Step 1 will gather the user's username and email
  • Step 2 will gather their password and confirmed password

In our Vue instance, we'll define those values as data properties. We're also going to add a number property called step, which we'll use to handle which piece of the form gets rendered to the user, as well as a boolean error flag.

// Register.vue

export default {
  name: 'Register',
  data() {
    return {
      step: 1,
      name: '',
      email: '',
      password: '',
      confirm_password: '',
      error: false,
    }
  },
}

Using v-if

To build the each step of our form, we'll create two <div> elements, each containing the inputs we'll want displayed depending on the step. We can use the v-if directive to provide this conditional logic.

<template v-slot:content>
  <form>
     <div v-if="step === 1">
        <BaseInput 
          label="Name" 
          placeholder="Enter your name..."                 
          v-model="name" 
          required
        />
        <BaseInput 
          label="Email"
          placeholder="Enter your email..." 
          v-model="email" 
          required
        />
     </div>
     <div v-if="step === 2">
       <BaseInput 
         label="Password" 
         placeholder="Enter your password..." 
         v-model="password" 
         type="password" 
         required
       />
       <BaseInput 
         label="Confirm password" 
         placeholder="Confirm password..." 
         v-model="confirmed_password" 
         type="password" 
         required
       />
    </div>
  </form>
</template>

Notice we're using v-model to bind each value to its property in the data() object, and that we've set up our input component to accept the required prop. That prop is getting passed to the component's native HTML input, which takes some validation work off of our plate!

With that, we've got two steps available to display:

vue_5_img2_register@2x.jpg

Now we need a way to change the step. Let's take a look at how to create handlers to get that done.

Handling the step

Each button component is set up to receive a @click handler as a prop. Here, we're going to define two handlers inside methods: one to increment the step, and one to decrement it.

export default {
  // ...rest of the object
  methods: {
    incrementStep() {
      const formFilled = this.username && this.email;
      if (this.step === 1 && formFilled) {
        this.step = this.step + 1;
      }
    },
    decrementStep() {
      if (this.step === 2) {
        this.step = this.step - 1;
      }
    },
  }
}

Each handler is set up with the following logic:

  1. incrementStep only works when step is equal to 1, so that it can only get as far as 2. I've also set up logic to ensure that username and email are provided before the user is able to move onto step 2.
  2. decrementStep only works when step is equal to 2, so that it can only get as far as 1.

When we attach those handlers to the buttons, we'll use some v-if directives too. On step 1, we want to allow the user to increment:

<BaseButton v-if="step === 1" @click="incrementStep">Continue</BaseButton>

And on step 2 the user can either go back, or submit.

<BaseButton v-if="step === 2" @click="decrementStep" variant="secondary">Back</BaseButton>
<BaseButton v-if="step === 2" type="submit">Confirm</BaseButton>

We'll place the buttons inside of a wrapper div in order to align them to the right with some spacing.

<form>
  <div v-if="step === 1"><!-- Step 1 inputs --></div>
  <div v-if="step === 2"><!-- Step 2 inputs --></div>
  <div class="button-wrapper">
    <BaseButton 
      v-if="step === 1" 
      @click="incrementStep">Continue</BaseButton>
    <BaseButton 
      v-if="step === 2" 
      @click="decrementStep" 
      variant="secondary">Back</BaseButton>
    <BaseButton 
      v-if="step === 2" 
      type="submit">Confirm</BaseButton>
  </div>
</form>

Now we can we interact with the form:

vue_5_img1_register@2x.jpg

Handling form submission

To bring it all together, we need to handle the submission of our form. Without a back-end, there's nowhere to really send it. But we can create the handler with some validation to get us started.

Just as we did with the Login page, we are going to add a @submit handler to our form with the prevent event modifier. Then, we'll include the handler as a method in our export object:

<form @submit.prevent="handleRegister">
    <!-- the form we've just built -->
</form>
export default {
    methods: {
      handleRegister() {
        const passwordsFilled = this.password && this.confirm_password;
        const passswordsMatch = this.password === this.confirm_password;
        if (passwordsFilled && passswordsMatch) {
          this.error = false;
          // perform form submission...
        } else if (!passswordsMatch) {
          this.error = true;
        }
      },
    }
}

Let's break down the above validation in handleRegister:

  1. We're making sure that password and confirm_password have values
  2. We're making sure that password and confirm_password match.

If both are truthy, we'll continue on with form submission and clear errors. Otherwise, we set error to true and inform the user of the error, which appears right above the first input inside of the "header" named slot.

<template v-slot:header>
  <h1>Register</h1>
  <p>Try out our watch list for free today and see what you think.</p>
  <p class="error" v-if="error">
    Either a password field is missing, or they do not match.
  </p>
 </template>

Now when there's a problem with the password, the user will see a message like this:

vue_5_img3_register – 1@2x.jpg

At this point, we've basically hit a dead end until we get login working and build the actual app.

That's why I'm going to be be turning my attention to the much discussed Supabase: the open-source alternative to Google's Firebase that's becoming quite popular. I've always been drawn to UI, so platforms like this are always exciting for me to work with. See you soon ✌️


You can find me on Twitter and Github , or you can check out my portfolio here . Feel free to reach out, I'd love to hear from you!