Designer Collab for Date Ranges

When searching for hotels, I often specify check in and check out dates with two clicks on the same mini-calendar. The new date picker’s dayFormatter function let me achieve the same user experience in an APEX app, with some CSS help from my designer colleague Jeff Langlais. I got the basic functionality working; then Jeff updated the stylesheet with his CSS magic to make it sing. Finally, a tip from colleague Stefan Dobre about JavaScript classes unlocked how my Java programming experience could be an asset while learning this new language. It inspired me to refactor my code to make using a date range picker very simple in future APEX apps I build.

Overview of the Strategy

My strategy involved using an inline date picker page item as the “surface” the user interacts with to set and see the date range. The date picker page item works together with two other date fields that capture the actual start and end dates. Depending on the application, it might be desirable for the user to see the start and end dates in an alternative display format. However, in my sample application I decided to set them to be Hidden page items. As shown in the figure below, the dayFormatter function associated with the date range picker, considers the values of the hidden Check In and Check Out dates to decide how to format the days in the date range between start date and end date. It also decides the appropriate tooltip to show the user based on these values.

The dayFormatter function on the date range picker styles the range of days and tooltips

CSS Style Classes Involved

The date picker used as the date range picker is tagged with the CSS class date-range-picker. This allows targeting the CSS style rules so that they only affect date range picker page items, without disturbing the styling of other date pickers used in the application. Next, I identified three different styles required to render the “stripe with rounded ends” look I imagined in my head. As shown below, the CSS class dateRangeStart represents the start date of the range, the dateRangeEnd for the end date, and thedateRangeMiddle class for those days in between. I wrote the dayFormatter function to return null for the CSS class property for any days in the “mini-month” that were before or after the date range. For those days within the range, it returns one of these three CSS class names depending on whether the day being formatted is the beginning, middle, or end of the range. The apex.date namespace functions parse(), isSame(), isBefore(), isAfter(), and isBetween() came in handy for writing the date-related logic in the dayFormatter function and the date change handler function described later.

Three CSS class names involved in formatting a date range as a “stripe with rounded ends”

After getting the initial dayFormatter logic working, I realized that some use cases might need a date range that starts and ends on the same day. For example, this would be the case for the dates of a single-day event. To allow for a more visually pleasing single-day date range, I decided a fourth CSS class dateRangeSingleDay was needed to achieve the appropriate “pill” shape the user would expect a one-day event to have. I adjusted the dayFormatter function to return this new class name if start date and end date were the same.

Additional CSS class to handle single-day events as a special case

Handling Date Range Input & Reset

When the user clicks on a day in the “mini-month” calendar of the date range picker, the Change event will fire for that page item. I wrote the logic of the change handler to work as follows:

  • If start date is not set, then set it to the clicked-on date
  • Otherwise, if the clicked day is after the start date, then set the end date to the clicked-on date
  • If the clicked day is before the current start date, then set start date to the clicked-on date
  • Finally, set the date range picker to the value of the start date again, and
  • Refresh the date picker item to re-evaluate the dayFormatter in the process

When the user clicks on the button to reset the date range picker, the Click event will fire for that button. I wrote the logic of the click handler to:

  • Set the value of the start date to null
  • Set the value of the end date to null
  • Set the value of the date picker to null
  • Refresh the date picker item to re-evaluate the dayFormatter in the process

Following good practice, I had written the bulk of my JavaScript logic in a shared application file dateRangePicker.js It defined a dateRangePicker JavaScript object with three functions:

  • assignDayFormatter() called from the Page Load dynamic action event
  • onChanged() called from the Change dynamic action event on the date picker
  • reset() called from the Click dynamic action event of the reset button

In the page containing the date range picker page item, the hidden start date item, the hidden end date item, and the reset button, I setup dynamic actions to invoke the helper methods like this:

Initial implementation using dynamic actions to call JavaScript functions in a helper object

Abstracting Interesting Bits into Metadata

After initially hard-coding the values of the date range picker item, the start date and end date page items, I next tried to add a second date range picker on the same page and rework my code to accept the interesting information as parameters that made the two instances unique. Instead of passing in 10 separate parameters, I decided to pass all the info required as a single parameter in a structured JavaScript object. An example of this parameter object appears below. It captures the names of the page items involved in a single date range picker:

{
  picker: {
    name: "P2_CHECKIN_CHECKOUT_PICKER",
    format: "DD-MON-YYYY",
    allowSingleDay: false 
  },
  start: {
    name: "P2_CHECKIN",  
    label: "Check In"
  },
  end: {
    name: "P2_CHECKOUT",
   label:"Check Out"
  }
}

By passing the appropriate JavaScript object to each of the helper methods, I was able to rework the code to easily support date range pickers on any page in my application and even multiple ones on the same page.

Working in Parallel with a Designer

Since I’m not a CSS expert, I started with the simplest possible dateRangePicker.css file containing the style classes for the four states the date range picker needed, setting a different font color and italic style for the different date range classes. I used the Chrome browser tools Inspect Element… feature to study what elements and classes would need to be selected by these basic CSS rules. In words, for example, the first rule below selects a <td> element having the CSS class dateRangeStart wherever it’s nested inside a containing element with class a-DatePicker-calendar (the “mini-month”) where that is nested inside a containing <a-date-picker> element having the class date-range-picker:

a-date-picker.date-range-picker .a-DatePicker-calendar td.dateRangeStart
{
    color: yellow;
    font-style: italic;
}

