Matt
DeveloperWe’ve recently been working on a couple of projects that involve allowing users to select dates. It has taken us on a quest to find the best way to enable excellent experience to mobile devices and edge browsers, but also older browsers. Graceful degradation, as it’s known, is a common theme that runs through the heart of what we do.
The protagonist of our story is the HTML5 date input. Before it arrived, the only way to display a calendar was using a JavaScript library of some sort. Most JS libraries provide good function, but are a challenge on mobile devices. The date input uses the native date picker for devices, offering a much better experience to mobile users. Here’s an example:
Android native data picker
jQuery UI date picker
Browser support
There are plenty of browsers that don’t support date inputs, including Firefox and desktop versions of Safari. So for these browsers, we use the tried and tested alternative – a text field with a JavaScript date picker. This is quite simple, and can be done using this code snippet:
|
Browsers that don’t understand “date” as an input type will fall back to type “text”, so we can use this to figure out if our current browser supports date fields or not, and act accordingly.
Styling challenges
There are a few landmines to tread gingerly around with this approach. Firstly, date inputs don’t support placeholders, so we have to absolutely position the desired placeholder text over the element. This is compounded by date fields on iOS having a height of 0 when not given a value, unlike other input types, as shown below:
iOS date input without value
iOS date with value
A successful workaround was to replace a text field (with a placeholder, and consistent behaviour) with a date field (where supported) on focus, which involved swapping the fields and then focussing on the new date field. It’s not possible to trigger the iOS keyboard through Javascript, so while the date field was active, there was no date picker visible for the user.
The only consistent way to handle this was to set a height for each input which included padding and line height, and then positioning a label over the input to act as a placeholder. This label is then removed if the current browser doesn’t support date fields.
Validation/Submission challenges
One hurdle we faced with this approach is differing date formats. With date fields, the format of the date submitted is yyyy-mm-dd, but it’s viewed as dd/mm/yyyy, which is much clearer, at least to people who are used to UK date formats. However, with text fields, the format viewed is the format submitted, so we have a situation where the same form can send the same date with multiple formats. As dd/mm/yyyy is the most common way of viewing dates (sorry if you’re reading this from the USA), we set up the Javascript date picker to use that format, and added it to the placeholder for anyone wanting to type the date. On the server side we converted any dd-mm-yyyy formatting to match the yyyy-mm-dd formatting within the form submission handler to ensure consistent date handling. While this means that a mm/dd/yyyy format date could make it through, the placeholder makes it clear to the user what the expected format is beforehand.
Conclusion
The JavaScript date picker we chose was an off the shelf jQuery UI element; it’s easy to use, provides a great experience, and as a bonus it comes packaged with Drupal, which is often our CMS of choice. You can find it here (https://jqueryui.com/datepicker/).
Here’s the final code we ended up with:
<div class="form-item-date"> <input id="date_field" type="date" placeholder="Date (dd/mm/yyyy)" name="date_field" data-covered-value="Date (dd/mm/yyyy)" /> <label class="date visible" for="date_field">Date (dd/mm/yyyy)"label> <div class="tooltip hidden">div> div>
if (!isDateInputSupported()) { $('input[type="date"]') .datepicker({ //settings here }) .val('').siblings('label').remove(); } else { $(document).on('blur', 'input[type="date"]', function () { var input = $(this); var value = input.val(); var coveredValue = input.attr('data-covered-value'); if (value == coveredValue) { input.siblings('label').addClass('visible'); } else { input.siblings('label').removeClass('visible'); } }); }
.form-item-date label { position: absolute; top: $form-input-border-width; left: $form-input-border-width; padding: $form-input-padding - $form-input-border-width; display: none; &.visible { display: block; } }