Date Picker for Rails
It’s nice to take advantage of the new HTML5 input types like <input type='date'>
. On the surface it seems so simple, but the devil is in the details.
Requirements
Add a date (no time) field to a rails form and have it:
- Use native HTML5 date controls where available
- Backfill HTML5 with a bootstrap-compatible datepicker when not available
- Enter and display according to locale (I18n)
Toolset
- bootstrap_form - a gem for rendering rails form tags in the bootstrap way
- bootstrap-datepicker - the most upvoted and flexible bootstrap datepicker
Getting Started
First, let’s get the HTML5 type=[date]
attribute working with the rails form helper:
<%= f.date_field :opened_on %>
At the moment, date_field
work’s well in iOS 9 and OSX Chrome 46.0. Request controller#new
and you get a native datepicker that shows the current month on a blank input control. Nice.
Backfilling
However, there is no native control support for Safari or Firefox so lets add a little Coffeescript to backfill with bootstrap-datepicker
:
unless Modernizr.inputtypes.date
$("input[type='date']").datepicker()
Request controller#new
in Safari and you get a bootstrap-styled datepicker that shows the current month on a blank input. Meanwhile, Chrome is still using the native control thanks to modernizr
’s ability to backfill incompatible browsers.
Issues
Out of the box, selecting a date from bootstrap-datepicker
fills the input control like 11/07/2015
. Press “Save” and the Rails params
hash look like this:
{"violation"=>{"name"=>"Test", "opened_on"=>"11/07/2015"}, "id"=>"4"}
Go to the console for a little “smoke testing” and:
pry(main)> Violation.find(4).opened_on
=> Sat, 11 Jul 2015
Bummer. Looks like we have a difference of opinion with regard to date formats – bootstrap-datepicker
emits mm/dd/yyyy
but date_field
expects yyyy-mm-dd
.
Under the Hood
Let’s bundle open actionview
, search for def date_field
and see what’s going on:
actionview-4.2.0/lib/action_view/helpers/form_helper.rb is where date_field
is declared:
def date_field(object_name, method, options = {})
Tags::DateField.new(object_name, method, self, options).render
end
actionview-4.2.0/lib/action_view/helpers/tags/date_field.rb is where Tags::DateField
is declared
module ActionView
module Helpers
module Tags # :nodoc:
class DateField < DatetimeField # :nodoc:
private
def format_date(value)
value.try(:strftime, "%Y-%m-%d")
end
end
end
end
end
Notice that format_date
doesn’t use any of Rail’s I18n
magic. Looking further, we find the parent DatetimeField
class implements the reverse transformation with datetime_value
:
private
def format_date(value)
value.try(:strftime, "%Y-%m-%dT%T.%L%z")
end
def datetime_value(value)
if value.is_a? String
DateTime.parse(value) rescue nil
else
value
end
end
This implies that an HTML5 date control expects the browser to deal with the localization. As it turns out HTML5 date controls expect values that are ISO 8601 / RFC 3339 values.
Making it Work
Now all we need is a little “glue” code to get bootstrap-datepicker
to output dates that work with Rails. My solution was to add a little Coffeescript to create a separate hidden
input that mimics the HTML created by Rails:
App.Forms.backfillDatePicker = ->
unless Modernizr.inputtypes.date
$("input[type='date']").each (i,e)=>
$e = $(e)
# create a hidden field with the name and id of the input[type=date]
$hidden = $('<input type="hidden">')
.attr('name', $e.attr('name'))
.attr('id', $e.attr('id'))
.val($e.val())
# modify the input[type=date] field with different attributes
$e.data('hidden-id', $e.attr('id')) # stash the id of the hidden field
.attr('name', "")
.attr('id', "")
.val(@formatDateToPicker($e.val())) # transform the date
.after($hidden) # insert the hidden field
# attach the picker
$e.datepicker()
# update the hidden field when there is an edit
$e.on 'change', (e)=>
$e = $(e.target)
$v = $('#' + $e.data('hidden-id'))
$v.val(@formatDateFromPicker($e.val()))
Notice there are a couple of custom Date formatting methods:
# dateStr is what Rails emits "yyyy-mm-dd"
App.Forms.formatDateToPicker = (dateStr)->
return '' if dateStr == ''
parts = dateStr.split('-')
return 'Invalid ISO date' unless parts.length == 3
"#{parts[1]}/#{parts[2]}/#{parts[0]}"
# dateStr is what the datepicker emits "mm/dd/yyyy"
App.Forms.formatDateFromPicker = (dateStr)->
return '' if dateStr == ''
parts = dateStr.split('/')
return 'Invalid picker date' unless parts.length == 3
"#{parts[2]}-#{parts[0]}-#{parts[1]}"
The reason to roll my own formatters is that the javascript Date
object contains timezone information. When you’re working with Dates that don’t include Times, it’s easier to just ignore the timezone.
Note: App
is a custom, global javascript object that I use to “namespace” and “modularize” all the “app-specific” javascript code to avoid naming collisions with other plug-ins.
Translation
bootstrap-datepicker
comes with translation files and it’s as easy as this to set the locale:
# attach the picker
$e.datepicker
orientation: 'bottom left'
language: App.config.locale
In this example, App.config.locale
is es
and derived from the url: example.com/es/controller/method
. The native HTML5 date controls are up to the browser to localize based on user preferences.
Left for Later
bootstrap-datepicker
should honor the date formats of other locales like en-GB
. This exercise is left for later and I’m hoping it can be accomplished with the i18n-js gem.
References
- http://stackoverflow.com/questions/18020950/how-to-make-input-type-date-supported-on-all-browsers-any-alternatives
- http://html5doctor.com/using-modernizr-to-detect-html5-features-and-provide-fallbacks/
- http://www.sitepoint.com/finding-date-picker-input-solution-bootstrap/
- http://tjvantoll.com/2012/06/30/creating-a-native-html5-datepicker-with-a-fallback-to-jquery-ui/