Form Validation: You want :not(:focus):invalid, not :invalid

Update 2021-01-28: In case your form controls have the required attribute set, you’ll even want to use the more extensive :not(:focus):not(:placeholder-shown):invalid selector.

See the update at the end of this post for more info on this.

We’ve all been in this situation, where the built-in form validation of the browser starts complaining while you’re still entering data into the form:

Highly annoying, but thankfully there’s an easy way to fix this.

~

# The problem

The problem is caused by a piece of CSS similar to this snippet:

.error-message {
    display: none;
}

input:invalid {
  border-color: var(--color-invalid);
}

input:invalid ~ .error-message {
  display: block; 
}

input:valid {
  border-color: var(--color-valid);
}

When entering an e-mail address — as Ryan is doing above — this is extremely annoying as your e-mail address is only valid when you’re done entering it. Try it in the demo below.

See the Pen
Form Validation on Blur (1/4)
by Bramus (@bramus)
on CodePen.

Ugh! 🤬

💁‍♂️ For a slight moment you’ll notice that an e-mail address in the form of [email protected] (e.g. without a .tld suffix) is also considered valid. As per RFC 822 the [email protected] format — used mainly in local networks — indeed is allowed.

~

# Making things better

It would be nice to only perform the validation when the field is not being edited anymore. In CSS we don’t have a blur event, but what we do have is a pseudo-class selector to indicate whether an input has the focus: :focus. Combine that with :not() and we have a way to target the “not being focussed” state, which also indicates that the field is not being edited.

Putting it all together, our CSS becomes this:

.error-message {
    display: none;
}

input:not(:focus):invalid {
  border-color: var(--color-invalid);
}

input:not(:focus):invalid ~ .error-message {
  display: block; 
}

input:not(:focus):valid {
  border-color: var(--color-valid);
}

This way the validations only happen when you’re blurred out of the form.

See the Pen
Form Validation on Blur (2/4)
by Bramus (@bramus)
on CodePen.

Ah, that’s better! 😊

~

# Making things even more better

In the demo above you’ll see one small side-effect though: the border is green by default, even though we didn’t enter any value. This is not exactly what we want Ideally we only want to validate in case the field is both not focussed and not empty.

In CSS we can’t use :empty for this though, as :empty targets elements that have no children/innerHTML content. What we can do however is abuse the :placeholder-shown pseudo-class.

  • If there’s no text entered, the placeholder is shown
  • If there is text entered, the placeholder is not shown

With this in mind, our code now becomes this:

.error-message {
    display: none;
}

input:not(:focus):invalid {
  border-color: var(--color-invalid);
}

input:not(:focus):invalid ~ .error-message {
  display: block; 
}

input:not(:focus):not(:placeholder-shown):valid {
  border-color: var(--color-valid);
}

⚠️ Do note that this requires a value for the input‘s placeholder.

<input type="email" placeholder="you​@​example​.​org" />

If you don’t want any placeholder to show, set its value to   (space)

Here’s an adjusted demo:

See the Pen
Form Validation on Blur (3/4)
by Bramus (@bramus)
on CodePen.

Yes, exactly what we want! 🤩

Note: In the future we will be able to use the :user-invalid pseudo class for exactly this use-case.

The :user-invalid pseudo-class represents an element with incorrect input, but only after the user has significantly interacted with it.

This feature is still in the works and not supported yet. Firefox supports it using the non-standard ::-moz-ui-invalid name. Thanks for the tip, Schepp!

~

# Update 2021-01-28: Playing nice with required

As reader Corey pointed out in the comments below the code above does not play nice with the required attribute. When the attribute is added, the error message will be shown when the form loads.

To work around this we also need to include the :not(:placeholder-shown) pseudo-class in our :invalid selectors.

.error-message {
    display: none;
}

input:not(:focus):not(:placeholder-shown):invalid {
  border-color: var(--color-invalid);
}

input:not(:focus):not(:placeholder-shown):invalid ~ .error-message {
  display: block; 
}

input:not(:focus):not(:placeholder-shown):valid {
  border-color: var(--color-valid);
}

Putting it all together, here’s our final demo:

