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.