Letʼs enhance our forms
Time to read
, 1563 words, 4th grade
In our previous article we showed how to create a form in HTML that validates input. And does so without using JavaScript or an external library.
Check out the example form from that article that validates input without JS.
Hiding and showing the password
Now, how can we use a bit of JavaScript to enhance the experience of our form? Well, one common enhancement is to add the ability to hide and show the password. So letʼs do that. To keep it simple, we'll strip out the other fields.
<div
class="xx-form-field"
id="xx-password-field"
>
<label
class="xx-field-label"
for="xx-password-input"
id="xx-password-label"
>
Password
</label>
<br />
<div
class="xx-field-help"
id="xx-password-help"
>
Four or more space-separated words of 4+ characters.
</div>
<input
aria-labelledby="xx-password-help xx-password-label"
class="xx-field-input xx-password-field"
id="xx-password-input"
name="password"
pattern="[a-zA-Z]{4,}( [a-zA-Z]{4,}){3,}"
required=""
size="36"
type="password"
/>
</div>
This example shows an outer div
element with CSS class “xx-form-field” and ID “xx-password-field”. This
element encloses the field and has four child elements:
-
A
label
element with CSS class “xx-field-label”, ID “xx-password-label,” and thefor
attribute set to the ID of theinput
element, “xx-password-input.” -
A
br
element to move the label above the input rather than to the left of it. -
A
div
element with CSS class “xx-field-help” and ID “xx-password-help.” This element contains help for the field. -
Last but not least, the
input
element itself. We set thearia-labelledby
attribute to the IDs of the help text and the label. This means users of screen readers hear both.We give the input two CSS classes: “xx-field-input” — common to all inputs — and “xx-password-field” — unique to password inputs. And we set the ID to “xx-password-inpput.”
The
name
attribute is set to “password”. We also set therequired
attribute to make the password a required field. We set thesize
attribute to 36 to set the width of the input, and set thetype
attribute to “password” to mask the input automatically.Finally, we set the
pattern
attribute to a regular expression that limits the input to four or more words of four or more characters length. Effectively, we want a passphrase, not a password.
We are going to assume that the above is pretty self-explanatory at this point. If not, read the previous article linked above.
The easiest way to do this is to add a button. When clicked, our button
will toggle the password field type. Now password
, now
text
. Easy peasy.
globalThis.addEventListener("DOMContentLoaded", function () {
const field = document.querySelector("#xx-password-field")
const label = field.querySelector("label")
const input = field.querySelector("input")
const button = document.createElement("button")
button.type = "button"
button.classList.add("xx-toggle-password")
button.innerText = "show"
button.setAttribute("aria-label", "Show password.")
button.addEventListener("click", () => {
if (input.type === "password") {
input.type = "text"
button.innerText = "hide"
button.setAttribute("aria-label", "Hide password.")
return
}
input.type = "password"
button.innerText = "show"
button.setAttribute("aria-label", "Show password.")
})
label.appendChild(button)
})
This example shows the JavaScript needed to provide show and hide functionality to the password field. It is described in the text below.
Letʼs go through this step by step. Itʼs pretty simple.
-
We begin by adding an event listener to our
globalThis
object to run on “DOMContentLoaded” (line #1). We pass it an anonymous arrow function that will add the enhancement to the password field. -
In our arrow function, we begin by setting four
const
variables (lines #2 to #5):-
We use
document.querySelector
to get the outerdiv
element of our field using its ID. We assign this to “field.” -
Using the query selector on the field itself, we get the
label
andinput
elements by tag name. We assign them to “label” and “input” variables, respectively. -
Finally, we use
document.createElement("button")
to create abutton
element. We assign it to a variable with name, “button.” Note our careful naming to make it easy to understand our code.
-
We use
-
Now we want to configure our button. As this is different from
declaring and assigning variables, we leave a blank line. This makes
the separation of concerns obvious. Then, on lines #7 to #10, we:
-
Set the button
type
to “button” to prevent it from submitting our form when activated. Kind of important, no? -
Use
button.classList.add("xx-toggle-password")
to add a CSS class to the button. This makes it easy to style it. -
Set
button.innerText
to label the button, “show.” -
Finally, set an
aria-label
on the button to “show password.” This makes the buttonʼs function clearer to users of screen readers.
-
Set the button
-
Now we have our button. We want to add an event listener to toggle the
password field type when the user clicks the button. Weʼll leave
another blank line. Then on lines #12 to #23, we will create our event
handler as an anonymous arrow function. We assign it to the buttonʼs
click
event.-
Line #12 uses
button.addEventListener("click", ...)
to assign the arrow function to the buttonʼ click event. -
In our arrow function, we will need to handle two conditions: one
when we have masked the input and one where we havenʼt. The
safest way to test this is by checking the
type
of the input. Ergo, on line #13, we check that the inputtype
is “password,”type === "password"
, our default state. -
If the condition returns
true
(type
is “password”), then we:-
set the input
type
to “text” on line #13, -
set the buttonʼs
innerText
to “hide” on line #14, -
and set the
aria-label
to “hide password” on line #15.
-
set the input
- Then we return from the function. The password should now be visible and the button should say, “HIDE.”
-
If the condition fails, i.e., the input
type
isnʼt “password,” then we have unmasked the field. So we:-
set the
type
back to “password” (line #18) -
set the buttonʼs
innerText
back to “show” -
and of course, our
aria-label
back to “show password.”
-
set the
-
Line #12 uses
-
Last but not least, we add our button to the form on line #22 with
label.appendChild(button)
. As a screen reader reads our label aloud, so, too, will it read the buttonʼsaria-label
.
As we say, easy peasy.
Why not a component library?
“But … but … but …” we can hear some readers say, “why not just use a pre-coded field from a popular component library?”
And, “why would we want to have to write this code every time we wanted an enhanced password field?”
Why on Earth would we do that? Or on Mars, for that matter?
I put this field together once. I use Astro for its bundling benefits: it makes a component architecture easy. I have no need for its “islands” architecture. So, I have been building my own component library bit by bit (pun intended).
Although I shun passwords as much as practicable, it pays to have a
field like this. So I added a PasswordField
to my Astro component library.
And I included a prop called allowShow
. When it is true
, then the component includes the JS enhance script and features a “SHOW” button. When it is false
or missing, the component
does not.
Easy peasy, as I may have said once or twice before. And the benefit is obvious:
I know every line of the code.
I wrote it, and the responsibility for making sure it is bug-free, accessible, user-friendly, etc. is on me. I am not counting on devs I have never met to keep my codebase current, robust, and secure.
And because it is an ad hoc component — written specifically to fill my needs — rather than a highly-abstracted, one-size-fits-all attempt to please everyone, it can be small and efficient. No superfluous parts. William of Ockham would be proud.
Case in point:
<div class="MuiFormControl-root css-kzv9dm">
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-sizeMedium MuiInputLabel-filled MuiFormLabel-colorPrimary MuiFormLabel-filled MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-sizeMedium MuiInputLabel-filled css-1rgmeur"
data-shrink="true"
for="filled-adornment-password"
>
Password
</label>
<div class="MuiInputBase-root MuiFilledInput-root MuiFilledInput-underline MuiInputBase-colorPrimary MuiInputBase-formControl MuiInputBase-adornedEnd css-1thjcug">
<input
aria-invalid="false"
id="filled-adornment-password"
type="password"
class="MuiInputBase-input MuiFilledInput-input MuiInputBase-inputAdornedEnd css-ftr4jk"
value=""
/>
<div class="MuiInputAdornment-root MuiInputAdornment-positionEnd MuiInputAdornment-filled MuiInputAdornment-sizeMedium css-1mzf9i9">
<button
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-edgeEnd MuiIconButton-sizeMedium css-slyssw"
tabindex="0"
type="button"
aria-label="toggle password visibility"
>
<svg
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-vubbuv"
focusable="false"
aria-hidden="true"
viewBox="0 0 24 24"
data-testid="VisibilityIcon"
>
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"></path>
</svg>
<span class="MuiTouchRipple-root css-w0pj6f"></span>
</button>
</div>
</div>
</div>
This example shows the HTML output of a password component from the
MUI component library. There is an outer div
element. This
encloses a label
element and another div
element.
The inner div
element contains an input
element (the password input) and yet another div
element. That innermost div
element contains a button
element which contains an svg
icon of an eye.
The label
has a data-shrink
attribute, most likely
used to cue the CSS/JS to use the Material Design placeholder-shrinks-to-tiny-label
design. The input
element has an aria-invalide
attribute set to “false.”
The button
has its tabindex
set to zero, pointlessly.
It also has an aria-label
of “toggle password
visibility.” Finally, the svg
element has focusable
set to false
and aria-hidden
set to true
.
There is also an empty span
element with class name “MuiTouchRipple” indicating that it is used solely for a visual “ripple” effect.
To be fair, the MUI folks have improved their code in many ways over the
past several years. Now they use the proper data-
attribute format. They have discovered aria-
attributes as well.
They even have an aria-label
attribute. That said, “toggle password visibility” is wordy and fails to make clear the current
state of the field. Is it unmasked or masked?
There remain several UX problems, but weʼll save those for another article.
What is most noticeable about the above code is the excessive number of CSS class names. Why on Earth do we need so many?
Well, here is the root of the problem. MUI must abstract beyond belief so that anyone can use it for anything. This is not an MUI problem; it is a component library problem.
The irony is that MUI was once based on Material Design, and still is to some degree. But they have wrangled it to the point that you could make it look like an old Windows 95 form if you wanted to.
But why? My sites each have a specific look and feel. I have a design system for each. I do not need an enormous stylesheet and a million classnames to make them look and work in different ways. Ways that I will never need.
I give each relevant HTML element one class — maybe two. Then I write my stylesheet to set the specific style for my design system.
It gets worse
Oh, but thereʼs more.
Where is the CSS for this field? Where is the JavaScript?
Why, they have bundled them away somewhere else. Good luck finding them. Good luck changing them.
Instead, MUI provides a complex system for configuring their components. Hey, you are no longer a programmer or even a developer. You are now a configurer.
Fun, right?
If you want this field to work some way that the MUI folks didnʼt allow for, then good luck. I have spent many painful hours trying to force MUI to fit some designerʼs style preference. Oh, the PTSD!
But when we code the field ourselves with simple HTML, CSS, and JavaScript, then it is all right there.
A recent fad — devs can never leave well enough alone — is something called LoB, Locality of Behavior. LoB states that the behavior of a unit of code should be as obvious as possible by looking only at that unit of code.
I guess multiple screens and the ability to put several files side-by-side was the wrong path. Silly me.
But with my Astro component model, I could, if I wanted to (I donʼt), create a Single-File Component (SFC). That would put the CSS, JS, and HTML all in the same “PasswordField.astro” file.
Me? I prefer to keep structure/semantics, style/layout, and behavior separate. In separate files. And with roughly 10ha of screen real estate at my workstation, I can refer to side-by-side files quickly as I work.
So my Astro PasswordField component has a folder called “PasswordField” and three files:
PasswordField/
-
index.astro
index.css
index.ts
I import the CSS and TypeScript files at the top of my Astro file. Astro builds the component, transpiles the TS to JS, and bundles and dedupes all the CSS and JS.
We can do a lot more with simple JavaScript enhancements. But it might surprise you how little we need to do so.
In a coming article, we will show how many HTML form components are already quite enhanced enough.