Dates & Times

Having an easy-to-use widget to select dates and times greatly improves user experience.

Overview

Over the course of HQ's history, we have trialed and tested several date and time pickers. We have finally settled on one library, Tempus Dominus, for its accessibility features and ease of localization. Below are examples of how to use it in various scenarios, as well as guidance for how to replace older tools on Bootstrap 3 pages with this new library.

Tempus Dominus emerged from the eonasdan-bootstrap-datetimepicker, which is still referenced in Bootstrap 3 pages as the datetimepicker jQuery plugin. Any references to datetimepicker should be replaced with tempusDominus when migrating a page from Bootstrap 3 to 5.

There are also references to the jQuery UI datepicker plugin, the timepicker plugin (from the abandoned bootstrap-timepicker project), and daterangepicker (from bootstrap-daterangepicker) in Bootstrap 3 pages. All of these should be replaced with Tempus Dominus during the migration of a page from Bootstrap 3 to 5.

Important Usage Note: Make sure to import TempusDominus from @eonasdan/tempus-dominus in javascript, as in the examples on this page.

Simple Date Picker Widget

If you are in need of a simple date picker widget that returns a format in YYYY-MM-DD, then your best option is to add the date-picker CSS class to any text input and make sure that hqwebapp/js/bootstrap5/widgets is included as part of your javascript dependencies.

The hqwebapp/js/bootstrap5/widgets module when loaded to a page will look for input fields with the date-picker CSS class, and apply the Tempus Dominus plugin to that field.

HTML
<input class="date-picker form-control"  type="text" name="a_date" />

Advanced Date Picker

If you are interested in having a date-only picker widget with extra options, you can use Tempus Dominus without the clock component. See the project's documentation for additional guidance.

HTML
<input id="js-dateonly" class="form-control" type="text" name="dateonly" />
JS
import { TempusDominus } from '@eonasdan/tempus-dominus';

new TempusDominus(
    document.getElementById('js-dateonly'),
    {
        display: {
            theme: 'light',
            components: {
                clock: false,
            },
        },
        localization: {
            format: 'L',
        },
    },
);

Date & Time

If you want to choose a date and time together, the default state of the Tempus Dominus plugin is a good starting point. See the project's documentation for additional guidance.

HTML
<input id="js-id-date-end" class="form-control" type="text" name="date_end" />
JS
import { TempusDominus } from '@eonasdan/tempus-dominus';

new TempusDominus(
    document.getElementById('js-id-date-end'),
    {
        display: {
            theme: 'light',
        },
    },
);

Time Only

If you only need to choose a time, you can use Tempus Dominus without the calendar option. See the project's documentation for additional guidance.

Bootstrap Migration Note: Older Bootstrap 3 pages will be using the old bootstrap-timepicker plugin. The example below should be a good starting point to use as a drop-in replacement for that widget.
HTML
<input id="js-id-timepicker" class="form-control" type="text" name="timepick" />
JS
import { TempusDominus } from '@eonasdan/tempus-dominus';

new TempusDominus(
    document.getElementById('js-id-timepicker'),
    {
        display: {
            theme: 'light',
            components: {
                calendar: false,
            },
        },
        localization: {
            format: 'LT',
        },
    },
);

If you want a time-picker in 24-hour format, the example below provides a good starting point.

HTML
<input id="js-id-timepicker-24" class="form-control" type="text" name="timepick24" />
JS
import { TempusDominus } from '@eonasdan/tempus-dominus';

new TempusDominus(
    document.getElementById('js-id-timepicker-24'),
    {
        display: {
            theme: 'light',
            components: {
                calendar: false,
            },
        },
        localization: {
            hourCycle: 'h23',
            format: 'H:mm',
        },
    },
);

Date Range

If you need to select a range between two dates (start and end), you can use Tempus Dominus with the dateRange option enabled. Previously, we used bootstrap-daterangepicker as the plugin of choice. The example below provides a drop-in replacement for the default usage of bootstrap-daterangepicker if you come across it in older Bootstrap 3 pages. For additional guidance, see the documentation for Tempus Dominus.

HTML
<input id="js-date-range" class="form-control" type="text" name="date_range" />
JS
import { TempusDominus } from '@eonasdan/tempus-dominus';

new TempusDominus(
    document.getElementById('js-date-range'),
    {
        dateRange: true,
        multipleDatesSeparator: " - ",
        display: {
            theme: 'light',
            components: {
                clock: false,
            },
        },
        localization: {
            format: 'L',
        },
    },
);

