Share

Building a DateTime Picker in Ember.js (Part 1)

I recently attended EmberConf 2017 and there was an interesting talk on
Higher Order Components by Miguel Camba (I highly recommend watching if you haven’t
already). Coincidentally I was also in the process of building a datetime picker for PagerDuty, so this talk was very
relevant to the work that I was doing at the time.

Miguel described a pattern of having a container component and having subcomponents nested within. This
pattern is useful when having a complex UI component that may have multiple components working together.

This concept and the DDAU (Data Down Actions Up) paradigm had a big influence on the design. I’ll illustrate by going into
the component architecture of the datetime picker that I implemented with a colleague. I named it datetime-selector so
moving forward, I’ll refer to it by that name.

datetime-selector

The screenshot above is the finished product. It’s essentially a text input such that when clicked, it shows the datetime
picker controls below. In the dropdown content, there’s a calendar for picking a date, number controls to select a time,
buttons for am/pm & 12/24 hour rendering, and a select input for timezone. When changing any of properties (like hour,
minute, second, timezone, etc), the change will reflect in the textfield.

Components Implemented

Thinking in terms of components, this is about 4-6 components, where they all need to be in sync with whatever the
currently selected date is. This is the way I constructed it:

drop-down

This component is a thin wrapper around the ember-rl-dropdown component. It handles the showing/hiding of the datetime picker controls.

datetime-selector

This is the container component that holds everything together. It is home to the date, am/pm, 12/24, and timezone
properties, which end up getting passed into the relevant child components. It also has the actions to change them which
also get passed into the relevant child components.

power-calendar

ember-power-calendar is an ember addon for the date picker (just
renders a calendar and has logic for date selection).

time-selector

This is a container component for all of the time related controls.

time-selector

number-control

This component simply increments/decrements a number using the up/down buttons. You can also provide min/max values
and a boolean argument prefixZero to render a single digit number prefixed with a 0 (like the screenshot below).

number-control

timezone-selector

This is a select input with all of the timezones.

pd-button

This is a simple button component that is used for the am/pm, 12/24, up/down, and next/previous month buttons.

Putting the Puzzle Pieces Together

While I won’t dive into the specific implementation of each component, what I want to focus on is the interaction among
the components. Let’s start at highest level component datetime-selector.

The datetime-selector component is the container component (aka the glue code) that also is the one source of truth
of the date & time. The component essentially looks something like this:

// computed properties of datetime-selector
export default Ember.Component.extend({
  classNames: ['datetime-selector'],
  is24hourTime: true,
  date: computed(function() {
    return moment();
  }),
  isAM: computed('date', function() {
    return this.get('date').hour() < 12;
  }),
  timezone: computed('date', function() {
    return this.get('date').tz();
  }),
  // Format the date as something like `Jun 12, 2017 at 4:37:54 PM - PST`
  renderedDate: computed('date', 'is24hourTime', function() {
    let hourMomentToken = this.get('is24hourTime') ? 'HH' : 'h';
    let meridiemMomentToken = this.get('is24hourTime') ? '' : 'A ';
    let date = this.get('date');
    return date.format(`MMM D, YYYY \\at ${hourMomentToken}:mm:ss ${meridiemMomentToken} - z`);
  }),

As seen above, the datetime-selector has all of the values that will eventually be fed into the child components
(i.e. power-calendar, time-selector, time-zone-select).

The template for datetime-selector looks something like this:

{{#drop-down as |dropdownExpanded|}}
  {{#drop-down/toggle}}
    
  {{/drop-down/toggle}}

  {{#drop-down/content dropdownExpanded=dropdownExpanded}}
    
    {{#power-calendar selected=date
                      onSelect=(action 'changeDate')
                      onCenterChange=(action (mut date) value='moment')
                      center=date as |calendar|}}
      {{calendar.nav}}
      {{calendar.days}}
      {{time-selector is24hourTime=is24hourTime
                      isAM=isAM
                      date=date
                      onTimeChange=(action (mut date))
                      onMeridiemChange=(action (mut isAM))
                      on24hourTimeChange=(action 'changeHourFormat')
      }}
      {{time-zone-select value=(readonly timezone)
                         onTimezoneChange=(action 'changeTimezone')
      }}
    {{/power-calendar}}
  {{/drop-down/content}}
{{/drop-down}}

Notice how the datetime-selector component renders each subcomponent passing in the relevant computed properties
and actions to call when changing the values. This pattern adheres to the DDAU (Data Down Actions Up) paradigm. Another
thing to notice is the use of the mut template helper.

What is mut?

mut is a helper to specify that a child
component can update the value passed to it. In most cases, I rarely use it on its own, but use it in tandem with action.
Let's look at this example:

{{time-selector is24hourTime=is24hourTime
                isAM=isAM
                date=date
                onTimeChange=(action (mut date))
                onMeridiemChange=(action (mut isAM))
                on24hourTimeChange=(action 'changeHourFormat')
}}

The onTimeChange=(action (mut date)) line is essentially defining an onTimeChange action that can be called from
within the time-selector component. So let's say we had a changeHour action in time-selector that gets called when
an hour changes. From there you could do something like this:

// snippet of time-selector
export default Ember.Component.extend({
  // ...
  actions: {
    changeHour(value) {
      this.get('onTimeChange')(moment(this.get('date')).hour(value));
    }
  }
});

We're basically calling the action that was passed in with the new date object that we want set upstream.
moment(this.get('date')).hour(value) will return a new moment object with the new hour in place. So when the hour
changes, it will change the date (in datetime-selector) to reflect the new hour.

Passing action (mut someProperty) as a property into a subcomponent is a pattern I used a lot. It is basically a
shorthand way to define an action inline. The same could be accomplished if I had an action like this:

export default Ember.Component.extend({
  // ...
  actions: {
    setSomeProperty(value) {
      this.set('someProperty', value);
    }
  }
}

The finished product looks something like this

datetime-selector

We're considering open sourcing this component as an ember addon. Let us know in the comments if this is something you'd
like as an addon!

Stay tuned for Part 2, where I'll dive deeper into the time-selector component.

Get Social