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.


Setup a new Rails app with Bootstrap

  • Generate an app: rails new datetimepicker-app
  • Add the bootstrap gem which loads the latest version of Bootstrap 4. The instructions for installing the Bootstrap gem are at github.com/twbs/bootstrap-rubygem.
  • Bootstrap requires jQuery so add the jquery-rails gem as well.
  • Disable the coffee-rails and jbuilder gems unless you plan on using those.
  • # 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";
    

    Font Awesome

    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.


    Tempus Dominus Datepicker Dependencies

    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.

    Tempus Dominus Installation Options

    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.

  • Create assets directories in your vendor folder: mkdir vendor/assets; mkdir vendor/assets/javascripts; mkdir vendor/assets/stylesheets
  • Download the library from tempusdominus.github.io/bootstrap-4 and unzip it.
  • Put the build/css/tempusdominus-bootstrap-4.css file into your vendor/assets/stylesheets folder.
  • Put build/js/tempusdominus-bootstrap-4.js into your vendor/assets/javascripts folder.
  • We are using the full versions instead of the minified ones since the files will be minified by the Asset Pipeline when put into production. This allows us to read and modify the code if needed.
  • You still need to add the other dependencies through gems, CDNs, or directly.
  • 3) Install the bootstrap4-datetime-picker-rails gem. We'll use this method.

    Install and set up Tempus Dominus

    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"
    

    Generate and setup an Events Resource

    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.

    Rails default _select fields

    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.

    Active Record and Datetime.

    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 input types

    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 Bootstrap 4 form fields

    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.

  • Use the text input field type.
  • The datepicker4 and datepicker3 are the id names used in the above linked documentation, but they could be any name as long as they align with the JavaScript target that we'll add next.
  • We'll set the value to the existing value if there is one using a ternary statement and to nil if there isn't. You can use the more specific @event instead of the generic form.object if you like. We need to convert the datetime formats to the ones we'll be setting in the JavaScript using the Ruby strftime method.
  • The date and time pickers appear when the user clicks on the calendar or clock icons. This gives the user the option to directly change the value in the input box or use the picker.
  • # 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>
    

    JavaScript

    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/.

  • I'm using jQuery only because that's what's used in the documentation examples, and it's a dependency.
  • We'll set it to fire on turbolinks load.
  • The default date format that Tempus Dominus uses is MM/DD/YYYY which Rails interprets as DD/MM/YYYY. So we have to change it using the format option. Tempus Dominus uses the Moment.js library for manipulating date formats. You can see the format syntax at momentjs.com/docs/#/displaying/format.
  • We are also setting the minimum date to the current date and the maximum date to one year from now using the JavaScript Date function.
  • We're using the icons option to select Font Awesome version 5 icon names. If you are using FA4 you can skip this.
  • We'll include a close button.
  • The Stepping option for the time picker specifies how many minutes each up/down arrow skips.
  • # 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}  
      });
    });
    

    DateTime Field

    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.

  • We'll link the starts_at and ends_at fields so that a user can't enter an end date before the start date or a start date after the end date.
  • function (e) is triggered by a change event on the datepicker value.
  • The sideBySide option makes the form much more readable than the default in my opinion.

  • DateTime JavaScript

    # 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);
    });
    

    Wrap up

    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.