a-date-picker.date-range-picker .a-DatePicker-calendar td.dateRangeMiddle
{
    color: darkmagenta;
    font-style: italic;   
}

a-date-picker.date-range-picker .a-DatePicker-calendar td.dateRangeEnd
{
    color: green;
    font-style: italic;   
}

a-date-picker.date-range-picker .a-DatePicker-calendar td.dateRangeSingleDay
{
    color: blue;
    font-style: italic;   
}

The effect wasn’t exactly what I had predicted, but as shown below I could see after selecting February 13th as the start date and 16th as the end date, that the dates in the date range were showing with the indicated colors and in italic. As you can see below, there was something about the date picker’s default styling of the current date (which, recall, coincides with the start date of the date range) that was overriding my styles. That current date was colored with a blue circle. However, I could see that the font style was italic, so I knew my style rule was correctly selecting that dateRangeStart day. I also noticed that today’s date was showing in the calendar with a different colored circle.

Initial attempt at CSS stylesheet to style the date range days differently

Rather than trying to become a CSS expert, I decided to pass these requests along to Jeff the designer so that he could incorporate solutions into the final CSS stylesheet he gave me back. In addition to the “stripe with rounded ends” look for the date range, I also asked him to explore hiding the current day indicator. You can explore the sample application’s dateRangePicker.css static application file to see the CSS magic that Jeff worked to make the date range picker look great. This was a concrete example of how an APEX developer with only the most basic CSS skills could easily collaborate with a highly-skilled CSS web designer to produce a nice-looking result.

Leaning Into JavaScript Classes

As a final step, I asked my colleague Stefan Dobre to review my JavaScript newbie code to suggest any improvements. He recommended I explore further encapsulating the logic of the date range picker into a self-contained DateRangePicker class. Its constructor could accept the JavaScript object describing the combination of picker, start date, and end date page items, and then internalize the details of:

  • Setting the date-range-picker CSS class on the picker page item
  • Assigning the dayFormatter function to the picker page item
  • Adding an event listener to the picker’s Change event to call onChanged()

By expanding the metadata captured by the constructor to also include the static id of the reset button, the DateRangePicker class could also internalize adding an event listener to the button’s Click event to call reset().

Since I’d programmed for many years in Java in my previous roles at Oracle, the idea of using a class felt second nature. But as a JavaScript neophyte, the idea never crossed my mind. So Stefan’s suggestion unlocked a positive path in my Java brain that will hopefully make future JavaScript development more familiar. You can see the full code for the DateRangePicker JavaScript class in the sample application’s dateRangePicker.js static application file, but the skeleton of the implementation looks like this. Its constructor accepts the JavaScript object describing the configuration details of the date range picker page items, sets the date-range-picker CSS style class on the picker page item, assigns the initial value to the date picker from the start date, assigns a dayFormatter function to the picker, and wires up the change and click event listeners to run the appropriate code to handle those actions.

window.DateRangePicker = class DateRangePicker {
    // Construct the DateRangePicker accepting object that describes
    // the picker, start date, end date names, and reset button id
    constructor(pConfig) {
        this.#config = pConfig;
        // Assign the date-range-picker CSS class to the picker
        this.#pickerItem().element.addClass("date-range-picker");
        // Assign the initial value of the picker from the start date
        this.#assignInitialValueFromStartDate();
        // Assign the dayFormatter funtion
        this.#assignDayFormatter();
        // Wire up the change event on the picker to call onChanged()
        this.#pickerItem().element.on("change", () => {
            this.#onChanged();
        });
        // Wire up the click event on the reset button to call reset()
        document.getElementById(this.#resetId()).addEventListener(
            "click", () => {
            this.#reset();
        })
    }

    // Private fields ==================================================
    #config;

    // Private methods =================================================
    #assignDayFormatter() {...}
    #onChanged() {...}  
    #reset(){...}
}

With this class in place, you can see how it’s used in page 2 and page 4 of the sample app. Their respective page load JavaScript code contains two simple calls like the following to construct two DateRangePicker class instances, passing the interesting info into each’s constructor.

// Example from Page 4 in the sample app's Page Load JavaScript
// Setup config for Event Start/End Date Range Picker
// Allows a single day to be both start and end
window.eventStartEndDateRangePicker = new DateRangePicker({
    picker: {
      name: "P4_EVENT_DATE_RANGE",
      format: "DD-MON-YYYY",
      allowSingleDay: true 
    },
    start: {
       name: "P4_EVENT_STARTS",  
       label: "Event Start"
    },
    end: {
        name: "P4_EVENT_ENDS",
        label:"Event Start"
    },
    reset: {
        id:"Reset_Event_Dates"
    }
});

With all the logic encapsulated in the JavaScript class, there is no setup left in the page other than making sure the picker, start date, and end date page items have their Value Protected property set to false and that they all use the same format mask. This resulted in a page you can experiment with in the sample to create or edit the details of an Event. Each Event in the sample app has a start and end date (which can be the same day) as well as a default check in and check out day for event attendees (which must be at least two different days).

Two date range pickers in action in a sample app editing Event details

Get the Sample App

You can download the APEX 22.2 sample application by clicking here. Thanks again to designer Jeff Langlais for helping me with the CSS styles to deliver the visual idea I had in mind, and to Stefan Dobre for teaching me about JavaScript classes to simplify how to uptake the date range picker functionality in future APEX apps I will build.