1

HTML Input Validation Without a Form

 2 years ago
source link: https://www.aleksandrhovhannisyan.com/blog/html-input-validation-without-a-form/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

HTML Input Validation Without a Form

Jan 16, 2022

When you submit an HTML form, your browser first performs client-side validation to make sure that the form contains clean data before sending it off to the server. This is done by comparing each input's value to constraints defined via certain HTML input attributes, like required, pattern, and others.

However, it's not always the case that you want (or need) to submit a form. Sometimes, you just want to use a form to retrieve user input but store those values as part of your application's state on the client side. Unfortunately, this means that you miss out on this auto-validation behavior because you're no longer using a submittable form.

But the good news is that we can still validate inputs without a form or a submit button. And we can do this without reinventing the wheel or creating accessibility problems for our users. All we need to do is use methods that browsers already provide for HTML input validation.

Table of Contents

A Note on Client-Side Validation

Before we proceed, I want to note two caveats about client-side validation for HTML forms.

First, while it can make for a better user experience because it allows you to provide real-time feedback to users as they fill out your form, it's completely dependent on JavaScript loading (at all, or correctly). If a user browses your app with JavaScript disabled (either voluntarily or some other reason), your form won't work without a submit button.

Second, it's important to understand that client-side validation should not be relied upon for any data that you intend to later send to a server. The approach described in this tutorial is only for client-side validation in apps that are storing user input in local state. In this case, since we're not dealing with any server-side logic, we don't need to worry about this issue.

HTML Input Validation with JavaScript

We'll first look at how to validate inputs without a submit button using vanilla JavaScript. Towards the end of this article, we'll also look at how you can use a JavaScript framework like React to create a self-validating input component.

Checking HTML Input Validity with checkValidity

Suppose we have an input that accepts an even integer from 2 to 10, inclusive:

<input id="input" type="number" min="2" max="10" step="2">

Certain input attributes prevent a user from ever entering disallowed values. For example, type="number" prevents a user from entering text. By contrast, while the min, max, and step attributes define constraints for the value, the input doesn't actually enforce those requirements because a user can still go in and manually input an invalid value, like 11. In its current state, this input won't provide any feedback to the user to indicate that the value they entered is not allowed because the input isn't part of a submittable form.

Fortunately, we can still validate this input ourselves using very little JavaScript. Every HTML input element has a checkValidity method that returns true if the input's current value passes validation and false otherwise:

const input = document.querySelector('#input');
input.addEventListener('input', (e) => {
  const isValid = e.target.checkValidity();
  console.log(isValid);
});

The browser validates the input behind the scenes by comparing the input's value to its constraints. In this example, those constraints are enforced by the type, min, max, and step attributes, effectively requiring that all inputs be even integers. If a user enters an invalid value, like 5, the checkValidity method will return false.

In its current state, the code we just wrote doesn't do anything meaningful. How do we provide feedback to the user to indicate that their input is invalid?

Reporting Input Validity with reportValidity

You may be tempted to give users feedback on their inputs with custom tooltips or related UI. But depending on how these are implemented, they may not convey the right semantics to users (and could therefore be inaccessible). It's much better to rely on the web's built-in APIs to provide input feedback. These same APIs are used to show accessible native tooltips when a user attempts to submit a form containing invalid data.

For this task, HTML inputs provide us with the reportValidity method. This can be used together with checkValidity to provide feedback to the user in an accessible manner:

const input = document.querySelector('#input');
input.addEventListener('input', (e) => {
  const isValid = e.target.checkValidity();
  if (!isValid) {
    e.target.reportValidity();
  } else {
    // if the input is valid, handle it accordingly here
  }
});

You'll want debounce the event listener to avoid reporting validity on every keystroke.

When invoked on an invalid input, reportValidity will:

  1. Forcibly re-focus the input.
  2. Show a native tooltip (with a role of alert) clarifying why the validation failed.

Screen readers will narrate the alert correctly, and sighted users will see the message in a familiar form tooltip whose styling depends on the browser and operating system being used. The browser may also suggest how the user can correct their input. Returning to our earlier example, if a user enters an odd number, the browser will suggest the two closest numbers:

A numeric input box contains the value 5 and two controls: up and down. Below it is a tooltip that reads: 'Please enter a valid value. The two nearest valid values are 4 and 6.'.

Communicating Input Validity with aria-invalid

Using reportValidity provides the user with feedback in an accessible manner that screen readers recognize and narrate appropriately. But for the sake of completeness, we should also set the aria-invalid attribute, which is used to convey an input's validity state. This is just a matter of setting the attribute on every input event, like this:

const isValid = e.target.checkValidity();
e.target.setAttribute('aria-invalid', !isValid);

If aria-invalid is true, a screen reader will identify the input as invalid when narrating it.

Determining Why an Input Failed Validation