Using the x-datepicker Alpine directive

In addition to CSS-class-based widgets, we provide an Alpine directive, x-datepicker, that wraps Tempus Dominus with sensible defaults. This is useful when you are already using Alpine for other page behavior.

Important: to use x-datepicker on a page, make sure the JavaScript entry file for that page imports the shared directive:

import "hqwebapp/js/alpinejs/directives/datepicker";

The directive is defined here.

It supports a small JSON configuration object passed via the x-datepicker / x_datepicker attribute:

  • datetime: true — enable date and time selection, using a 24-hour clock and the format yyyy-MM-dd H:mm:ss.
  • useInputGroup: true — attach the picker to the surrounding .input-group (for use with crispy AppendedText / PrependedText). The input must be inside an .input-group.
  • container: "#selector" — render the popup inside a specific DOM container (for example, an offcanvas or modal).

Simple inline usage (date only)

For a simple date-only picker rendered directly on the page, you can use x-datepicker without any extra configuration:

Selected date:

HTML
<div
  class="card mb-3"
  x-data="{
    selectedDate: '',
  }"
>
  <div class="card-body">
    <label
      for="alpine-datepicker-simple"
      class="form-label"
    >
      Simple date picker (YYYY-MM-DD)
    </label>
    <input
      id="alpine-datepicker-simple"
      type="text"
      class="form-control w-auto"
      x-model="selectedDate"
      x-datepicker=""
      placeholder="YYYY-MM-DD"
      autocomplete="off"
    />
    <p class="mt-2 mb-0 small text-muted">
      <strong>Selected date:</strong>
      <span x-text="selectedDate || '—'"></span>
    </p>
  </div>
</div>

Date & time with x-datepicker

To capture both date and time (24-hour clock), pass datetime: true in the configuration JSON:

Selected date & time:

HTML
<div
  class="card mb-3"
  x-data="{
    selectedDateTime: '',
  }"
>
  <div class="card-body">
    <label
      for="alpine-datepicker-datetime"
      class="form-label"
    >
      Date &amp; time picker (24-hour, yyyy-MM-dd H:mm:ss)
    </label>
    <input
      id="alpine-datepicker-datetime"
      type="text"
      class="form-control w-auto"
      x-model="selectedDateTime"
      x-datepicker='{"datetime": true}'
      placeholder="YYYY-MM-DD HH:MM:SS"
      autocomplete="off"
    />
    <p class="mt-2 mb-0 small text-muted">
      <strong>Selected date &amp; time:</strong>
      <span x-text="selectedDateTime || '—'"></span>
    </p>
  </div>
</div>

Using x_datepicker in crispy forms

When working with crispy forms, you typically pass the configuration as JSON into the x_datepicker attribute on a crispy.Field. The directive will automatically handle the Tempus Dominus initialization and cleanup.

Combine this pattern with AppendedText to add an icon, as shown in the example.

Python
import json

from crispy_forms import bootstrap as twbscrispy
from crispy_forms import layout as crispy
from django import forms
from django.utils.safestring import mark_safe

from corehq.apps.hqwebapp import crispy as hqcrispy


class DatepickerAlpineForm(forms.Form):
    datepicker = forms.CharField(
        label="Date",
        required=False
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = hqcrispy.HQFormHelper()
        self.helper.form_method = 'POST'
        self.helper.form_action = '#'

        alpine_data_model = {
            'datepicker': self.fields['datepicker'].initial,
        }

        # Layout: Alpine is used to toggle visibility of the "value" field
        # based on the selected match type.
        self.helper.layout = crispy.Layout(
            crispy.Div(
                twbscrispy.AppendedText(
                    'datepicker',
                    mark_safe(  # nosec: no user input
                        '<i class="fcc fcc-fd-datetime"></i>'
                    ),
                    x_datepicker=json.dumps(
                        {
                            'datetime': True,
                            'useInputGroup': True,
                        }
                    ),
                ),
                # Bind the Alpine data model defined above to this wrapper <div>.
                x_data=json.dumps(alpine_data_model),
            ),
        )

Behavior details:

  • For date-only pickers, selecting a date is a single-click action; the directive automatically hides the picker after a successful selection.
  • For date+time pickers (datetime: true), the widget shows a close button and does not auto-hide, since picking both date and time is usually a multi-step interaction.
  • If the input value cannot be parsed, the directive clears the Tempus Dominus value and stops the error from bubbling further, preventing the widget from getting into a broken state.