Vue Formulate has been in the wild for 2 months now, and with the latest release (v2.3) the project has enough momentum to warrant a post from its creator (me, Justin Schroeder) on why it exists, what it does, and where it is going.
The problem with forms
When you’re learning to program, one of the most exciting early progressions is when you make your "Hello World" app interactive by prompting a user for their name. Take those mad I.O. skills to the web and it gets even easier! Just plop an <input>
tag into your markup and you’re off the races right? Well...not so fast.
Over the past two months, I've gotten a lot of questions about Vue Formulate. Unsurprisingly one of the most frequent ones is, "What’s wrong with HTML?".
There’s nothing wrong with HTML, of course, just like there was nothing wrong with JavaScript before Vue and React (I know, I know, Vanilla purists’ blood is boiling out there). HTML, React, Vue... it doesn’t matter — the reality is: creating high-quality forms requires a lot of consideration. Labels, help text, validation, inline file uploads, and accessibility are just a few of the items a developer will need to address. This almost inevitably amounts to gobs of copy/paste and boilerplate markup littered throughout your codebase.
There are other issues too. HTML validation, for example, is pretty limited. What if you want to asynchronously check if a username is already taken? What if you want to have well-styled validation errors? What if you want to offer the ability for someone to add more attendees on their ticket purchase? None of these are available to native HTML/React/Vue without considerable effort. Furthermore, maintaining a high level of quality while working on such disparate features becomes secondary to just making the form work. This is fertile ground for a library to help increase developer happiness while pushing quality and accessibility.
Why is Vue Formulate different?
Vue Formulate is far from the first library to address these concerns. Our long-time friends in the community have been fighting these battles for ages: vue-forms, VeeValidate, Vuelidate, and even some UI frameworks like Vuetify aim to help developers author better forms. These are great packages and I wouldn’t discourage you from using them if they’re appropriate for your project. However, Vue Formulate approaches the same problems with two specific objectives:
- Improve the developer experience of form authoring.
- Increase the quality of forms for end-users.
In order to provide a great developer experience, Vue Formulate needs to focus on being a comprehensive form authoring solution. It cannot just be a validator and doesn’t aspire to become a full UI library. Instead, these guiding principles have resulted in a highly consistent component-first API focused solely on first-class form authoring. To that end, every single input in Vue Formulate is authored with the same component <FormulateInput>
, smoothing out the inconsistencies in HTML’s default elements such as <input>
, <textarea>
, <select>
and others. In Vue Formulate you simply tell the <FormulateInput>
what type of input it should be — a text input (<FormulateInput type="text">
) and a select input (<FormulateInput type="select">
) can even be dynamically exchanged by changing the type
prop on the fly.
Why is this better you ask? It’s better because it’s easy to remember, fast to compose, and reduces mistakes. We absolutely shouldn’t discount those very real quality of life improvements... but of course that’s not all.
By ensuring all inputs conform to a single component interface we allow for more powerful enhancements like automatic labels, declarative validation, form generation, automatic accessibility attributes, and support for complex custom inputs. This allows a FormulateInput
component to maintain an easy-to-use API while being endowed with super powers. Consider how similarly these two inputs are authored using Vue Formulate and yet how different their actual HTML implementation is:
<FormulateInput
type="email"
name="email"
label="Enter your email address"
help="We’ll send you an email when your ice cream is ready"
validation="required|email"
/>
<FormulateInput
type="checkbox"
name="flavor"
label="Pick your 2 favorite flavors"
validation="min:2,length"
:options="{
vanilla: 'Vanilla',
chocolate: 'Chocolate',
strawberry: ’Strawberry',
apple: 'Apple'
}"
/>
Now, notice some of the things we didn’t have to deal with in that example:
-
<label>
elements inputs were automatically generated and linked to the<input>
element via auto-generated ids (specify if you want). - Help text was generated in the proper location and the input was linked to it with
aria-describedby
. - We added real time input validation without having to explicitly output errors.
- Multiple checkboxes were rendered with their values linked together.
- The labels for the checkboxes automatically adjusted their position.
By consolidating inputs into a single FormulateInput
component, we drastically improve the quality of life for developers, and simultaneously create a powerful hook for adding new features and functionality to those inputs. As a bonus, when it comes time to upgrade to Vue 3’s Composition API, Vue Formulate’s component-first API means developers won’t need to refactor anything in their template code.
Neato, but where’s my form?
I’ve explained Vue Formulate’s purpose and its unique approach to inputs, but how about the form itself? Let’s consider the purpose of the native <form>
element: to transmit input from a user to a server by aggregating the values of its input elements. What does that look like in Vue Formulate? Pretty much exactly what you would expect:
<template>
<FormulateForm
@submit="login"
>
<FormulateInput
type="email"
name="email"
label="Email address"
validation="required|email"
/>
<FormulateInput
type="password"
name="password"
label="Password"
validation="required"
/>
<FormulateInput label="Login" type="submit" />
</FormulateForm>
</template>
<script>
export default {
methods: {
login (data) {
/* do something with data when it passes validation:
* { email: 'zzz@zzz.com', password: 'xyz' }
*/
alert('Logged in')
}
}
}
</script>
Great, so data aggregation works just like a normal form, but there’s not anything “reactive” here yet. Ahh, let’s slap a v-model
onto that form — and — presto! We have a fully reactive object with all the data in our form.
<template>
<FormulateForm
@submit="login"
v-model="values"
>
<FormulateInput
type="email"
name="email"
label="Email address"
validation="required|email"
/>
<FormulateInput
type="password"
name="password"
label="Password"
validation="required"
/>
<FormulateInput label="Login" type="submit" />
<pre>{{ values }}</pre>
</FormulateForm>
</template>
<script>
export default {
data () {
return {
values: {}
}
},
methods: {
login (data) {
/* do something with data:
* { email: 'zzz@zzz.com', password: 'xyz' }
*/
alert('Logged in')
}
}
}
</script>
And yes, v-model
means its two-way data binding. You can write values into any input in your form by changing properties on a single object. Aim small, miss small — so let’s shoot for making “it just works” the default developer experience.
Slots, custom inputs, plugins — oh my!
This article is just an introduction — not a substitute for the full documentation — but it wouldn’t be fair to leave out some of my favorite extensibility features. Form authoring tools need to be flexible — there’s an edge case for everything right? Vue Formulate’s highly opinionated component-first API may seem at odds with flexibility, but in reality that consistent API is the core behind a highly flexible architecture.
Slots are a great example of how consistency pays the bills. Central to Vue Formulate’s inputs is a comprehensive context
object that dictates virtually everything about an input. The model, validation errors, label value, help text, and lots (lots!) more are members of this object. Because every input has a consistent API, every input has a consistent context object.
While the flexibility to use scoped slots is great — they can hurt the consistency and readability of our form’s code. To address this, Vue Formulate also includes the ability to override the default value of every slot. We call these “Slot Components”, and they’re fundamental to maintaining a clean consistent authoring API. Want to add that example tooltip to every label? No problem. You can replace the default value in the label slot on every input in your project without having to use scoped slots or wrap your components in your templates at all.
If you decide you’re better off creating your own custom input type, you can do that too! Custom inputs keep form authoring buttery-smooth, just pick your own input type
and register it with Vue Formulate. Your custom input will get validation, labels, help text, model binding, and more out of the box. Even better, once you’ve created a custom input you can easily turn it into a plugin to share with your team members or the larger community.
Where you go is where I wanna be...
In the excellent Honeypot Vue documentary, Thorsten Lünborg summed up what I consider to be the number one reasons for Vue’s spectacular success:
The focus in Vue.js from the get-go was always that the framework is more than just the code. It’s not like, “this is the library, this is the documentation, of how it works, and now you solve the rest.”
In essence, the Vue core team was willing to go where developers were feeling pain points the most. As a result they have created a framework that isn’t just elegant — it’s delightful for real-world developers to use. Vue Formulate maintains this spirit; to meet developer’s pain points with delightful form authoring solutions. We believe we’ve now paved the road for 90% of users — but if your road is less traveled and you find yourself at an edge case — please shout it out. We're listening.
If you’re intrigued, checkout vueformulate.com. You can follow me, Justin Schroeder, on twitter — as well as my co-maintainer Andrew Boyd.