In the examples we looked at so far, we only ever checked if an input has a valid or invalid value, but we never determined why the validation failed. And that's expected—you usually won't need to do this yourself because reportValidity already determines the failure condition and reports an appropriate error message. But in case you do need to know why an input failed validation, you can check InputElement.validity, which is a ValidityState object containing boolean flags for the following conditions:

  • badInput
  • customError
  • patternMismatch
  • rangeOverflow
  • rangeUnderflow
  • stepMismatch
  • tooLong
  • tooShort
  • typeMismatch
  • valueMissing

For example, suppose we have a text field that only accepts letters but not numbers:

<input id="letters-only" type="text" pattern="[a-zA-Z]*">

If this input's current value contains a number, then validity.patternMismatch will be true. You can then check this condition and handle it accordingly in your code.

Showing a Custom Error Message

HTML input attributes can enforce a wide range of constraints, and reportValidity offers a convenient and accessible way of providing feedback to the user. However, you may sometimes want to check an input's validity yourself and show a custom message.

We can do this using the setCustomValidity method, which overrides the browser's default error message for the input and displays the message when we call reportValidity.

Suppose we have the same HTML input that only accepts letters:

<input id="letters-only" type="text" pattern="[a-zA-Z]*">

If a user enters numbers, the browser will only show a vague error message, like "Please match the requested format." In practice, this input should have an appropriate label associated with it that clarifies the expected format. But in any case, we can still detect the error condition by checking validity.patternMismatch in our event handler and setting a custom message:

const input = document.querySelector('#letters-only');
input.addEventListener('input', (e) => {
  const isValid = e.target.checkValidity();
  e.target.setAttribute('aria-invalid', !isValid);
  if (e.target.validity.patternMismatch) {
    e.target.setCustomValidity('You may only enter letters.');
  } else {
    e.target.setCustomValidity('');
  }
  if (!isValid) {
    e.target.reportValidity();
  }
});

Now, the user sees our custom message instead of the browser's default for that type of error:

A text input is labeled as: 'Enter only letters'. The text input's current value is abc123. A native  browser tooltip is visible below the input and reads: 'You may only enter letters.'

All of the code is the same as before, except now we have this new condition:

if (e.target.validity.patternMismatch) {
  e.target.setCustomValidity('You may only enter letters.');
} else {
  e.target.setCustomValidity('');
}

Note that we need to clear the custom validity message if the input is valid, or else it will stick around forever. This is done by passing an empty string to the function: setCustomValidity('').

Creating a Self-Validating Input Component

All of the code that we looked at in this tutorial can be used in any JavaScript framework to provide feedback on input validity without a submittable form. But one of the great things about using a framework is that we can create a custom component to centralize and standardize all of this logic throughout our code base. More specifically, we can create an Input component that wraps the native input, accepts the same props, and checks the input's validity before invoking its onChange prop. Here's an example using React:

const Input = (props) => {
  const { onChange, ...otherProps } = props;
  const [isValid, setIsValid] = useState(true);

  const handleChange = (e) => {
    const isValid = e.target.checkValidity();
    setIsValid(isValid);
    if (isValid) {
      onChange?.(e);
    } else {
      e.target.reportValidity();
    }
  };

  return (
    <input
      onChange={handleChange}
      aria-invalid={!isValid}
      {...otherProps}
    />
  );
};

The component intercepts the onChange prop and only ever calls it if the input is valid. It also maintains some local state for the validity so it can set the aria-invalid attribute accordingly.

As I mentioned before, you'll probably want to debounce your input to avoid thrashing the browser with repeat updates on every keystroke. You can do this using any debounce function as well as a prop that controls the delay of the onChange event. You'll want to memoize this event handler so that it isn't reconstructed on every render; in React, we can do this with the useMemo hook. Here's the updated code:

const Input = (props) => {
  const { onChange, delayMilliseconds, ...otherProps } = props;
  const [isValid, setIsValid] = useState(true);

  const debouncedHandleChange = useMemo(
    () => {
      const handleChange = (e) => {
        const isValid = e.target.checkValidity();
        setIsValid(isValid);
        if (isValid) {
          onChange?.(e);
        } else {
          e.target.reportValidity();
        }
      };
      if (delayMilliseconds === 0) {
        return handleChange;
      }
      return debounce(handleChange, delayMilliseconds);
    },
    [delayMilliseconds]
  );

  return (
    <input
      onChange={debouncedHandleChange}
      aria-invalid={!isValid}
      {...otherProps}
    />
  );
};

Summary

When a traditional HTML form is submitted, the browser validates each input and reports any issues. But it's not always the case that you want or need to submit form data to a back end. If all you want is to use a form to receive user input and store it locally in your app's state, you can use the checkValidity, reportValidity, and setCustomValidity methods to provide feedback to the user. If you're using a JavaScript framework, you can also take things a step further and create a custom self-validating Input component.

Comments

Post comment

This comment system is powered by the GitHub Issues API. You can learn more about how I built it or post a comment over on GitHub, and it'll show up below once you reload this page.

Loading...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK