loading...

Why it took me three weeks to implement a Login Form

vannsl profile image Vannsl ・5 min read

History

End of October I've published a post about how to authenticate a user with Email and Password with Firebase and Nuxt.js. The Setup takes roughly one hour. Until today I am not fully done to implement the full functionality of a Login Form. But why?

Because I care about the users. And I want to guide them using micro transitions and helpful messages.

Result

Here is my login form:

Login Form

Error Types

The problem was how to handle errors. The following errors could happen:

  • Wrong combination of email and password (Login)
  • Too many attempts of wrong combinations (Login)
  • Email address is already registered (Registration)
  • Password is too short (Registration)
  • Password-Check doesn't match to first password field (Registration)

The first four errors are thrown by Firebase, the last one is a custom error (Firebase does not care about a second password field for a registration). In other words: To receive the errors of Firebase the client will send a request to the server and wait for the response to resolve a promise. The double password check can be done by the client and would be done in no time. It would also be possible to validate the password (min. of 6 characters for Firebase) with JavaScript before sending it to Firebase.

The UX Story

Let me tell you the story I had in my mind during the implementation:

User: Ok, I've entered my credentials, what is going on?

Website: You should know that you may have to wait a few moments until Firebase responds. Here, I disable the the submit button, so you know, that you have triggered it correctly and don't need to push it again.

If the data attribute loading is true, the probably self explaining TailwindCSS classes opacity-50 and cursor-not-allowed get appended to the classlist of the button.

<!-- Vue templating -->
<button
  :class="{ 'opacity-50 cursor-not-allowed' : loading}"
  :disabled="loading"
  type="submit"
  v-text="buttonText"
/>

Shake

User: Help! Something is wrong!

Website: No need to worry! So, in a moment I'll check, which error has happened. But first, I want to show you, that an error has occurred. No, of course, I will not throw anything in your face - no popup, no push notification, no red colors. You really don't need to worry. I will just shake my head to tell you that something is not right.

The form is wrapped in a box with a lock SVG icon on top. This icon to "shakes its head" in case of any error. The class hasError gets appended to the icon component if hasError is true.

<!-- Vue templating -->
<keyhole-svg :class="{ shake: hasError }" />

For the animation, the CSS of the jsfiddle from Hassen Charef is used.

.shake {
  animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  transform: translate3d(0, 0, 0);
  backface-visibility: hidden;
  perspective: 1000px;
}

User: Okay, good, I'm calmed down. What now?

Website: No worries, I won't leave you. Let me check which error has happened. [...] Ok, your password is not long enough. I've marked the password fields and added a hint that you need a minimum of six characters. I haven't used a red color, because I don't want to give you the feeling that you've done something bad. It happens. I'll highlight them in a shiny and friendly yellow.

User: Ok, thanks, I'm trying again... oh, why are you shaking your head again? I've entered more than six characters, I promise!

Website: Yes, you did. I will delete the old error message and replace it with the new one: Both passwords did not match. I'll keep highlighting the password fields.

User: Good catch, I forgot to change the second password field. Wait a minute... Do I have a deja-vu. Was I here before?

Website: Maybe, maybe not. I won't tell you if this email address is already registered. Security, you know? :) So here, I need to keep you in the dark, sorry.

(Do you have any idea how to respond to email-already-in-use errors?)

Login

User: Oh no, a wrong combination of email and password. Eh... was it the password or email?

Website: I won't tell you that :) If I will you that the password was wrong, you would find out that the email address is registered at this portal. It sounds like a security risk, doesn't it? :) So, I guess you've lost your password? Do you want to reset it?

User: No, I will try again, maybe I can remember it...

Website: Sure, but you've tried a lot of times! Are you sure you are human? Are you a bot? I won't let you steal the user's credentials by guessing! I will block the form for a while now.

User: Sigh.. no, I just really forgot it.

Website: Maybe that was a bit harsh. The form is still blocked, do you want to use that situation to reset your password now?

User: Yes, thanks for offering again! But I'm on my phone and don't want to enter my long eMail address twice.

Website: I can totally understand this. I've already pre-filled the eMail field in the reset form with the eMail address you've entered before.

User: Thanks!

Website: After clicking on "Reset", I will reset here everything, too. So now you can have a fresh Login Form after you've got your new password. Bye bye!

The helpful error message component

The ErrorMessage.vue is one Single File Component (SFC) written in Vue.js. It is able to display all of the error types. By using Vuex the components receives the error messages set by the Login Component itself.

The markup of the ErrorMessage Component is wrapped inside a fade transition. That has a reason:

After the icon "shook its head" because of invalid input, the user should be guided to read the helpful error message. Without focus and concentration, the fade animation is not very obvious but can help to catch the user's attention.

Some errors require the question to reset the password. An array containing these errors checks the existency of every new error message.

Here is the ErrorMessage.Vue:

<!-- Vue.js component -->
<template>
  <transition name="fade">
    <div
      v-if="hasError"
      class="relative -mt-2 rounded-lg p-2 mb-4 border-4 border-yellow-300 bg-yellow-100 text-black"
    >
      {{ label }}
      <button
        v-show="showPasswordReset"
        class="font-bold no-underline hover:underline"
        @click.prevent="reset"
      >Forgot Password?</button>
    </div>
  </transition>
</template>

<!-- See script section in the code block below -->

<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
  max-height: 500px;
  transition: max-height 0.25s ease-in;
}
.fade-enter,
.fade-leave-to {
  max-height: 0;
  overflow: hidden;
}
</style>

Note to v-show: It would also be possible to use v-if. Personally, I tend to use v-if only if there is also an "else". More information can be found in the original documentation.

// Vue Script block, Linter: Prettier
<script>
import { mapGetters } from 'vuex'
export default {
  name: 'ErrorMessage',
  computed: {
    label() {
      switch (this.errorMsg) {
        case 'auth/email-already-in-use':
          return this.$t('registration.emailAlreadyInUse')
        case 'auth/password-mismatch':
          return this.$t('registration.passwordMismatch')
        case 'auth/weak-password':
          return this.$t('registration.weakPassword')
        case 'auth/wrong-password':
          return this.$t('registration.wrongPasswword')
        case 'auth/too-many-requests':
          return this.$t('registration.tooManyRequests')
      }
    },
    showPasswordReset() {
      return ['auth/email-already-in-use', 'auth/wrong-password'].includes(
        this.errorMsg
      )
    },
    ...mapGetters({
      hasError: 'auth/hasError',
      errorMsg: 'auth/errorMsg'
    })
  },
  methods: {
    reset() {
      this.$emit('reset')
    }
  }
}
</script>

And all of this error handling is why it took me three weeks to get it done. I am not regretting it.

Discussion

pic
Editor guide