All web forms must be validated, both on the client side and on the server side; so says the law of GIGO. When HTML5 form validation was first introduced, I was hopeful that it would standardize this mundane implementation task and make life a little easier. It did standardize form validation error display and improve the user experience somewhat, and also aspired to meet web accessibility guidelines. But it ended up introducing a whole new set of challenges instead, including the error messages which I think are too generic and leave a lot of room for improvement.

Here is a technique that I developed for implementing custom validation logic and messages directly in Twig templates for Drupal 8. I wrote another post about specific form element theme suggestions if you want to target specific fields, but you can use this technique for generalized form element templates as well.

Getting Started

I read an informative article on stackoverflow that discusses the structure that makes this all work, and made me think of how to localize all of the logic directly in Twig. Twig exposes Drupal's attribute object in most form element templates as the primary element wrapper, making it convenient to add only what you need, and HTML5 validation uses the setCustomValidity JavaScript method to define what messages appear.

The goal is to define the HTML event handler attributes oninput and oninvalid like in this example text field:


oninvalid = this event sets the validation message through setCustomValidity

oninput = this event re-sets the value on valid input, making the message empty and not displayed

The HTML of this example field:

<input type="text" 
minlength="5" required="required" placeholder="Your Name" 
oninput="this.setCustomValidity('')" 
oninvalid="this.setCustomValidity('Please provide your name')">

The attributes object contains a collection of all of the attributes needed to render the tag, which can be added to by core and other modules and plugins when processed. In Twig, you will only need to add the additional attributes for validation.

Sure, you could develop field widget hooks for similar effect, which would look something like this:

function hook_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
  ...
  $element['#attributes']['minlength'] = "5";
  $element['#attributes']['oninput'] = "this.setCustomValidity('')";
  $element['#attributes']['oninvalid'] = "this.setCustomValidity('Please provide your full name')";
  ...
}

But these examples show how concise and compact it is to implement just in Twig:

Minimum field length

{%
 set attributes = attributes
  .setAttribute('minlength', '5')
  .setAttribute('oninput', "this.setCustomValidity('')")
  .setAttribute('oninvalid', "this.setCustomValidity('Please provide your full name')")
%}
<input{{ attributes }} />{{ children }}

Email address pattern

{%
 set attributes = attributes
  .setAttribute('pattern', "^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},*[\W]*)+$")
  .setAttribute('oninput', "this.setCustomValidity('')")
  .setAttribute('oninvalid', "this.setCustomValidity('Please provide a valid email address')")
%}
<input{{ attributes }} />{{ children }}

Required select option

I've set the required attribute here, usually you'd set this on the field definition.

{%
 set attributes = attributes
  .setAttribute('required', 'required')
  .setAttribute('oninput', "this.setCustomValidity('')")
  .setAttribute('oninvalid', "this.setCustomValidity('Please select a topic')")
%}
<select{{ attributes }}>
  {% for option in options %}
    <option value="{{ option.value }}"{{ option.selected ? ' selected="selected"' }}>
       {{ option.label }}
    </option>
  {% endfor %}
</select>

Any textfield

This could be set in a generic template like input.html.twig, so your message would be used for any input field that's not otherwise defined

{%
 set attributes = attributes
  .setAttribute('oninput', "this.setCustomValidity('')")
  .setAttribute('oninvalid', "this.setCustomValidity('This field is required, please provide a value')")
%}
<input{{ attributes }} />{{ children }}

As with many aspects of Drupal, there's more than one way to skin a cat. This technique allows you to localize your field validation logic and messaging all in your Twig template directly, without having it spread out across separate javascript files and module hook PHP code.

Last Updated November 16th, 2019