See the Pen
Form Validation on Blur (4/4)
by Bramus (@bramus)
on CodePen.

Phew! 😅

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Note: The title of this post is definitely a reference to this post on CSS-Tricks and this post by Kilian.

React Hook Form – Form Validation Hook based on Uncontrolled Components

Now this is an interesting approach, building further upon Uncontrolled Components:

The React Hook Form (react-hook-form) library provides a new form validation mechanism which embraces uncontrolled form validation and support controlled components.

The core idea is to register HTML input’s ref into the custom hook and therefore subscribe to its input value, validate and evaluate the form submission.

I guess this one makes more sense to many as it immediately gives you a values object containing all form values. Validation is also built-in

import React from "react";
import useForm from "react-hook-form";

const Example = () => {
  const { handleSubmit, register, errors } = useForm();
  const onSubmit = values => {
    console.log(values);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        ref={register({
          required: 'Required',
          pattern: {
            value: /^[A-Z0-9._%+-][email protected][A-Z0-9.-]+\.[A-Z]{2,4}$/i,
            message: "invalid email address"
          }
        })}
      />
      {errors.email && errors.email.message}

      <input
        name="username"
        ref={register({
          validate: value => value !== "admin" || "Nice try!"
        })}
      />
      {errors.username && errors.username.message}

      <button type="submit">Submit</button>
    </form>
  );
};

React Hook Form Presentation (Twitter Thread) →
React Hook Form →

💡 The React Hook Form Twitter Thread was compiled from its origin tweet to one single page using Threader

Symfony Form Validation: Validating a date range

One of the (Symfony based) PHP projects I’m working on contains a form which allows the user to generate video clips from CCTV footage. To do this the user can enter a start and stop DateTime. For this to work the submitted input data is then checked: both start and stop must be dates, and the stop date must be set to a point in time after the start date.

Symfony’s DateTime Constraint can make sure both entries are DateTime instances. To check whether that the end date is after the begin date, one can use the Callback Constraint. Injected into that callback is a ExecutionContextInterface by which you can access the form, and thus other form params.

Here’s an example with the inputs start and stop:

use Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

// …

$builder
    ->add('start', 'datetime', 
        'constraints' => [
            new Constraints\NotBlank(),
            new Constraints\DateTime(),
        ],
    ])
    ->add('stop', 'datetime', [
        'constraints' => [
            new Constraints\NotBlank(),
            new Constraints\DateTime(),
            new Constraints\Callback(function($object, ExecutionContextInterface $context) {
                $start = $context->getRoot()->getData()['start'];
                $stop = $object;

                if (is_a($start, \DateTime::class) && is_a($stop, \DateTime::class)) {
                    if ($stop->format('U') - $start->format('U') < 0) {
                        $context
                            ->buildViolation('Stop must be after start')
                            ->addViolation();
                    }
                }
            }),
        ],
    ]);

If you want to have a minimum duration between both start and stop, you can change the number 0 in the snippet above to any number of seconds.

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

React Pattern: Centralized PropTypes

Cory House:

In real apps with large objects, using PropTypes quickly leads to a lot of code. That’s a problem, because in React, you’ll often pass the same object to multiple components. Repeating these details in multiple component files breaks the DRY principle (don’t repeat yourself). Repeating yourself creates a maintenance problem. The solution? Centralize your PropTypes.

Using the shape function of the prop-types package it’s possible to define types yourself. You could have your own definitions an address or a user for example. These type definitions can then be used like other prop-types types.

Cory centralizes his (custom) PropTypes in types/index.js

import { shape, number, string, oneOf } from 'prop-types';

export const addressType = shape({
    id: number.isRequired,
    street: string.isRequired,
    street2: string,
    city: string.isRequired,
    state: string.isRequired,
    postal: number.isRequired,
});

export const userType = shape({
  id: number,
  firstName: string.isRequired,
  lastName: string.isRequired,
  company: string,
  role: oneOf(['user', 'author']),
  address: addressType.isRequired,
});

… and then imports them when needed:

import React, { Component } from 'react';
import {userType} from './../types';

export default class User extends Component {
  static propTypes = {
    user: userType.isRequired,
  };

  // …
}

React Pattern: Centralized PropTypes →