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

Posted by Lee Rowlands
Senior Drupal Developer

Dated

Comments

Comment by MaskOta

Dated

This line

$element['date']['#date_date_format'] = 'd/m/Y';

Causes the date picker to start in the year 2021 (min year value) instead of the current date. If i remove that line of code the date picker works correctly but after you select a date it is displayed in the default Y-m-d format on the field.

Comment by Tom

Dated

You can re-initialize the jQuery datepicker to use the correct format.
$el.datepicker({
dateFormat: "dd/mm/yy"
});

Comment by Ismini

Dated

I have the same problem (only in Mozilla, in Chrome works fine). Did you find a way to fix it?

Comment by Anonymous

Dated

Nice article!

Comment by @JustDrupaling

Dated

Worked great for me. I wanted the dates in m/d/y format so I just went ahead and swapped those values in the .module file. Thank you so much!

Comment by Sylvain Lavielle

Dated

I had the same issue. I did this :

In the PHP part, Do not call :

$types['datetime']['#process'][] = 'datetime_tweaks_datetime_set_format';

and remove datetime_tweaks_datetime_set_format callback function

In the javascript part, just switch the dateFormat, letting the datepicker convert the date itself in the desired format

...
$context.find('input[data-drupal-date-format]').once('default-date').each(function () {
$(this).datepicker( "option", "dateFormat", "dd/mm/yy" );
});
...

It worked for me so far.

Tom, thanks for the trick, it helped a lot

Comment by RM Drupaler

Dated

Hello Sylvain Lavielle,
Whether this is still works for you? I am not able to bring into dd/mm-yyyy format for the browser supporting html5 date. For the browser not supporting html5, it woks fine.
Please share your feedback.

Comment by Brooke

Dated

Wow. The fact that all of this is necessary is... insane. C'mon Drupal 8 :/

Comment by Brooke

Dated

Thanks for this. Very odd that seconds are 'standard' rather than optional. This is all rather a pain.

Comment by Paul Driver

Dated

Thank you for this Lee. All that is needed for the date field now is an all day check box and decent date and time picker

Comment by Max

Dated

Unfortunately this does not resolve the date presentation issues when using HTML5 date (in browsers which support it). For example if my computer has locale settings with the date format mm/dd/yyyy, then tooltip will be wrong for me it will still say dd/mm/yyyy because it has been built on backend. And this is not possible to handle this case properly in backend.
To avoid this (as fast workaround) - completely discard HTML5 date widget and use datepicker for all browsers.

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.