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

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