edruder.com i write about anything, or nothing

Using a Bootstrap DateTime Picker in Simple Form and Rails 5.1

I’m using Bootstrap to make the pages of my Rails app look halfway decent–I’m a programmer who’s creatively challenged. (I can already feel the scorn from my designer friends, but hey–I actually like how Bootstrap looks!) I’m also using Simple Form to clean up the HTML forms in my app.

In one of my forms, the user needs to choose a date & time, and the default way to enter a date & time in Rails is horrendously ugly, even to my soulless, dead eyes. A little Googling for ‘bootstrap datetime picker rails’ landed me on the bootstrap3-datetimepicker-rails gem page. This gem just wraps the bootstrap-datetime-picker, whose minimal setup looked perfectly suitable to me. Piece of cake, I thought!

It was straightforward to install and configure the gem using the instructions on its home page. The hard part was trying to figure out how to use the picker in my Simple Form form.

The bootstrap-datetime-picker home page describes the markup that renders the picker. From the top of that page, this is what I wanted Simple Form to output:

<div class="form-group">
  <div class="input-group date" id="datetimepicker1">
    <input type="text" class="form-control" />
    <span class="input-group-addon">
      <span class="glyphicon glyphicon-calendar"></span>
    </span>
  </div>
</div>

(There is some markup wrapping this, but this is the part that is important.)

I read the (very complete!) Simple Form documentation, but I didn’t see how to specify a stock Simple Form field that would result in something resembling the markup above. I read several of the Simple Form wiki pages about building custom input components, and I decided that that’s what I needed to create to get the markup that would render the datetime picker properly.

Unfortunately, that documentation didn’t get me to the point where I knew how to write the custom component, but with that and several other examples I found online, my mental picture of what I needed to do became clearer. I didn’t find a good soup-to-nuts description of how to build a custom input component, starting with the markup you want, but I figured I could dive in and figure it out.

A Solution

First, the Markup

I have the markup I’m aiming for (above). Here’s the Simple Form field that I think should create it (published_at is the Active Record datetime field to be created/edited):

<%= f.input :published_at, as: :date_time_picker %>

I started with a shell of a custom input component, from the Custom inputs section of the Simple Form documentation. If you put the file in the app/inputs directory (which is Simple Form-specific–you’ll probably need to create it) and restart your server, Simple Form will pick it up:

# app/inputs/date_time_picker_input.rb

class DateTimePickerInput < SimpleForm::Inputs::Base
  def input(wrapper_options)
    merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)

    @builder.text_field(attribute_name, merged_input_options)
  end
end

Looking at the markup I’m shooting for, I’m going to need a text input, and I’m guessing that’s what @builder.text_field is going to create, so let’s just try it out! When I restart my server and render a form using a this custom input component (using exactly the f.input line, above), this is the HTML that’s rendered:

<div class="form-group date_time_picker required content_published_at">
  <label class="control-label date_time_picker required" for="content_published_at">
    Published at
  </label>
  <input class="form-control date_time_picker required"
         type="text"
         value="2018-01-01 23:38:00 UTC"
         name="content[published_at]"
         id="content_published_at" />
</div>

Not a bad start! It looks like Simple Form will generate a label by default, and I don’t want one. Poking around in the documentation and examples, I see that there is a label method that I can define to customize the label tag of this component. What if I define one that returns nothing?

# app/inputs/date_time_picker_input.rb

class DateTimePickerInput < SimpleForm::Inputs::Base
  def input(wrapper_options)
    merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)

    @builder.text_field(attribute_name, merged_input_options)
  end

  def label(_); end
end

And its output:

<div class="form-group date_time_picker required content_published_at">
  <input class="form-control date_time_picker required"
         type="text"
         value="2018-01-01 23:38:00 UTC"
         name="content[published_at]"
         id="content_published_at" />
</div>

Bingo! (The _ parameter is there because the label method requires one parameter, but we aren’t using it and don’t care about it. This is a Ruby convention that’s pretty handy.) Moving on…

I see that the outer div with a class of form-group is already there without me having to do anything. We can ignore the other classes on that outer div, but you can see the name of our component there, the model name/field name (content_published_at) that Rails wants, etc.

Comparing the HTML to the exemplar, the input needs to be wrapped in a div. From some of the examples I saw in the documentation and elsewhere, I can wrap a div around the input using template.content_tag(:div). Let’s give that a shot in the input method:

# app/inputs/date_time_picker_input.rb

def input(wrapper_options)
  merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)

  template.content_tag(:div, class: 'input-group date', id: 'datetimepicker1') do
    @builder.text_field(attribute_name, merged_input_options)
  end
end

Here’s the HTML that’s generated:

<div class="form-group date_time_picker required content_published_at">
  <div class="input-group date" id="datetimepicker1">
    <input class="form-control date_time_picker required"
           type="text"
           value="2018-01-01 23:38:00 UTC"
           name="content[published_at]"
           id="content_published_at" />
  </div>
</div>

Closer!

Thinking about this for a second, the custom component should not be assigning an id to that div–this is a general purpose component. We’ll figure out how to set the id later, if we need it–for now, I’m going to delete the id.

Now I need to create the span that follows the input. And that span needs to contain a nested span inside of it. Hmm–that’s a pattern we’ve seen before. The nested span can be created like this:

template.content_tag(:span, class: 'input-group-addon') do
  template.content_tag(:span, '', class: 'glyphicon glyphicon-calendar')
end

Adding that into the input method, we have:

# app/inputs/date_time_picker_input.rb

def input(wrapper_options)
  merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)

  template.content_tag(:div, class: 'input-group date') do
    @builder.text_field(attribute_name, merged_input_options)
    template.content_tag(:span, class: 'input-group-addon') do
      template.content_tag(:span, '', class: 'glyphicon glyphicon-calendar')
    end
  end
end

When we run this, we get this HTML:

<div class="form-group date_time_picker required content_published_at">
  <div class="input-group date">
    <span class="input-group-addon">
      <span class="glyphicon glyphicon-calendar"></span>
    </span>
  </div>
</div>

Wait! What happened to the input?

Well, the way these tag builder blocks work is that the “return value” of the block is what’s wrapped by the tag. In this case, the return value of the block is whatever the template.content_tag(:span) builder returns, which is the nested spans that we see. The output of the @builder.text_field() call fell on the floor and wasn’t used at all.

The return value of the template.content_tag(:div, class: 'input-group date') builder block needs to be contain both the input and the span, in that order. The input and the span builders just return strings, so we can simply concatenate them! Here’s a simple way to do that:

# app/inputs/date_time_picker_input.rb

def input(wrapper_options)
  merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)

  template.content_tag(:div, class: 'input-group date') do
    input = @builder.text_field(attribute_name, merged_input_options)
    span = template.content_tag(:span, class: 'input-group-addon') do
      template.content_tag(:span, '', class: 'glyphicon glyphicon-calendar')
    end
    "#{input} #{span}".html_safe
  end
end

And the HTML:

<div class="form-group date_time_picker required content_published_at">
  <div class="input-group date">
    <input class="form-control date_time_picker required"
           type="text"
           value="2018-01-01 23:38:00 UTC"
           name="content[published_at]"
           id="content_published_at" />
    <span class="input-group-addon">
      <span class="glyphicon glyphicon-calendar"></span>
    </span>
  </div>
</div>

Awesome!

Because we haven’t hooked up the JavaScript part of this component, it doesn’t do anything, yet. Let’s do that now.

The JavaScript

The bootstrap-datetime-picker home page describes the simple JavaScript that hooks up and gives life to the picker. From near the top of that page, this is all that it takes:

$(function () {
  $('#datetimepicker1').datetimepicker();
});

This JavaScript uses jQuery to call the datetimepicker function (provided by bootstrap-datetime-picker, which is already installed) on the DOM element whose id is datetimepicker1. Whoops–I deleted that!

Our component doesn’t have an id, but does it need one? Let’s say we had a form that had two or more of these pickers in it–we’d want all of them to be turned on. The HTML of our component has plenty of identifying classes attached to various elements of it–we can find all of the picker components in the DOM using those classes.

A slight change to the JavaScript will enable all of the pickers in the DOM:

$(function () {
  $('.date_time_picker > .input-group.date').datetimepicker();
});

This generic JavaScript is written this way so that the code inside of the anonymous function gets called when the page has finished loading. (Exactly how that works is beyond the scope of this post, but a little Googling around should find tons of articles about it.) Rails 5.1 has a different way to run JavaScript when the page is loaded (the details of which are also beyond the scope of this post):

$(document).on("turbolinks:load", function() {
  $('.date_time_picker > .input-group.date').datetimepicker();
});

Here’s the equivalent CoffeeScript (which is the default flavor of JavaScript as of Rails 5.1):

$(document).on "turbolinks:load", ->
  $('.date_time_picker > .input-group.date').datetimepicker()

With that code added to your page’s JavaScript (I’m not going to explain how to do that, but there’s lots of Rails documentation and tutorials online), the picker component works! If you’ve been following along, hopefully you see something like this:

picker

When I started using this picker, it kind of worked, but had some problems.

The first was that when it was used to edit an existing value, the picker would start out blank, instead of populated with the value. When I watched closely, I saw that the right value flickered quickly in the picker, but was erased almost immediately. For some reason, the value does populate the picker initially, but when the datetimepicker() function is called, it gets erased.

My quick fix to that problem is to modify the JavaScript to grab the value of the picker, call the initializer, then restore the value. Seems to work. (There’s probably a better way to do this. Let me know!)

The next problem I saw was that the date that was saved in the Active Record datetime field was a little funky–sometimes it was just a little wrong, sometimes it wasn’t stored at all. Turns out that the format of the date/time string has to match the format that the Rails database expects it. It will try to parse the string using its format, and sometimes it will be just a little wrong, and other times it would be an illegal date, and nothing gets stored!

The way I chose to fix this was to change the format of the string that’s returned by the bootstrap-datetime-picker to match the format that my database (MySQL) expects. (I think the format that MySQL expects–YYYY-MM-DD HH:mm:ss–is fairly common, if not ubiquitous, among databases.)

Here’s the final JavaScript that works well for me:

$(document).on("turbolinks:load", function() {
  const $pickerInput = $('.date_time_picker input.date_time_picker');
  const initialValue = $pickerInput.val();
  $('.date_time_picker > .input-group.date').datetimepicker({ format: 'YYYY-MM-DD HH:mm:ss' });
  return $pickerInput.val(initialValue);
});

Here’s the equivalent CoffeeScript:

$(document).on "turbolinks:load", ->
  $pickerInput = $('.date_time_picker input.date_time_picker')
  initialValue = $pickerInput.val()
  $('.date_time_picker > .input-group.date').datetimepicker({ format: 'YYYY-MM-DD HH:mm:ss' })
  $pickerInput.val(initialValue)

Please leave a note if you have questions, answers, suggestions, or a better solution!