Start with workable HTML forms, then make them better.
Less than 6 minutes, 1487 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>
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)
})
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”. 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:- We use
document.querySelector
to get the outer<div>
element of our field using its ID. We assign this to “field.” - Using the query selector on the field itself, we get the
<label>
and<input>
elements by tag name. We assign them to “label” and “input” variables, respectively. - Finally, we use
document.createElement("button")
to create a<button>
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 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 we will create our event handler as an anonymous arrow function. We assign it to the buttonʼs
click
event.- We use
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, 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”, - set the buttonʼs
innerText
to “hide”, - and set the
aria-label
to “hide password”.
- 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”, - set the buttonʼs
innerText
back to “show”, - and of course, our
aria-label
back to “show password.” And thereʼs our toggle.
- set the
- We use
- Last but not least, we add our button to the form 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>
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.
Top comments (0)