Skip to main content

Making Drupal 8 datetime widgets use human formats

Drupal 8 includes a datetime field type and widget out of the box.

The widget uses the HTML5 date element on supported browsers, providing a polyfill to a text field with jQuery UI's datepicker for browsers that don't yet support HTML5 date inputs.

However - HTML5 date formats always work in ISO8601 format - ie YYYY-MM-DD - which isn't very user-friendly for those using Firefox and Internet Explorer.

Luckily, with a few tweaks you can easily swap this into DD/MM/YYYY format for those browsers and then switch it back server side into the format Drupal expects.

 

by lee.rowlands /

Step 1 - Telling jQuery UI what format to display the date in.

Luckily, the JavaScript that provides the polyfill interrogates a data attribute on the field to decide what date format to use when the user selects a value.

So our first step is to make sure this attribute is set.

We start with a hook_element_info_alter() and add a process callback:

/**
 * Implements hook_element_info_alter().
 */
function datetime_tweaks_element_info_alter(array &$types){$types['datetime']['#process'][] = 'datetime_tweaks_datetime_set_format';
}

And then in our process callback, we make sure we set the data-drupal-date-format and #date_date_format attributes. We also set a nicer title value - both for user feedback and also for modules like Clientside validation - which use this for error messages.

/**
 * Element process callback for datetime fields.
 */
function datetime_tweaks_datetime_set_format($element){
// Use d/m/Y format.
$element['date']['#attributes']['data-drupal-date-format'] = ['d/m/Y'];
$element['date']['#date_date_format'] = 'd/m/Y';
$element['date']['#attributes']['title'] = t('Enter a valid date - e.g. @format', [
'@format' => (new \DateTime())->format('d/m/Y'),
]);return $element;
}

Now when the user selects the value in the jQuery time-picker, the value is pasted in the more friendly d/m/Y format. However we have a problem.

Drupal expects the incoming values to be in Y-m-d format, as that is how the HTML5 date element works. The values are sent/received in Y-m-d and the browser formats them according to the user's locale settings. E.g. all of the world except the US would get d/m/Y while the US would get m/d/Y.

So we need to make sure we switch the values back.

Step 2 - switching values back.

Again we go to our old friend hook_element_info_alter(), this time we add a #value_callback to override the default one provided by the datetime element - (\Drupal\Core\Datetime\Element\Datetime::valueCallback).

/**
 * Implements hook_element_info_alter().
 */
function datetime_tweaks_element_info_alter(array &$types){
  $types['datetime']['#value_callback'] = 'datetime_tweaks_datetime_value';$types['datetime']['#process'][] = 'datetime_tweaks_datetime_set_format';
}

Then in our value callback we do the switch if the incoming value is in the format we were expecting. We use the very handy \Drupal\Component\Datetime\DateTimePlus::createFromFormat inside a try-catch block, that way we only convert dates that match the format we switched the jQuery UI datepicker to use. For browsers that aren't using the polyfill, they'll already be sending the value in Y-m-d and we don't want to intervene. Finally now that we've switched the date back to the expected format, we let the default value callback run.

/**
 * Element validate callback for browsers that don't support HTML5 type=date.
 */
function datetime_tweaks_datetime_value(&$element, $input, FormStateInterface $form_state){
if ($input !== FALSE){
try {
if ($date = DrupalDateTime::createFromFormat('d/m/Y', $input['date'], !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL)){
// Core expects incoming values in Y-m-d format for HTML5 date elements.
$input['date'] = $date->format('Y-m-d');
}
}
catch (\Exception $e){
// Date is not in d/m/Y format - nothing to do.
}
}
return Datetime::valueCallback($element, $input, $form_state);
}

Now we have the values being sent in d/m/Y from browsers that don't support the date element, but switched back into the format Drupal expects, however Drupal will still be sending default values in the format it is expecting - Y-m-d, so we need to handle those.

Step 3 - Default values

So for browsers that don't support the date element, the incoming default values in the DOM will be in Y-m-d format, but we want to display them to the user in d/m/Y format. So we go back to our process callback and attach some JavaScript that uses Modernizr to check for date support, and applies a polyfill to switch the formats clientside.

/**
 * Element process callback for datetime fields.
 */
function datetime_tweaks_datetime_set_format($element){
// Use d/m/Y format.
$element['date']['#attributes']['data-drupal-date-format'] = ['d/m/Y'];
$element['date']['#date_date_format'] = 'd/m/Y';
$element['date']['#attributes']['title'] = t('Enter a valid date - e.g. @format', [
'@format' => (new \DateTime())->format('d/m/Y'),
]);$element['#attached']['library'][] = 'datetime_tweaks/default_date';
return $element;
}

And then in our new polyfill for default date

/**
 * @file
* Default date values.
 */

(function ($, Drupal) {

  'use strict';

  Drupal.behaviors.datetimeTweaksDefaultDate = {
    attach: function (context, settings) {
      var $context = $(context);
      // Skip if date is supported by the browser.
if (Modernizr.inputtypes.date === true) {
        return;
      }
      $context.find('input[data-drupal-date-format]').once('default-date').each(function () {
        var $el = $(this);
        var val = $el.val();
        // If default date is in Y-m-d format, switch to d/m/Y for browsers
        // that don't support html5 date format.
if (val.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
          var parts = val.split('-');
          $el.val(parts[2] + '/' + parts[1] + '/' + parts[0]);
        }
      });
    }
  };

})(jQuery, Drupal);

So we now have the pieces we want.

Bonus points - removing seconds from time element.

In addition, the default behaviour for browsers that support the date element (e.g. Chrome) is to require seconds for time, which is rarely needed, and can also be easily changed.

Back in our process callback, we add a nicer title, and change the step attribute. The default step attribute is 1, meaning the element accepts time increments of 1 second. Switching this to 60 means the smallest unit is minutes and the seconds element isn't added by the browser.

/**
 * Element process callback for datetime fields.
 */
function datetime_tweaks_datetime_set_format($element){
// Use d/m/Y format.
$element['date']['#attributes']['data-drupal-date-format'] = ['d/m/Y'];
$element['date']['#date_date_format'] = 'd/m/Y';
$element['date']['#attributes']['title'] = t('Enter a valid date - e.g. @format', [
'@format' => (new \DateTime())->format('d/m/Y'),
]);
$element['time']['#attributes']['title'] = t('Enter a valid time - e.g. @format', [
'@format' => (new \DateTime())->format('h:i'),
]);
// Remove seconds in browsers that support HTML5 type=date.
$element['time']['#attributes']['step'] = 60;
$element['#attached']['library'][] = 'datetime_tweaks/default_date';
return $element;
}

Wrapping up

So if this sounds useful for your project, all of the code can be found in this github repository

Thanks to all those that worked on Date support in core, especially the maintainers - Jonathan Hedstrom 'jhedstrom' and Matthew Donadio 'mpdonadio'.

If you want to help out - they're hard at work on adding support for end dates in a single field - why not head over there and help out with reviews and manual testing?

Tagged

Drupal 8, Date