Conquering Forms in Vue


Web forms are the connective tissue of the internet. Without them, people cannot request their health records, apply for university, order pad thai, or book a plane ticket. Yet too often, the tools we use to build those forms are a grab bag of libraries and DIY solutions that can result in a subpar user experience, poor accessibility, and low maintainability.

In this talk, we will introduce FormKit — a form-building framework — and explore how this tool can empower Vue developers to write consistent, accessible, even delightful forms without spending a lifetime building them.

Talk table of contents:

  • - The problem with forms, why they’re hard
  • - Introduction to FormKit
  • - Input library
  • - Validation
  • - Accessibility
  • - Form architecture
  • - Generating forms from JSON
  • - Next steps and closing statements


Hi, vue.js Live. I'm talking to you today about conquering forms in vue. We're going to learn about how easy and wonderful it is to make forms in vue with a particular bent on FormKit. So I'm Justin Schrader, and you might know me from the internet, you might not. Some of the open source projects that I'm involved in are FormKit, which is what we're talking about today, Arrow.js, which is a small two kilobyte lightweight alternative to things like react and vue. Another one is AutoAnimate, which is really neat, works great with vue and react and really any other ones. And it lets you automatically animate DOM elements coming in and out of the DOM or moving around. And then vue.Formulate, which is the spiritual ancestor of FormKit. It was a vue 2 library all about building forms in vue. And finally, I'm a partner in an agency called Braid here in Charlottesville, Virginia. So that's me. This is also me in 2019 staring at a massive client project that we had built with hundreds, maybe even thousands of inputs and forms. It was incredibly tedious to build, very hard to maintain. And at the time we were thinking, there's just got to be a better way. So let's talk a little bit about that. Because the prevailing wisdom on the internet right now, especially in Dev Twitter, is forms are not hard, forms are easy. In fact, Ryan Florence, one of the creators of remix, recently had this tweet. And I think it applies, you know, he's talking about remix and react, but I think it applies to really any of the big libraries out there. Let me read this. I don't think there should even be form abstractions. forms are fundamentally markup, user interactions, event handlers, state management, requests between views, Vell to react. What will a form library add here? Toss in some validation and be done with it. Interesting sentiment. And you know what? Fundamentally, Ryan's right. forms are easy. All you really need is a form tag and a couple of input tags, and voila, you've got yourself a completely valid form. Here's one. It's a username and password. This could be a login or a registration. And as you can see, it's incredibly simple. Now, to be fair, we probably should put a button in there for submitting the form. In this case, we put a register button in there, so this must be a registration form. And realistically, for accessibility reasons, we're going to need some labels on those. Plus, people won't even know what to fill out if we don't put them on there. So let's go ahead and do that. We'll just add an ID to our inputs because we need to semantically link them with the for attribute. So we'll just pop that on there. Great. Look at our form. Renders. People would know how to fill this out. We are good to go. Although, to be fair, it does look a little bit like a login form, so we should probably add some help text on those inputs. And to do that, all we got to do is add an aria described by on our actual input. And then wherever we put our help text, we can put an ID, something like, you know, username help. And now we're good. Now we're off to the races. Here we go. Now our form is starting to look better. We've got help text. You know, could probably use some styles, but it's looking good. forms are easy. To be fair, this doesn't actually do anything yet. So let's go ahead and put a script set up in the head here, and we'll just put an at submit. This is view. Super easy. Submit handler. And now we're ready to actually accept this data and do something with it. And there's a couple ways we can get the data, right? One way would be if we added an ID on the form. In this case, ID register. And then all we need to do is pass that element to form data, and it's going to do some collection for us and get some data out of our form. Now that does work, although in view we like to sort of have a little bit more control over what's going on with the inputs. So realistically, we're probably going to use a reactive object with maybe our username and password as properties inside of it. And then we'll just vmodel. We'll vmodel form data dot username. We'll vmodel the password. And then we'll pass all of that data to whatever function is going to actually submit it to the api. So this works. I don't see any problem with it. Well, there's one little thing. To be fair, when we call submit to api, we don't know what's going to happen. There could be errors. So we probably need to handle errors coming from our back end. But you know what? No big deal. We're engineers. We're up for the task. We're going to get it done. So let's take a look here. All we really need to do is have some way to store our errors reactively, something like a ref. Now we've got our errors here, and then we'll put a try catcher in our submit to api. And we'll just assign the result of those errors to errors value. And then somewhere down inside of our form, maybe at the bottom close to where the person clicked the submit, we will have something like a vf errors. And we'll just render all of the errors right there at the bottom. No problem. What's this here? Nielsen Norman Group. Very respected. Let's read this. Keeping error messages next to the fields in error minimizes the memory workload. Users can see the error messages while fixing the error instead of having to remember it. Got it. So they don't want all the errors at the bottom of the form. They would like them placed where the actual inputs are. Cool, cool, cool. No problem. We can figure that out. Now we probably, at this point, to be fair, this form is starting to look a little bit ugly. We should probably wrap some of these, extract them into components. Like this area right here, for example, would be much better if it was written as an actual separate component. So here we go. Here's a new component for our actual inputs. We've got defined props. We're going to accept the label, name help, and now we're going to accept the errors so that way we can place them with the input. And then down below here, the only thing that I see, you know, most of this is coming in. We're now rendering the errors. But we do still have this v model. So that is a problem because, of course, form data is no longer in our scope here. So what do we need to do? Well, we need to add model value and then something like define emits for updating the model value. And then instead of doing a v model, now we need to listen to the input event on the input and we need to update an event called update model value with our new value. Good to go. Hmm, what's this one? I've got another one. Ideally, all validation should be inline. That is, as soon as the user has finished filling in a field, an indicator should appear nearby if the field contains an error. Hmm, validation, inline validation. Okay. Before I rush in and keep trying to make these iterations, let's figure out a quick list of what needs to be done here. So we need to add inline validation rules somehow on the inputs themselves, I guess. Then we need to figure out how to get the validation from down inside of those inputs and aggregate them so that way we know whether or not our form is valid before it gets submitted. Frustrating, but we can do that. And then we need to probably have some sort of loading feedback, I would imagine. I mean, what happens if we have to nest these a little bit deeper? Then we'd have to pass all of those props down through other components. Okay, we need to add styles. This form is only two inputs, literally two inputs. And the complexity is getting out of hand and it still looks like garbage. So it brings up the question, forms are easy? Let's reexamine this hypothesis. Okay, so here's Ryan Florence's tweet again. I don't think there should even be form abstractions. forms are fundamentally markup, user interactions, event handlers, state management requests. Between vue, svelte, and react, what will a form library add here? Toss in some validation functions and be done with it. Interesting. I think what Ryan is trying to say is that if you think of the two domains that need to be handled by a form in a framework, that the overlap between them doesn't look like this. I think Ryan is saying that people out there building form libraries have an overlap in their minds that looks something like this. Like a little bit of what the framework does is a little bit of what the form does. And what Ryan is saying is actually the Venn diagram is completely overlapped. Everything that forms do, frameworks do. And with that, I agree. The only difference is I believe that the domain of the form looks a little bit more like this. It is a dramatic amount of additional work on top of what the framework is doing. So it brings up the question, form framework? forms are easy, huh? Well, here's what I'm saying. If forms are just markup, user interaction, event handlers, state management requests, and they still suck to build, then they deserve their own framework. This is what we're spending all of our time doing as engineers, at least a significant portion of it. Maybe we can make it better. So that is why we created FormKit. And that's why I'm excited about it and passionate about it. FormKit is specifically a form building framework. It's like an entire framework specifically for this one use case. It is not a UI library. So FormKit is much more about the architecture of your form, making sure that all the validation and everything else is all put together, rather than just being like a component library. Let me show you what I mean. So when I sat down and was thinking about all of the features that are in FormKit, and I wanted to make sure that we covered them all, I just decided to start penning them out, kind of make a dirty list of them all. And I didn't even really finish before I realized this is just too ridiculous to fit into a 20-minute talk. So what we're going to do today is just talk about four of these, okay? And I'll leave it up to you to go look at the documentation for FormKit, or if you find another form framework out there, go look at them. Here's the four things I want to talk about. First of all, the choice of having a single component for FormKit. What that is, what does it mean, and why did we do it? Second of all, input values are collected automatically. Third, apply validation with a single prop. And fourth, form generation. All right, let's start with a single component. What do we mean by that? Well, fundamentally, a FormKit component is roughly equivalent to the input tag in html. So if you take a look here, we got FormKit type checkbox, and that is roughly equivalent to input type checkbox. Makes sense. Now if you want to have a select in html, this is a different tag. But in FormKit, it's still the FormKit component. And this is applicable to literally every single input that FormKit ships with. It kind of smooths out the api of html. So every single native html input is part of the core FormKit open source package. We're talking about buttons and the native color wheels, the native date picker, text areas text. All of those things that you're used to using in html are part of it, just with some superpowers that FormKit adds. And now recently, we've started, in order to try to support the project, the open source project, we've also got some of these like paid pro inputs that don't come native. So we're thinking like things like autocomplete and rating inputs and repeaters and so on and so forth. So what does it look like? Here is a FormKit text input. We just say type text. And now we're getting to do some things like we were doing in our earlier example in native html and native view. We've got a label here, favorite color, help, some help text with it. And what does it generate? It generates markup that looks remarkably similar to what we had before. It's got some appropriate wrapper classes around it, has a label. As you can see here, it automatically generated an ID for my input and then semantically linked it to my label. And it did the same thing for the aria-describedby like we were talking about before, and it applied that to the help text. Now you can just swap out the type and you automatically get different rendered output from the same component. So for example, in this case, I swapped it out for checkbox, and now I'm getting a checkbox that's actually rendering in that location. And here you can see there's a FormKit decorator, because if you've ever had to do checkboxes, you know that styling them is a nightmare because you can't really style the actual checkbox itself. So we are developers at FormKit, and we wanted to make that easier. So by default, it ships with this decorator that allows you to make nice looking checkboxes like this. Great. Now what if I want to do multiple checkboxes? Well, in that case, you can just say options and pass in the options that you want. And the rendered output that you're going to get is dramatically different, because in order to do multi-checkboxes, it's actually quite complicated in native html. Technically, you should have something wrapping them like a field set, so that way accessibility knows that these inputs are all part of the same group. And so here is the rendered output for multiple options. And it looks something like this, at least with our default built-in theme. And it works great. Now what are the advantages of the single component? First of all, the learning curve is dead simple. You basically already know how to use FormKit if you've ever written an html form before, because we're copying all of those concepts over. There's a consistent api. Everybody on your team who's coming into a form can look at it and generally understand what's happening there, because it works the same way across inputs. So most of the props are the same. The component looks the same. The way it passes data back and forth is the same. And it kind of smooths out those inconsistencies in html. You know, things like an input having the value applied via value versus a text area having the value be applied as the child of the input. In FormKit, this is just FormKit type text area, FormKit type text. Okay, the next thing to talk about here is input values are collected automatically. So here's a traditional view form. I've got a submit handler. I'm remodeling the address, address line two, the city, the state, and the zip code. None of this is actually necessary in FormKit, because it does it all on its own. So I'm going to attempt a demo here. Pull up the playground. And here you can see this is an equivalent form in FormKit. I've just got a FormKit type form, and then I've got all of my inputs down below here. And to show you what I'm talking about, I'm just going to destructure out of the default slot the value of the form. You can do this with any FormKit input, but I'm going to do it with the form. And I'll put a pre-tag right here and render the value. And as you can see, all of the values of the form are automatically being collected, and they just will get handed to your submit handler automatically. That's pretty nifty. But where this really gets exciting is if you want to create any sort of structure. So in my case, I'm going to create something like address.view, because I want to reuse these pieces of the address. And I'm going to take these, I'm going to copy them, and I'm just going to paste them over here. I'm not going to, whoop, not in the setup, in the template here. I'm not going to use anything else other than that, and in this component, I'll just do a script setup. And I will import address from address.view. And then we'll just render this. And what's great is my values are automatically still flowing up, and I can do this nesting at any depth. And if I want to, I can even group them. So for example, I could say form kit type group, and I could call this name address. And I'm just going to wrap my address component in it. I could put this group in the address component. Now you can see that I've automatically in my form gotten the name address, and all of the values of my address are going to flow directly underneath that namespace. So this is a great way to just really quickly structure your data. So some advantages of having this be collected automatically. Well, for one, it's less boilerplate, and that doesn't just mean less work. That also means less opportunity for errors. Form kit's automatically handling this for you, so you can just build your form and then know that the data is going to be where it's supposed to be when it's supposed to be there. It encourages reusability. Like you saw, if I wanted to take a standard V model and apply it to an address component like that, that's fine. It's not too hard. But as soon as I start to nest that down under repeated components, it becomes a real pain because I have to pass those values like the model value and the emits through all of those components and pretty soon I'm going to have multiple channels running up and down through there, and it gets pretty hard to debug. It also avoids two-way binding. You'll notice I didn't need to use V model anywhere. Now you can, to be clear. You can use V model with form kit, but I didn't need to. And if you've ever had to deal with a really large form where you're V modeling things more complex than a single scalar value, you know that it can get really hard to do that. There can be problems with infinite loops and recursion and things like that that happen because objects are fundamentally new even when they have their same values. Okay. Apply validation with a single prop. So this is pretty self-explanatory, but here I've got some checkbox of some ice cream flavors and it would render looking something like this. And if I wanted to add validation to that, all I need to do is add validation equals and then type the validation rules. Form kit ships with dozens of validation rules out of the box and all of them have internationalization support for the messages they produce as well. So let's take a real quick look here. And sure enough, here is my input and I can check these boxes. Nothing really happens. That's great. But then I want to add validation. Say validation is required. And now when I check a box and uncheck it, I get flavor is required. Okay. And if I want to add another rule, I'd say the minimum is two and I'm going to add three rules. Minimum is three. And now when I start interacting with my form, you can see, okay, if I only have one, it says cannot have fewer than two flavors. I check two, all is good. Check three, all is good. Go to fourth and you cannot have more than three flavors. And I can add that those types of validation rules to any input in form kit. They all support it out of the box right away. Even if you create your own inputs, they automatically get all the validation support. And I should mention that the validation at the top of the form automatically knows about the validation states of all of the subchildren. So it's able to stop your form submission until your validation is complete, if that's the behavior we would like. Some advantages. Again, the learning curve is easy. I mean, it's so simple. I'm sure you basically learned all you need to know about validation by seeing that. The api is consistent. Your team doesn't need to go look through validation models and other complexities. They can just look at the input and see what it is. And that's the third benefit is it's co-located. Your validation rules are on the inputs that they're being used on. Okay. And then the last feature that I want to talk about, which is a whole subject in and of itself, but it's form generation. So in order to generate forms in form kit, we have something called the form kit schema. Let's take a look. Form kit schema is a JSON serializable data format for storing DOM structure and component implementations, including form kit forms. It's kind of wordy, but let's see what we mean by that. Here is a simple html div, just a div with hello world. Now we can represent this in schema using something like this. Now this is not JSON, but you can see obviously this is easily serializable to JSON. I just L, dollar sign EL div, and then children, hello world. If we were to pass that object to form kit schema, it would actually render the same div. But why is that important? Well, that's important because we can take that schema and we can put it anywhere. We can put it in the cloud, we can store it in a database, we can send it over the wire, we can construct it. We can do all kinds of things with it and then use that to automatically generate markup and forms. So in the form kit schema, there are three special elements that can get rendered. One is a DOM element. You just do that with dollar sign EL. Second is dollar sign CMP. That renders a component and you can pass props to it just like you would pass props to a component in a view template. And then third is a shorthand. And this shorthand just allows you to more quickly render form kit inputs. You could use the dollar sign CMP as well if you wanted to since form kit is just a component. But what really makes it powerful is expressions. Form kit schema supports actual expressions. What do we mean? Well, you can write Boolean logic basically in your JSON. Equals, ands, ors, does not equal, things like that. You can support entire arithmetic. So we're talking addition, multiplication, subtraction, exponential, math. All of that can be written directly into your JSON. It supports function calls. Conditional rendering, you know, like render this or this based on some condition. Dot rendering, and let's take a little look at some of this. So here we have a form kit schema component and you can see that we are passing schema directly into it, which is just this little object right here. Form kit form has a submit label. And then as a child inside of the form, we have a number. Now a valid piece of form kit schema is actually just a string. So as long as I start my string with a dollar sign, it tells this little compiler to boot up and interpret this into render functions. So if I just ask for, for example, the value of my form and the quantity, this will automatically render the value of my input. You can see it's all reactive because it renders it to view render functions. And that's not all. I can actually write arithmetic here, right? So I can say, you know, by some price, you know, the quantity times a price, and here you go. Now we're actually getting reactive values rendered on the page. The other thing though that we're passing to the form kit schema is this data object. And it's just a view reactive object and it has only one thing in it. It's got this dollars. And if you look, dollars is just the INTL currency function. So I'm getting a USD format and I can actually call that function right here. Dollars and I'm going to just pass these values directly into it. And sure enough, now we're getting formatted numbers. And if I want, I can pass other values too. So for example, let's say the price is, you know, 233. I could just reference the price now, now that I'm passing it in dynamically. So just using dollar sign price and that matches what's in my dynamic object. And now I've got this rendering automatically. That's a really, really brief look at form kit schema. I would love to spend a lot more time talking about it, but we'll save it for another day. So again, those are only four of the features that I wanted to cover. There's a lot more there and it really does help build your forms and make it much more easy, which brings up the question, are forms easy? It's a really valid question. And I would say they're really only easy if you're using something like a form framework. That's it for me. Thank you so much. Really appreciate getting to talk here. We've got the documentation at You can follow me on Twitter at JPSchrader. And the Twitter handle for FormKit is useformkit. And just one little plug here. ViewSchool actually did a whole series of courses on FormKit, which you can go get at So thank you very much.
24 min
15 May, 2023

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic