Use ALL the Features: How To Create a Fancy Password Input With Vue3 🔑✅

Pascal Thormeier - Apr 1 '23 - - Dev Community

Vue 3 has brought us a ton of fancy new features. The composition API lets us create components in a more intuitive, JS-y way, there is now support for multiple v-model directives, full TypesSript support out of the box, and the reactivity improved a lot when compared to Vue2.

Today, we'll put these things to use to create a fancy password input component: One that lets us define password requirements and informs the outer component and the user about the password validity and the password.

The basis

First, we create a new Vue component called PasswordInput.vue. In there, we add some template, an amepty script tag and some styling:



<template>
  <div class="password-input">
    <label for="password">
      Enter password
    </label>

    <!-- We'll add stuff to this input later -->
    <input
      type="password"
      id="password"
    >

    <button>
      Show password
    </button>

    <label for="password-repeat">
      Repeat password
    </label>

    <!-- We'll add stuff to this input later -->
    <input
      type="password"
      id="password-repeat"
    >

    <ul class="requirements">
      <!-- Add requirements here -->
    </ul>
  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
.password-input {
  display: flex;
  flex-direction: column;
}

label {
  margin-bottom: 8px;
  font-size: 16px;
}

input {
  margin-bottom: 12px;
  padding: 8px;
  font-size: 16px;
}

button {
  margin-bottom: 12px;
}

.requirements {
  font-weight: bold;
}

.is-success {
  color: #96CA2D;
}

.is-error {
  color: #BA3637;
}
</style>


Enter fullscreen mode Exit fullscreen mode

We added two input fields for password and password repeat, a button that will show and hide the password as well as an unordered list for the password requirements that we'll add later.

Notice how we're using <script setup lang="ts"> to tell Vue that we want to use the composition API for the entire component.

The styling already includes classes for success and failure. The rest is rather rudimentary. When we use the component, we should see something like this:

Two input fields with labels and a button.

Hooking up the fields

Let's add two refs for the password and password repeat. We also add a flag to determine if the password is currently shown or not. While we're at it, we also import computed and watch, because we'll use that later. For that, we add the following to the script tag:



import { computed, ref, watch } from 'vue'

const password = ref('')
const passwordRepeat = ref('')
const showPassword = ref(false)


Enter fullscreen mode Exit fullscreen mode

We can now hook these up with the template:



<!-- ... -->
    <label for="password">
      Enter password
    </label>

    <input
      :type="showPassword ? 'text' : 'password'"
      v-model="password"
      @change="$emit('update:password', $event.target.value)"
      id="password"
    >

    <button @click="showPassword = !showPassword">
      {{ showPassword ? 'Hide password' : 'Show password' }}
    </button>

    <label for="password-repeat">
      Repeat password
    </label>

    <input
      :type="showPassword ? 'text' : 'password'"
      v-model="passwordRepeat"
      id="password-repeat"
    >
<!-- ... -->


Enter fullscreen mode Exit fullscreen mode

Adding the requirements

Now comes the fun part. We want to validate the password with the following requirements:

  • It should be at least 8 characters long
  • It should contain at least one uppercase letter
  • It should contain at least one lowercase letter
  • It should contain at least one symbol
  • It should contain at least one number
  • It must match the "Repeat password" field

To get Vue's reactivity in, we can create a computed value that checks the password ref for all fo these:



const passwordRequirements = computed(() => ([
  {
    name: 'Must contain uppercase letters',
    predicate: password.value.toLowerCase() !== password.value,
  },
  {
    name: 'Must contain lowercase letters',
    predicate: password.value.toUpperCase() !== password.value,
  },
  {
    name: 'Must contain numbers',
    predicate: /\d/.test(password.value),
  },
  {
    name: 'Must contain symbols',
    predicate: /\W/.test(password.value),
  },
  {
    name: 'Must be at least 8 characters long',
    predicate: password.value.length >= 8,
  },
  {
    name: 'Must match',
    predicate: password.value === passwordRepeat.value,
  }
]))


Enter fullscreen mode Exit fullscreen mode

Using an array of requirements has several advantages:

  1. We can extend it whenever we like
  2. Rendering it is trivial with a v-for loop
  3. We can reuse the predicates and move them to a utils file or use them in a state management system

Let's render the requirements in the list:



<!-- ... -->
    <ul class="requirements">
      <li
        v-for="(requirement, key) in passwordRequirements"
        :key="key"
        :class="requirement.predicate ? 'is-success' : 'is-error'"
      >
        {{ requirement.name }}
      </li>
    </ul>
<!-- ... -->


Enter fullscreen mode Exit fullscreen mode

Now, whenever the user changes either one of the values, the requirements are recalculated and updated in the template, giving feedback to the user directly. We should now see something like this:

The same password input as above, but now with a list of fulfilled and unfulfilled requirements

So far so good!

Emitting changes and usage

We now want to make the component usable. For that, we need to emit changes. Since Vue now allows to have multiple v-model directives on a component, we can emit different events in case of changes to either password field. For that, we add @change event listeners to the password input fields:



<!-- ... -->
    <input
      :type="showPassword ? 'text' : 'password'"
      v-model="password"
      @change="$emit('update:password', $event.target.value)"
      id="password"
    >

<!-- ... -->

    <input
      :type="showPassword ? 'text' : 'password'"
      v-model="passwordRepeat"
      @change="$emit('update:passwordRepeat', $event.target.value)"
      id="password-repeat"
    >
<!-- ... -->


Enter fullscreen mode Exit fullscreen mode

We then also want to emit if the password is valid or not, so a single boolean value. We use a watch for that, that will watch the passwordRequirements compured value and emit a single boolean.



const emit = defineEmits([
  'update:password',
  'update:passwordRepeat',
  'update:validity'
])

watch(passwordRequirements, () => {
  emit(
    'update:validity',
    passwordRequirements.value.reduce((v, p) => v && p.predicate, true)
  )
})


Enter fullscreen mode Exit fullscreen mode

And done! The component now emits changes for the password, the password-repeat field, and the password validity. It also shows the user direct visual feedback about the password's validity and let's them show or hide the password to see if they perhaps mistyped.

We can now use the component as follows in any template:



<template>
  <!-- ... -->
  <PasswordInput
    v-model:password="password"
    v-model:password-repeat="passwordRepeat"
    v-model:validity="isPasswordValid"
  />
  <!-- ... -->
</template>


Enter fullscreen mode Exit fullscreen mode

Awesome!


💡 Want to learn more about Vue and its advanced features? I recently published a course on Educative.io called "Advanced VueJS: Build Better Applications Efficiently"!

In 51 lessons, you can learn about all kinds of features, such as custom events, advanced templating, how to use APIs, I18n or slots and hooks.

You can find the course over on Educative.io!


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❤️! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee or follow me on Twitter 🐦! You can also support me directly via Paypal!

Buy me a coffee button

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .