Using a Bootstrap DateTime Picker in Simple Form and Rails 5.1
27 Jan 2018I’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:
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!