Introduction

Every SPA has its library of input groups. And when such an extensive work as wrapping every input type into a label and adding consistent styles between them is done, developers are tempted to handle forms by themselves. It doesn’t look too hard but turns into Greenspun’s tenth rule all the time.

Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.

Substitute C to home-made abstractions, Common Lisp to built-in browser APIs, and here you are.

I spent two days to understand how to handle forms and to build my lib if NIH hits me hard in the process. Thankfully, it didn’t.

When I need a forms library, I need validations handling in all its complexity.

  • Cross-field validation. The most common example of such validation is password + password confirmation;
  • Errors from the backend. These may be quite a puzzle because backends are different; responses can’t always be automatically matched to inputs. So some flexibility here definitely won’t hurt.
  • Strategies of displaying validation errors. When to show messages, when to hide them, when to change invalid indications to valid, all that stuff.

I don’t need

  • form groups as label + input + whatnot, they are different on every site
  • nested forms: while there is a convention of sending nested attributes as name_of_nested[], this has nothing to do with a forms lib. Well, you can abstract input name, but it’s penny-wise.
  • submit: forms should send formData, no extra juggling needed. Except handling errors from the server, but it’s a validation problem again.

TL;DR: There are plenty of browser APIs and pseudo-classes, but not enough for complex cases. If you feel drowning in boilerplate and broken reactivity, use vue-form. If you feel like “the final push and I’ll be there,” don’t fool yourself and use vue-form.


I checked four options and have a clear favorite.

HTML5

MDN, but I’ll outline a bit.

You can reach pretty far without a library for forms. Pseudo-classes like :valid, :invalid, :focus, and :placeholder-shown allow indicating an error when it’s needed. If you can keep browser validation popup and don’t need a custom error message, you can get away with that only.

input:invalid { border: 1px solid red; }

If you want to display error message in another element, but it is constant, :before or :after pseudo-elements are to help. Well-written, concise requirements, and not too strict rules might look no worse than reactivity. There is no way to get the actual message except for calling a js method, though.

.password-input:invalid::after {
  content: 'Password should contain a letter, a number, first ten digits of Pi and current date in ISO 8601.'
}

Then goes element.checkValidity and element.validationMessage in onblur, oninput, onchange callbacks and so on. This level brings much more pain and intense desire not to abstract all this by yourself and bring a library. But for a couple of forms could work.

element.setCustomValidity helps with server response and cross-element validations.

Things get even more complicated with reactive frameworks. You don’t have control over your DOM and should always take into account that your form gets re-rendered all the time. This fact means that there should be a state object that persists between renders. HTML standard describes the validation state pretty clearly. If you can and want to handle it all yourself, you should release your own forms library. But vue-form already exists.

vue-form

link, read it carefully.

I rarely say it, but I understand what every piece of this lib is for, and I don’t mind it. Moreover, I am amazed by how the lib author used vue abilities and their own creativity to build a very concise and flexible instrument.

  • <validate /> validates inputs
  • <vue-form :state="formstate" /> is a form state copied to object. And this is the way to combine reactivity and forms. Implemented carefully.
  • <field-messages /> picks up errors from form state and passes them to slots.

Good thing that it uses HTML attributes from <input />s.

Nested forms and adding extra inputs work without extra efforts as formstate is a parameter and survives between renders. Looks something like that:

<div v-for="(account, i) in model.accounts" :key="i">
  <validate auto-label>
    <label>Site</label>
    <input type="text" :name="`accounts[${i}].sitename`" required v-model="model.accounts[i].sitename" />
    <field-messages auto-label :name="`accounts[${i}].sitename`"  show="$touched || $submitted">
      Success!
      <div slot="required">required</div>
    </field-messages>
  </validate>
</div>

Different strategies of displaying errors can be flexibly implemented using $dirty, $valid, $touched, $focused, $submitted flags and v-modal sorcery: v-model listens for the input event, v-model.lazy doesn’t, only blur.

Server errors should not be part of the validation but should use an input state. E.g.,

<validate auto-label>
  <input name="my-input-name" />
  <div class="server-error" v-show="formstate['my-input-name'] && !formstate['my-input-name'].$touched">
    {{serverErrors['my-input-name']}}
  </div>
</validate>

Overall It’s a minimal possible wrapper that doesn’t seem to leak. Like it.

Full example:

vee-validate

link

<ValidationProvider /> wraps an element and gives validation error as slot prop. <ValidationObserver /> wraps several elements for cross-element validation or just form handling. Alien rules for that, but that’s possible. Validation works not all the time, even in examples.

Validation rules should be set in <ValidationProvider /> instead of the input. Not a big deal, but meh.

The lib has 27 built-in validation rules. Nice, indicates that authors use it on some mature site.

Localization presence seems weird. I think it should be a separate lib.

Unnecessary verbose and reinvents too many wheels, but tries hard.

vue-simpleform

link

Formik inspired it. Formik is a famous library for react. I’m not a fan, but it’s one of the best libs in the ecosystem.

<SimpleForm /> provides validate and handleSubmit callbacks.

The lib is brutally honest, doesn’t even pretend to support HTML attributes. Validate is a js function. You can bring a custom validation library (people like yup).

Has touched and submitted flags like vue-form, doesn’t have focused and dirty. Backend responses should be handled similarly to vue-form.

Changing the form object might break the validation state, not sure about it.

The lib is yet to mature, but I like the idea.

Conclusion

Start without a library, but bring vue-form in case of any non-trivial work on the horizon.

Sources

https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constraints https://developer.mozilla.org/en-US/docs/Web/API/Constraint_validation https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation https://css-tricks.com/form-validation-ux-html-css/ https://css-tricks.com/almanac/selectors/p/placeholder-shown/ (doesn’t work in IE)