Some time ago, I wrote about the need for simple HTML5 form validation in ~6 lines of JavaScript. Well, I’m back on my bullshit, this time in a Vue 2/Nuxt application. I finally hit the point on this project where I needed some form validation. Rather than jumping headlong into a whole form builder library dependency, I thought I’d try my simple method again. Here’s a demo of it in action.

See the Pen Happier HTML5 Form Validation in Vue by Dave Rupert (@davatron5000) on CodePen.

There are a few differences compared to the vanilla DOM implementation. The non-framework version relied on querySelectorAll and addEventListener to loop through inputs, which I guess you could do inside a Vue component, but feels a bit like an anti-pattern, so I had to modify the approach a smidge.

<template>
  <form :class="errors ? 'errors' : false">
    <label for="email">Email</label>
    <input id="email" type="email" required @invalid="invalidateForm"/>
  </form>
</template>

First step is I’m applying an .errors styling class to the parent <form> when one of the inputs invalidates. This is triggered by the component-level this.errors state. Originally the class was put on the input, but we can safely put it on the form instead of each individual input’s state.

The second bit is the @invalid bind directive on each required input. This replaces looping through each input and adding an addEventListener but has the same effect of attaching a method to the invalid event. The invalidateForm() method helps us visually invalidate the form and just like before HTML does all the submit event cancellation for us.

<script>
export default {
  data() {
    return {
      errors: false
    }
  }
  methods: {
    invalidateForm: function() {
      this.errors = true
    }
  }
}
</script>

We initialize the form component with errors: false because we don’t want the error styling until the user has submitted the form. The invalidateForm function just sets this.error = true. That’s one problem with the CSS :invalid pseudo class, it’s way too eager. Hooking into the invalid events, delays the styling until after the first form submission attempt and we know the form has errors.

<style lang="scss">
form.errors {
  :invalid {
    outline: 2px solid red
  }
}
</style>

Now, when the form has errors, then we can apply the :invalid styles. I put this in as a global style but you do you.

Final thoughts

Aggressive form validation situations like on blur or on change might change the utility of this approach and you’d probably need some kind of input-level state management. I’m trying to avoid a component-per-input situation, so I’m hoping I don’t run into this and even if I had that concern I could maybe layer it into this approach.

Overall, I’m pleased again with this lightweight solution. It’s a bit repetitive across form pages in my Nuxt app, but I’m able to yank it out if we need a library for more robust error handling. Maybe I can find a way to abstract this, because that is something I do miss from the querySelectorAll approach, I just had to write it once, or hang it off of selector, but in a Vue context, I’m doing this n times for every page with a form rather than just once. I irrationally loathe wrapper components, but maybe there’s a way to make this less repetitive.