Conf42 JavaScript 2021 - Online

Building Super Powered HTML Forms with JavaScript

Video size:

Abstract

I spent a year writing a series of articles that explored best practices for building HTML forms in terms of semantics, accessibility, styling, user experience, and security. I want to show you how to leverage those lessons with JavaScript to give your forms super powers without bloating your client-side bundles.

Summary

  • Today I'm going to be discussing adding JavaScript to HTML forms to give them superpowers. These improvements do not negatively impact functionality, native functionality, accessibility, semantics, performance or security. The slides will be available at this URL.
  • Almost every input would actually benefit from having a form HTML tag wrapped around it. Why not take the native HTML validation constraints and tap into those using JavaScript to enhance upon it. There are occasions when you might still consider a third party library.
  • Now, moving on from using JavaScript to achieve feature parity in terms of validation and form submissions for better user experience. We can add additional features such as preventing data loss. Those are things that we could do to take native HTML and use JavaScript to build on top of it.
  • The benefit of using component frameworks such as react or view means that it simplifies. Or we can simplify the form creation process. Next, we get repeatable quality. By having component frameworks, we can implement that same component in multiple places.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
You. Today I'm going to be discussing adding JavaScript to HTML forms to give them superpowers. Before we get too far, I want to explain what I consider to be superpowers. Those are going to be user experience improvements that do not negatively impact functionality, native functionality, accessibility, semantics, performance or security. This is also commonly referred to as progressive enhancement. So, getting to JavaScript the first rule of JavaScript is knowing when to use it, and therefore knowing when not to use it. JavaScript itself has some inherent costs when we decide whether or not to add it to a page. So HTML and CSS are generally going to be faster, except for special occasions. And at the point that we decide to start adding JavaScript, we may start incurring things like extra HTTP requests, more data to download. We might have JavaScript that blocks some rendering performance or rendering time, JavaScript that has runtime performance issues. We may have JavaScript that has unexpected errors or exceptions that prevent the rest of the script from running, and it's possible that JavaScript may be disabled or blocked by a user. Now, HTML, on the other hand, natively gives us a lot we have some concept of state management in the inputs values. We have clickable labels without needing to add any sort of JavaScript to click something and make a label focused we have accessibility built in for keyboard navigation, focus states, screen reader support. We have consistent experience across browsers and devices. If a user uses a radio or checkbox in one browser and should behave the same in another. Those is also great for users that prefer using the keyboard to fill out forms. And we have benefits for things like implicit submission, which we can discuss later. Lastly, that's going to be a recurring theme is validation. Now, validation built into HTML costs us zero, and it has quite a good number of resources available to us. Another benefit of the built in validation attributes is they could hint to assistive technology, things like whether an input is required now between or when we're building HTML forms, we're really looking at two different things, the inputs or the controls, and those form itself. We'll start by looking at the individual inputs built into the browser. We have 24 different options if you account for all of the input types, text areas, selects, et cetera. So when I browse the Internet, I still wonder why I see things like this. A div that's designed to look like a checkbox and has some Javascript event listener, it says I'm a checkbox. One of the reasons we still see this probably has to do with styling. In 2019, Greg Whitworth did a survey he asked. He got 1400 respondents and of them the most common reason people create their own native controls or recreate native controls is for styling with the number one control that they recreate being the select. You can see more at this URL. The slides will be available. In that discussion he points out that the amount of work it takes to implement an accessible alternative with complete feature parity is massive. And here's an example of that. If we want to take that previous checkbox and make it a little bit more on par with what we get built in, we can see that we have a class to add, some styling to make it look like a checkbox, some ARIa attributes for assistive technology, we have some tab index for keyboard navigation, and we have two event listeners, one for clicks with a mouse, one for key downs. Compare that to the native solution which is an input with a label. Looks like more work to me, and this isn't even considering the Javascript that goes into those event bundles. And it's not the most complex form component to recreate like radios or selects. So the good news is, when it comes to styling, it's better than it used to be. CSS gives us a lot of pseudo classes to work with based on the state of the input. We also have features like appearance none and pseudo elements that let us create custom checkboxes and radios. For example, we can use tricks like a visually hidden input and then target a sibling selector to make something look cool based on what that input is doing. And there's more information at my blog. Again, the links will be available in the slides if you need. Here are a couple examples of some totally custom looking UI form components. All of these can be accomplished with just HTML and CSS. No need for Javascript for things like this. The other good news is that the future is bright. There's people discussing potentially bringing things like new pseudo selectors and parts so that we can style things like the select drop down which is a complex component more specifically, and potentially things like name slots. So if we want, we could take those selects and be able to customize the individual aspects of it by providing our own markup. This is just an example, which I guess you can do with emojis, but you get the idea. There's more details about those things that may be coming down the line if you go to openui.org. A lot of discussion going on there as well. There's some editorial proposals, forms, select checkbox and file as of today. There's other editorial proposals, but these are the ones that relate to forms. Now you may be wondering yourself, I thought this was a Javascript conference. What's the deal? And the good news is that Javascript is actually really good for taking what we have natively and improving upon it without breaking something. We can take that native form validation that we were discussing, and we can customize the message or manually trigger it. We can also do things that HTML alone cannot do, such as toggling aria well, for accessibility toggling aria invalid or aria disabled attributes, we can provide improved user experiences by doing things like an input that's a password input that you can toggle, whether the visibility of the password or a text area that automatically expands and shrinks based on how much content is in there, or a phone input that does the masking and formatting to show that it's a phone number. Of course you want to do all this in a way that doesn't detract from the user experience. And lastly, getting back to the conversation of validation, we may want to provide our own custom UI for validation messages. So looking into that, the browser has built into it without anything else that we need to reach out for a third party library. The validity state web API, which is one of my favorites. If you have an input dom node, it's right on the validity property and it gives you an object with all of these different properties that are true or false based on whether the corresponding HTML attributes are valid or not. We can use this API to do things like toggle the Aria Invalid state, or we can add maybe like a Div or a list of error messages and associate that to the input with Aria described by and give it a live region so that assistive technology users are updated. So with the inputs kind of covered now we can transition over to forms. Now forms are going to be a little bit different because forms don't have a built in UI that you have to deal with. And so some people omit them all together and will do something like an input that you just press the enter key on and it does something. Now I would argue that almost every input would actually benefit from having a form HTML tag wrapped around it, because it can give us some additional features without with actually doing less work, things like that native validation that we just talked about, that only works if we have an input inside of a form element, the implicit return, which is when we compared to a JavaScript event listener that looks for the key down event and checks if it's the enter key and then does a fetch request. We can get all that built in by just putting a text input and a form. When you hit the enter key, it's going to submit the form. We can simplify the JavaScript API request by using a form tag. So if we want to send Ajax requests, we can actually make it easier on ourselves by using an HTML form. We also get resiliency, again, going back to that idea that Javascript may experience an error and you probably want to fall back to HTML to submit forms if your Ajax request is not available, right? So assuming that we're using forms, we can do the same thing that we did with inputs where we can take JavaScript and enhance upon the native experience. Because some people don't want to submit all of their forms with the native experience, which is a page refresh. So some things that we can do that are in addition to the native experience might be keyboard shortcuts like control enter to submit the form. When you're focused on a text area, which is not possible with just HTML, we can have repeater input fields. So think of a collection of a few inputs that you can make multiple copies of. Thinking if you have like an ecommerce site and you want to have a product name, price and picture, and you want to add many of them at the same time, drag and drop is a common thing that we can do with JavaScript that we can't do with HTML, and maybe that lives within a form somewhere, I don't know. Then we can have again the custom validation user experience if you don't want to rely on the native validation, because looking at the native validation, this is what it looks like, we might try and submit this form. It's a required field, and we get a pop up that says please fill out this field. And native validation is actually quite useful in terms of the features that it provides. One thing is when we submit an invalid form, it focuses on the first invalid input, which brings our focus there. As an added benefit of the focus going there, we will actually also scroll there, the browser will scroll to that input. So if the form is long enough to take up greater than the screen of the browser, and you hit that submit button, you want to scroll to the input that is invalid. And naturally, or obviously it explains to the user what the error is with the form or with the input. Now there's just one problem with the native UI, and that is that there's not really a good way to customize it. So if we care about branding, we may want to be able to do that. And I think about this as why not supporting both? Why not take the native HTML validation constraints and tap into those using JavaScript to enhance upon it. This way, if JavaScript does get disabled, our form validation logic will still work because it falls back to the native HTML one. There's also less to learn because compared to a third party library for validation, we don't have to learn what their API is, we just learn what's native to the browser and there's no need know. If you learn one library and then decide to move on to the next one, they have a completely different API right, compared to libraries. Again, we probably will have less to download as well, which would improve performance. Also make maintenance easier because we don't have some third party dependency that we have to keep up to date, and it could potentially improve security by not having to deal with NPM vulnerabilities that intentional or unintentional. Now the last point is that validation logic really doesn't belong entirely on the front end. You don't want the business logic of your application relying on clientside validation because it's possible to make form submissions outside of the front end anyway, and so you're going to be doing validation on the back end. So there's really not a need to have a whole robust solution on the front end, just something that improves those user experience but doesn't necessarily need to be super robust. There are occasions when you might still consider a third party library. I'm not saying you shouldn't use them, but I like to start with the native things and enhance on it a little bit before I need to reach for a third party. Now, looking at rolling our own sort of custom validation experience, the first thing we want to do is prevent the default native validation UI from occurring. The way we can do that is by adding those no validate property onto our form so that it tells the Browser hey, don't bother validating this now. We want to do that with JavaScript because that means JavaScript is enabled. Next we want to listen to a submit event and we want to scroll to that first input, the first invalid input. So we can do that by on submit checking if the form is valid or invalid. We can do that with the check validity method on the form DoM node it returns a boolean and if the form is not valid then we can do a query selector for the first invalid input and focus on it. That's also going to maintain parity with the native HTML experience where it focuses on it, and because it's focused it will scroll to it as well. If it's invalid, we can return early if it's not invalid, we can go ahead and do the submission or the logic to submit our form, maybe with a fetch request. We also want to prevent the default behavior, which would be the browser refreshing and sending that request. Next we can look at what does that API request look like? Or sending that fetch request. Now I want to look at this in a way that maintains feature parity, and we can actually use this on whatever form we want. We don't need to have a specific fetch event for the login form versus the register forms versus whatever form. We can use the same thing on all of them. So this function might look like this. We'll start by listening to a form submission event. We'll grab the form Dom node out of the event target. We'll start building out our fetch parameters based on the form attributes. So we'll get the URL from the action. We'll get those form method from the form method. Then we'll capture the data from our inputs in that forms using the form data web API. We don't need to do anything more to capture the information as long as our inputs are semantically written and have a name and everything. We can also capture data in the form of a URL search params, which will make more sense in a moment. We want to do a check whether this form's encoding type is multi part form data. If that doesn't make sense to you, it basically kind of comes down to whether we're sending a file or not, but it's not the default encoding type, so we'll get to that in a moment as well. Next, the forms can have a get request, can send a get request or a post request. So we want to check whether it's a get request. If it's a get request, we want to send our data by means of URL search string parameters. So we'll take the URL that we had and we'll append onto it the query string parameters for that payload. I think I actually missed the little question mark there, but that's all right. If it's not a get request, we know it's supposed to be a post request, which means we'll put our payload in those body of the request and we can check whether it's a multipart form data. If it is, we can send it with the forms data object. If it's not, we can send it with the URL search parameters object at the very end. We want to prevent the default behavior because we want to make sure that everything before this line has completed successfully before. We prevent the form from submitting using the native HTML submission, and at this point we know that everything's all good, so we can send that fetch request with the URL and the methods that we want or the options that we defined. There are a couple of caveats to sending form submissions this way. One is if we're pulling the methods from the HTML form, we really only have the get and the post method available, which if you don't control your API endpoints, that might be an issue. This also probably means you want to detect whether the form or whether the data is being sent to your backend through JavaScript or through a native form submission. The reason being, if you send it through JavaScript, it might be safe to expect a JSON payload as the response. However, if you send it with HTML because it's going to reload the page, you probably don't want to reload the page with a JSON response. You probably want to, I don't know, maybe redirect the user back to the page that they came from so that essentially the page refreshes and they're none the wiser. This also doesn't work if you're dealing with complex data types. So if you're dealing with nested objects or an array of objects or things like that, you just can't do that with HTML forms. You also can't send graphQl requests because that needs a special sort of formatting. Now, moving on from using JavaScript to achieve feature parity in terms of validation and form submissions for better user experience, we can add additional features such as preventing data loss. So if a user is filling out a long form, it might be really annoying for them to accidentally navigate away or refresh. And we can help them by having a little pop up that checks hey, are you sure you want to leave the page right now? The way we can do that is by tapping into the before unload event on the page. So we can do that with the window add event listener to before unload and then do a check whether the user has made any changes or not. If they have not messed with the form at all, it's probably okay for them to refresh the page. So we can just return early and we don't even need to show them this little message. If they have made changes to the form, we can trigger those message. We can't customize it, but we can trigger it by doing the preventing the default behavior on that before unload event. We also need another line for Chrome for whatever reason, but this is a nice little user experience improvement to make your user's life better, because then they don't lose the data that they've spent so much time working on. There's more information on how to do this on my blog as well, if you want to get the slide presentations and check that out. In addition to preventing data loss, we can do things like keeping backups of the data that they have. Now, of course you don't want to do this for very sensitive data, but let's say we're not dealing with sensitive data. That's okay to share. What we can do is check whether the user has made any change to any of the inputs on the page. And every time that they make a change, we can capture the data from the form. So the inputs and their values as like key value pairs, we can put that into a JSON object and then stringify it and store that in local storage. And then later on if they leave and come back when the browser loads, or when that form lands on the page, we can check local storage, see if that data exists in local storage. If it is, and we have an object, we can loop through the properties and values of that object and assign those values to their corresponding inputs within the form. That logic is a little bit too long for me to put here, so just imagine it was really awesome looking code. Finally, when that form is submitted, we want to clear out local storage so that when they come back, they're not looking at data that they've already submitted. Just an example. So those are things that we could do to take native HTML and use JavaScript to build on top of it and give it sort of these super cool improvements without sacrificing accessibility or resiliency. Now I want to take a moment to look at component frameworks, because I think this is where the real superpowers get unlocked. The benefit of using component frameworks such as react or view means that it simplifies. Or we can simplify the form creation process. We'll look at that. I'll explain what that means in a minute. But essentially, as a developer consuming some of our components, it's not as much work to do. All of the markup and ids and labels and aria attributes and event listeners and all this stuff. It's just much simpler. Next, we get repeatable quality. So when we spend so much time working on forms, we want to make sure that they're well built and they work for everyone and they work across devices and browsers and all that. So by having component frameworks, we can actually implement that same component in multiple places and we get the same quality over and over. It also makes maintenance easier. As we've implemented that component over and over and over, we may discover that there is actually a bug in that component. And rather than having to go throughout a site and fix every instance that there ever was of an input or a form, we can make that fix in one place and have that fix permeate throughout our application so that every input suddenly is fixed or every form. We can also do things like enforcing best practices so react and view I can speak to, I have experience with, and they provide methods for you to require certain things when you implement things like an input. So saying that every input requires a label or a name or things like that you can enforce. There's also things that we know are required for every input, such as ids, but we don't have to be as strict with we can generate those and have kind of a fallback for developers. So let's look in a view example of what that might look like is I have a component here where I've defined here just the props, and with these props I can say that we have a label and we have a name prop that this component expects. And I can say that both of those are required as the developer creating the component. I have no idea how this input is going to be used, but as the developer consuming it, you're going to be required to give me those fields, because for a fully accessible and quality input I need them. I also need the id of an input in order to maintain those aria attributes. But I don't necessarily need you to give me an id. You can, you may if you want, and I'll take it. Otherwise I can fall back to generating a randomly created one. Now this input we may want to add validation logic to anytime that it experiences a blur event. And then that validation logic we want to show some errors for. So we can start with some reactive properties of tracking an array of errors. And then on that blur event we can tap into the validity state of that input and check whether it's invalid or not. For each of those properties that we saw before, we can loop over them, see what the property, check what the property is, check whether it's valid or invalid. If it's valid, we can move on to the next property. If it's invalid. In this example we're looking at the range underflow property which corresponds to the min attribute. So if the min attribute is invalid, we can push to our error object hey, this input must be greater than whatever the minimum attribute is, and we can push that error to our reactive error array and then present that in the UI. So looking at the UI for this component, it might look something like this. We might have the label that's associated to the input through that id. We might check whether this is a required input or not based on the attributes, and if so maybe put a little red asterisk next to the label. We'll have our input of course that has our validation event handler and the id and everything else an aria described by. And then we might have our UI for showing those error messages. And that can be associated with the input through the aria described by attribute. It's generated with the ID or based off of the ID, and it has a role of alert. There's a little bit more. This example is inspired from an input component in the view tensors library, which you can check out later. Now, besides the input, we can also create a component for our form. And our form component might have a submit event handler that checks the validity of the form, kind of like we saw before using the check validity method. And if the form is invalid we can automatically focus and scroll to the first invalid input. But then in addition to the same features that we've discussed and kind of adding those to a component, we have new features available which are custom event emitters, and this will make more sense in a moment, but we can basically create custom events for when the form is invalidly submitted and when the form is validly submitted. This component's markup is a lot simpler. It's just a form. It falls back to a post method for security reasons, which I don't have time to get into now. It implements a slot in react. This corresponds to the component children to make life easier for all of the developers. Maybe it includes a submit button, which is not customizable in this case, but you can imagine. And so putting all of this together, once we have this component logic, we have a couple of components that make life a lot easier, have robust functionality and user experience improvements, and the implementation details are actually very simple. So as the developer now that's implementing these, I might create an on valid submit handler and an on invalid submit handler, and the on valid submit, I want to send that information using the JavaScript fetch function that we defined earlier. On an invalid submit I don't do things right, so I'm just using console log because whatever. And then when we actually implement our form, we define the action where we want the form to submit things to on a valid submission we use the JavaScript submit handler. On an invalid submission we just console log it. Within that form we have two inputs, one for those email, one for the password, and you can see that this markup really simplifies what our forms could actually look like. So it's a very nice user experience or a developer experience for me, and it's a very good user experience for the end user because they get all of the quality that I've put into the input components at the end of everything. JavaScript is really awesome for forms because, well, in my opinion, when we use progressive enhancement because one is by relying on the native UI or the native elements, we get a consistent experience across all browsers and all devices. When you see a checkbox in one place and you see the same checkbox in the other place, you know how to use it. Number two, it's accessible for everyone. So able bodied users, visual users, people that prefer keyboards, people that are reliant on assistive technology, everyone can use your application, which is great. We have minimal performance impact when compared to either only using JavaScript to build out those custom form controls that we discussed earlier, and having to add all of the sort of logic in order to have feature parity and accessibility and everything. And when you enhance it with JavaScript, it works with JavaScript, but when you build it in a way to fall back to HTML, it also works in case JavaScript is disabled. There's an ad blocker somewhere, your script has an error in it for whatever reason. I know that I experienced one time that I actually tried to sign up for an application and they relied on JavaScript to sign users up. And because I had an ad blocker or a tracking blocker or something like that, the application didn't work. So as a result I couldn't even use the application. And I don't personally want to lose out on users that experience something like that. If it falls back to HTML, it's great, it still works. And lastly, when we use component frameworks, we really get the benefit of being able to compartmentalize all of the logic, all of the quality, all of the user experience improvements into one place that we can use over and over throughout our application, put that quality over and over throughout our application, and also simplify the maintenance. That's the end of my talk today. I hope you enjoyed it. If you want more. I spent like a year writing a series on all of the things that I think make building HTML forms good. I also maintain the view tensol View JS library that includes the custom input and form controls as well as a whole bunch of other things. I write a newsletter and a blog if you want more content like this. Or you can follow me on Twitter. And probably the main reason to be here today is that I have a really cute dog. His name is Nugget. He's a chewini. He's eleven pounds, loves chasing squirrels and food and you should give him a follow. So thank you very much for your time and paying attention and I hope that this talk worth it.
...

Austin Gil

Senior Full Stack Developer @ Reveal Biosciences

Austin Gil's LinkedIn account Austin Gil's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways