DEV Community

Cover image for Letʼs enhance our forms
Charles F. Munat for Craft Code

Posted on • Edited on • Originally published at craft-code.dev

Letʼs enhance our forms

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>
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

Letʼs go through this step by step. Itʼs pretty simple.

  1. 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.
  2. In our arrow function, we begin by setting four const variables:
    1. We use document.querySelector to get the outer <div> element of our field using its ID. We assign this to “field.”
    2. 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.
    3. 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.
  3. 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:
    1. Set the button type to “button” to prevent it from submitting our form when activated. Kind of important, no?
    2. Use button.classList.add("xx-toggle-password") to add a CSS class to the button. This makes it easy to style it.
    3. Set button.innerText to label the button, “show.”
    4. Finally, set an aria-label on the button to “show password.” This makes the buttonʼs function clearer to users of screen readers.
  4. 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.
    1. We use button.addEventListener("click", ...) to assign the arrow function to the buttonʼ click event.
    2. 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 input type is “password,” type === "password", our default state.
    3. If the condition returns true (type is “password”), then we:
      1. set the input type to “text”,
      2. set the buttonʼs innerText to “hide”,
      3. and set the aria-label to “hide password”.
    4. Then we return from the function. The password should now be visible and the button should say, “HIDE.”
    5. If the condition fails, i.e., the input type isnʼt “password,” then we have unmasked the field. So we:
      1. set the type back to “password”,
      2. set the buttonʼs innerText back to “show”,
      3. and of course, our aria-label back to “show password.” And thereʼs our toggle.
  5. 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ʼs aria-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>
Enter fullscreen mode Exit fullscreen mode

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)