By Steve Carey - 5/18/2018
Creating a good user experience when selecting a date and time in a form is much more complicated than it should be. I will show how to add a date picker, time picker and datetime picker to a Rails application using the Tempus Dominus JavaScript and CSS library for Bootstrap 4. This library is the successor to the popular bootstrap-datetimepicker library for Bootstrap 3.
The finished version of the code is available at github.com/steve981cr/rails-datetimepicker-tutorial.
rails new datetimepicker-app
# Gemfile # gem 'coffee-rails' # gem 'jbuilder' gem 'jquery-rails' gem 'bootstrap'
bundle install
Require the jquery, popper and bootstrap-sprockets.
# app/assets/javascripts/application.js //= require jquery3 //= require popper //= require bootstrap-sprockets ...
Change the extension of the application.css file to scss. Then import the css file.
# app/assets/javascripts/application.scss @import "bootstrap";
Bootstrap 4 no longer uses glyphicons but instead leverages Font Awesome. At the time of this writing Font Awesome has just released version 5.0. It's not available as a gem yet so for now we can add a link to the CDN, found at fontawesome.com/get-started, at the bottom of the application.html.erb head element. Version 5 has breaking changes from version 4 so many of the icons that the Tempus Dominus Bootstrap 4 library uses won't work without changing some options.
Now we have a working Rails app with Bootstrap, albeit with no content.
jQuery: Like Bootstrap, Tempus Dominus uses jQuery.
Font Awesome: TempusDominus Bootstrap 4 uses Font Awesome icons.
Bootstrap 4: This is not actually a dependency and it does work without Bootstrap. But it uses Bootstrap CSS classes so unless you write your own CSS for the relevant class names, it works best with Bootstrap 4.
Moment.js: Moment.js is a lightweight javascript date library for parsing, manipulating, and formatting dates. It's docs are at momentjs.com. You can access the library via a gem called momentjs-rails or though it's CDN.
There are three ways you can install Tempus Dominus:
1) Add links to their content delivery network (CDN) in the head element of your application.html.erb page. Find the links at tempusdominus.github.io/bootstrap-4 which includes cdn links to their jQuery and Moment.js dependency libraries. The Font Awesome CDN link can be found at fontawesome.com/get-started. The advantages and disadvantages of using CDNs vs. installing them locally are out of scope for this tutorial. Just be aware there are valid reasons to use either method.
2) Place the libraries directly into your app. The main reason to do this is if you want to modify them in some way.
mkdir vendor/assets; mkdir vendor/assets/javascripts; mkdir vendor/assets/stylesheets
3) Install the bootstrap4-datetime-picker-rails gem. We'll use this method.
Tempus Dominus Bootstrap 4 is wrapped in a Rails-ready gem called bootstrap4-datetime-picker-rails. Start by installing the required gems. This gem has momentjs-rails as a dependency so it will automatically be installed, but we'll add it explicitly.
# Gemfile gem 'jquery-rails' gem 'bootstrap' gem 'momentjs-rails' gem 'bootstrap4-datetime-picker-rails'
bundle install
Require the jquery, moment, and tempus-bootstrap-4.js files.
# app/assets/javascripts/application.js //= require jquery3 //= require popper //= require bootstrap-sprockets //= require moment //= require tempusdominus-bootstrap-4.js ...
Then import the css file.
# app/assets/javascripts/application.scss @import "bootstrap"; @import "tempusdominus-bootstrap-4.css"
We'll use a scaffold generator. Many Rails developers never use the scaffold generator but I usually do. You can get rid of the unnecessary stuff by disabling the jbuilder gem and adding --no-helper and --no-assets flags. We'll create one date field, one time field, and two datetime fields, as well as a title field. There is not a Rails convention for date/time column names pre se, but ThoughtBot has a Rails styleguide that recommends using _on suffixes on date columns, _time suffixes on time colums, and _at suffixes on datetime columns, which Rails also uses for it's timestamp columns. That seems reasonable so we'll follow suit.
rails generate scaffold Event title:string held_on:date start_time:time starts_at:datetime ends_at:datetime --no-helper --no-stylesheets
rails db:migrate
Make the Events index page your root directory.
# config/routes.rb root 'events#index' resources :events
Open another Terminal window and from the project root directory start/restart the server rails server
. Open up the browser to localhost:3000 and boom, we're in business. Gotta love Rails for that.
If you click the New Event button in your browser it will take you to the event form. Notice there are select boxes for day, month, year, hour, and minute in the relevant form fields. This is a Rails trick when you use the date_select, time_select, and datetime_select form helpers which you can read about and view the options at api.rubyonrails.org - DateHelper - date_select. Here are the default scaffold fields:
# app/views/events/_form.html.erb <div class="field"> <%= form.label :held_on %> <%= form.date_select :held_on %> </div> <div class="field"> <%= form.label :start_time %> <%= form.time_select :start_time %> </div> <div class="field"> <%= form.label :starts_at %> <%= form.datetime_select :starts_at %> </div>
This is not a terrible solution but the UI is not very nice even when you add CSS. But at least it's consistent in all browsers.
Go ahead and create and submit an event in the new events form so we can see what it looks like in the database. Then open the rails console and run:
Event.first
You'll see that the date field has the format of YYYY-MM-DD. The Datetime field is in the format of YYYY-MM-DD HH:MM:SS. These are the ISO 8601 international standard date and time formats which SQL and SQL databases like SQlite, Postgres and MySQL adhere to. Notice that the time field has a full date and time 2000-01-01 10:00:00 using 10am as an example. The date part is set to Jan 1, 2000 and is ignored by Rails. It looks odd but it's apparently the most efficient way to do it. If we were using a Postgresql database the values would be the same.
We are using a SQlite database which actually doesn't have column types for date, time, or datetime. Rather it uses the string data type but saves the data in the appropriate SQL format. Postgres does have date, time and datetime column types. Remember, we assigned those data types when we created the database table, and you can see them in the db/schema.rb file.
When a user submits a form, Rails recognizes commonly used date formats such as June 28, 2018, Thurs Jun 28, 2018, 2018-06-28, 28/6/2018 (but not 6/28/2018), etc. Rails is expecting a date, time or datetime format based on the data type of the field and converts the submitted data into a Ruby date, time, or datetime object. Then when it saves the object to the database it converts it to the standard SQL date, time, or datetime format. It does the opposite when retrieving data from the database.
HTML5 introduced form input types for date, time and datetime among others. Rails has corresponding form input tags of date_field, time_field and date_time field, which you can read about at api.rubyonrails.org - FormHelper. If you change the scaffold generated form fields to date_field, time_field, and datetime_field, your form fields will look like this:
# app/views/events/_form.html.erb <div class="field"> <%= form.label :held_on %> <%= form.date_field :held_on %> </div> <div class="field"> <%= form.label :start_time %> <%= form.time_field :start_time %> </div> <div class="field"> <%= form.label :starts_at %> <%= form.datetime_field :starts_at %> </div>
If you were to inspect the form's HTML with Chrome Dev Tools you would see the date input field is converted to the below HTML element, and similar for time and datetime:
<input type="date" name="event[held_on]" id="event_held_on">
Chrome, MS Edge, and iOS and Android browsers have native date and time pickers for these fields. The old IE browsers, and significantly Safari do not have native date and time pickers for these fields. Firefox has it for date and time but not datetime. In browsers without native datetime pickers these fields are treated as text fields. Without datetime picker support across all major browsers this option is simply unavailable. But the truth is the UI does not look that great in the desktop browsers anyway and is not controllable. If you changed the form to the above and refresh your browser and you'll see what I mean.
Tempus Dominus to the rescue. Lets start with the date and time fields. Change the form fields to the below. This comes straight from tempusdominus.github.io/bootstrap-4/Usage with Rails erb added in.
# app/views/events/_form.html.erb <div class="row"> <div class="col-md-6"> <div class="form-group"> <%= form.label :held_on, "Date", class: 'control-label' %> <div class="input-group date" id="datetimepicker4" data-target-input="nearest"> <%= form.text_field(:held_on, value: form.object.held_on ? form.object.held_on.strftime('%B %d, %Y') : nil, class: "form-control datetimepicker-input", data: {target:"#datetimepicker4"}) %> <div class="input-group-append" data-target="#datetimepicker4" data-toggle="datetimepicker"> <div class="input-group-text"><span class="fas fa-calendar-alt"></span></div> </div> </div> </div> </div> <div class="col-md-6"> <div class="form-group"> <%= form.label :start_time, "Start Time", class: 'control-label' %> <div class="input-group date" id="datetimepicker3" data-target-input="nearest"> <%= form.text_field(:start_time, value: form.object.start_time ? form.object.start_time.strftime('%I:%M %p') : nil, class: "form-control datetimepicker-input", data: {target:"#datetimepicker3"}) %> <div class="input-group-append" data-target="#datetimepicker3" data-toggle="datetimepicker"> <div class="input-group-text"><span class="far fa-clock"></span></div> </div> </div> </div> </div> </div>
You need to add some JavaScript to the events.js file. You can see all the available options at tempusdominus.github.io/bootstrap-4/Options/.
# app/assets/javascripts/events.js $(document).on('turbolinks:load', function() { $('#datetimepicker4').datetimepicker({ format: 'MMMM D, YYYY', minDate: Date(), maxDate: new Date(Date.now() + (365 * 24 * 60 * 60 * 1000)), icons: { up: 'fas fa-arrow-up', down: 'fas fa-arrow-down', previous: 'fas fa-chevron-left', next: 'fas fa-chevron-right', close: 'fas fa-times' }, buttons: {showClose: true } }); $('#datetimepicker3').datetimepicker({ format: 'LT', stepping: 15, icons: { up: 'fas fa-arrow-up', down: 'fas fa-arrow-down', close: 'fas fa-times' }, buttons: {showClose: true} }); });
The datetime field uses the same concepts as above.
# app/views/events/_form.html.erb <div class="row"> <div class='col-md-6'> <div class='form-group'> <%= form.label :starts_at, 'Start Date and Time', class: 'control-label' %><br> <div class="input-group date" id="datetimepicker1" data-target-input="nearest"> <%= form.text_field(:starts_at, value: form.object.starts_at ? form.object.starts_at.strftime('%B %d, %Y %I:%M %p') : nil, class: "form-control datetimepicker-input", data: {target:"#datetimepicker1"}) %> <div class="input-group-append" data-target="#datetimepicker1" data-toggle="datetimepicker"> <div class="input-group-text"><i class="fas fa-calendar-plus"></i></div> </div> </div> </div> </div> <div class='col-md-6'> <div class='form-group'> <%= form.label :ends_at, 'End Date and Time', class: 'control-label' %><br> <div class="input-group date" id="datetimepicker2" data-target-input="nearest"> <%= form.text_field(:ends_at, value: form.object.ends_at ? form.object.ends_at.strftime('%B %d, %Y %I:%M %p') : nil, class: "form-control datetimepicker-input", data: {target:"#datetimepicker2"}) %> <div class="input-group-append" data-target="#datetimepicker2" data-toggle="datetimepicker"> <div class="input-group-text"><i class="fas fa-calendar-plus"></i></div> </div> </div> </div> </div> </div>
The corresponding JavaScript is below.
# app/assets/javascripts/events.js $('#datetimepicker1').datetimepicker({ format: 'MMMM D, YYYY h:mm A', stepping: 15, minDate: Date(), maxDate: new Date(Date.now() + (365 * 24 * 60 * 60 * 1000)), sideBySide: true, icons: { up: 'fas fa-arrow-up', down: 'fas fa-arrow-down', previous: 'fas fa-chevron-left', next: 'fas fa-chevron-right', close: 'fas fa-times' }, buttons: {showClose: true } }); $('#datetimepicker2').datetimepicker({ format: 'MMMM D, YYYY h:mm A', stepping: 15, useCurrent: false, sideBySide: true, icons: { up: 'fas fa-arrow-up', down: 'fas fa-arrow-down', previous: 'fas fa-chevron-left', next: 'fas fa-chevron-right', close: 'fas fa-times' }, buttons: {showClose: true } }); $("#datetimepicker1").on("change.datetimepicker", function (e) { $('#datetimepicker2').datetimepicker('minDate', e.date); console.log(e.date); }); $("#datetimepicker2").on("change.datetimepicker", function (e) { $('#datetimepicker1').datetimepicker('maxDate', e.date); });
To change the date or time format you need to change both the JavaScript datepicker format option using the moment.js format function and the value attribute in the form using the corresponding Ruby strftime method directives.
Setting default values for time and date turned out to be tricky. You can set the datetimepicker defaultDate option or set a default value in the form field value attribute in place of nil in the above examples. However, neither of these methods worked consistently, meaning they would work then inexplicably stop working so I left it out of the tutorial. Hopefully that's just a bug that gets fixed in later versions.