Codementor Events

Building a Time Picker with React JS

Published Oct 06, 2015Last updated Jan 18, 2017
Building a Time Picker with React JS

I recently wrote a tutorial on how to build a date picker with React JS, so I figured I'd expand upon that and detail how to write a time picker, too. First things first: just like last time, I've put the code up on GitHub if you want to see the full monty. I've also made a bower package for the time picker in case you want to use it on your site somewhere.

Note: The code is far from production ready, so use at your own risk.

If you'd just like to take a look at the time picker in action, I've also put together a (really simple) demo page. Take a look!

The Classes

I've broken down the React code into a bunch of distinct classes. Here's what they are and what they're responsible for.

  • TimePicker - This is the overall class that stitches everything together. It's what the user will instantiate in their own React project. It's responsible for rendering the trigger (an input field showing the currently selected time) and the Clock class.
  • Clock - The class used to show the user the actual time picker, which floats above or below the trigger depending on the user's vertical scroll position in the window. It handles showing and hiding the Hours and Minutes classes, as well as rendering the AmPmInfo class.
  • AmPmInfo - A class used to show two things: the currently selected time, and AM and PM buttons to allow the user to switch between morning and afternoon, respectively.
  • Hours - Responsible for showing the hours clock face from which the user can choose an hour for their time. Builds a list of hours, then hands off to the Face class.
  • Minutes - Same as the Hours class, but for minutes.
  • Face - This class is responsible for drawing the clock face utilized by the Hours and Minutes classes. It includes Ticks and LongHand.
  • LongHand - When a clock face is rendered, there's a line that's drawn from the centre to the selected value, just like the long hand on a clock. This class is responsible for drawing that line and updating its position.
  • Ticks - This class draws tick lines that correspond to minutes on the clock face. They sit around the edge.

There's quite a few classes here, but most of them are small and have a really narrow set of responsibilities. Lets get started.

TimePicker

var TimePicker = React.createClass({
    getInitialState: function getInitialState() {
        return {
            visible: false,
            hour: 12,
            minute: 0,
            am: true,
            position: {
                top: 0,
                left: 0
            }
        };
    },

    componentWillMount: function componentWillMount() {
        document.addEventListener("click", this.hideOnDocumentClick);
    },

    componentWillUnmount: function componentWillUnmount() {
        document.removeEventListener("click", this.hideOnDocumentClick);
    },

    show: function show() {
        var trigger = this.refs.trigger.getDOMNode(),
            rect = trigger.getBoundingClientRect(),
            isTopHalf = rect.top > window.innerHeight / 2;

        this.setState({
            visible: true,
            position: {
                top: isTopHalf ? rect.top + window.scrollY - CLOCK_SIZE - 3 : rect.top + trigger.clientHeight + window.scrollY + 3,
                left: rect.left
            }
        });
    },

    hide: function hide() {
        this.setState({
            visible: false
        });
    },

    hideOnDocumentClick: function hideOnDocumentClick(e) {
        if (!_parentsHaveClassName(e.target, "time-picker")) this.hide();
    },

    onTimeChanged: function onTimeChanged(hour, minute, am) {
        this.setState({
            hour: hour,
            minute: minute,
            am: am
        });
    },

    formatTime: function formatTime() {
        return this.state.hour + ":" + _pad(this.state.minute) + " " + (this.state.am ? "AM" : "PM");
    },

    render: function render() {
        return <div className="time-picker">
            <input ref="trigger" type="text" readOnly="true" value={_getTimeString(this.state.hour, this.state.minute, this.state.am)} onClick={this.show} />
            <Clock visible={this.state.visible} position={this.state.position} onTimeChanged={this.onTimeChanged} onDone={this.onDone} hour={this.state.hour} minute={this.state.minute} am={this.state.am} />
        </div>;
    }
});

First, we're setting up the main state for the entire time picker in the getInitialState function. Changes to these values drive rendering updates in the rest of the module. In the componentWillMount and componentWillUnmount methods, we're setting an event handler to hide the time picker in the event that the user clicks outside of it using the hideOnDocumentClick function.

The show method is a little more complicated. It's responsible for not only showing the time picker, but also for positioning it correctly within the window. We've set a ref on the trigger input element so that we can get its position. A ref in React JS obfuscates the DOM element away because most of the time we don't need to mess around with it. To get at the underlying DOM element, we call the getDOMNode function. We then call getBoundingClientRect to get its position in the page from which we can determine if the time picker should display above or below the trigger.

The hide method just hides the time picker by setting the state's visibility property to false.

The onTimeChanged method is an event handler that's fired when the user updates the time. That includes changes to the hour, the minute or the am/pm value. These state properties are what the Hours, Minutes and AmPmInfo classes are using to render their respective selected values.

The render method is required in a React class. For the TimePicker class, it's used to render two things: the trigger and the Clock module.

Clock

var Clock = React.createClass({
    getInitialState: function getInitialState() {
        return {
            hoursVisible: true,
            minutesVisible: false,
            position: "below"
        };
    },

    componentWillReceiveProps: function componentWillReceiveProps(props) {
        if (this.props.visible && !props.visible)
            this.setState({
                hoursVisible: true,
                minutesVisible: false,
                amPmVisible: false
            });
    },

    getTime: function getTime() {
        return {
            hour: this.props.hour,
            minute: this.props.minute,
            am: this.props.am
        };
    },

    onHourChanged: function onHourChanged(hour) {
        this._hour = hour;
        this.setState({
            hoursVisible: false,
            minutesVisible: true
        });
    },

    onHoursHidden: function onHoursHidden() {
        this.props.onTimeChanged(this._hour, this.props.minute, this.props.am);
    },

    onMinuteChanged: function onMinuteChanged(minute) {
        this.props.onDone();
        this._minute = minute;

        this.setState({
            minutesVisible: false,
            amPmVisible: true
        });
    },

    onMinutesHidden: function onMinutesHidden() {
        this.props.onTimeChanged(this.props.hour, this._minute, this.props.am);
    },

    onAmPmChanged: function onAmPmChanged(am) {
        this.props.onTimeChanged(this.props.hour, this.props.minute, am);
    },

    render: function render() {
        return <div className={"clock " + (this.props.visible ? "clock-show" : "clock-hide")} style={this.style()}>
            <div className="clock-face-wrapper">
                <Hours visible={this.state.hoursVisible} time={this.getTime()} onClick={this.onHourChanged} onHidden={this.onHoursHidden} />
                <Minutes visible={this.state.minutesVisible} time={this.getTime()} onClick={this.onMinuteChanged} onHidden={this.onMinutesHidden} />
            </div>
            <AmPmInfo time={this.getTime()} onChange={this.onAmPmChanged} />
        </div>;
    }
});

The state for the Clock class is pretty straightforward: all we're doing is setting which clock face is initially visible and that the position should be below the trigger.

The componentWilReceiveProps method isn't often used in React classes (or rather, I haven't used it very much), but it's a powerful method. It allows the class to react to changes in a prop before the render method fires. In our case here, I'm resetting the initial state of the module when the current visibility (which is retained in the componentWillReceiveProps method) is true and the new visibility is false, indicating that the parent wants the Clock to hide.

The getTime method is a convenience function for encapsulating the selected hour, minute and am/pm value to hand off to child classes.

The onHourChanged and onHoursHidden event handlers are linked. The former fires when the user has clicked on an hour, while the latter fires when the Hours clock face has faded away. These don't happen at the same time due to a fade transition that occurs. The onHourChanged function sets a class variable (not in the module's state, as we don't want to trigger a rerender yet) that contains the selected hour. Then, roughly half a second later when a transitionend event is triggered, the onHoursHidden event handler fires and updates the hour value in the Clock's state via the onTimeChanged prop, which in turn triggers the render across the module. The onMinuteChanged and onMinutesHidden function exactly the same, but for minutes instead of hours. The onAmPmChanged event handler doesn't trigger any animations, so it simply indicates to the parent that the time has changed.

During the render method, the Clock class builds the Hours, Minutes and AmPmInfo classes, along with a few positioning and style divs.

AmPmInfo

var AmPmInfo = React.createClass({
    render: function render() {
        var time = this.props.time;
        return <div className="am-pm-info">
            <div className={"am" + (time.am ? " selected" : "")} onClick={this.props.onChange.bind(null, true)}>AM</div>
            <div className="time">{_getTimeString(time.hour, time.minute, time.am)}</div>
            <div className={"pm" + (!time.am ? " selected" : "")} onClick={this.props.onChange.bind(null, false)}>PM</div>
        </div>;
    }
});

The AmPmInfo class is responsible for two things: providing buttons for AM and PM to allow the user to switch between the two, and to display the currently selected time. The Time class is responsible for the latter and sits in between the morning and afternoon buttons.

Hours

var Hours = React.createClass({
    buildHours: function buildHours() {
        var hours = [];
        for (var i = 1; i <= 12; i++) hours.push(i);
        return hours;
    },

    render: function() {
        var { time, ...props } = this.props;
        return <Face {...props} type="hours" values={this.buildHours()} selected={this.props.time.hour} />;
    }
});

The Hours is basically just a passthrough to the Face class. The buildHours function simply builds an array filled with the numbers 1 to 12 inclusive, which is passed in to the Face instance as the values to display.

The rest of the props that are passed in are passed in turn directly into the Face class. If you're unfamiliar with ES6, I'm using the destructuring assignment operator to pull time out of props, then using the spread operator to pass those props directly into the Face class. The bottom line here is that the props for the Hours class are basically the same as what's required for the Face class, so I'm taking all but the time prop and passing them directly to the Face instance.

Minutes

var Minutes = React.createClass({
    buildMinutes: function buildMinutes() {
        var minutes = [];
        for (var i = 1; i <= 12; i++) minutes.push(_pad((i === 12 ? 0 : i) * 5));
        return minutes;
    },

    render: function() {
        var { time, ...props } = this.props;
        return <Face {...props} type="minutes" values={this.buildMinutes()} selected={this.props.time.minute} />;
    }
});

The Minutes class is really similar to the Hours class in that it hands off to the Face class with a few custom data parameters. The values array is now populated with minute values instead of hours. That's it.

Face

var Face = React.createClass({
    componentDidMount: function componentDidMount() {
        this.refs.face.getDOMNode().addEventListener("transitionend", this.onTransitionEnd);
    },

    componentWillUnmount: function componentWillUnmount() {
        this.refs.face.getDOMNode().removeEventListener("transitionend", this.onTransitionEnd);
    },

    onTransitionEnd: function onTransitionEnd(e) {
        if (e.propertyName === "opacity" && e.target.className.indexOf("face-hide") > -1) this.props.onHidden();
    },

    render: function render() {
        return <div ref="face" className={"face " + this.props.type + (this.props.visible ? " face-show" : " face-hide")}>
            {this.props.values.map(function(value, i) {
                return <div key={i} className={"position position-" + (i + 1) + (parseInt(this.props.selected) === parseInt(value) ? " selected" : "")} onClick={this.props.onClick.bind(null, value)}>
                    {_pad(value)}
                </div>
            }.bind(this))}
            <LongHand type={this.props.type} selected={this.props.selected} />
            <div className="inner-face"></div>
            <Ticks />
        </div>;
    }
});

The Face class is responsible for drawing the hours and minutes clock faces that allow the user to select an hour and a minute, respectively. That includes drawing the ticks around the edge of the clock face (via the Ticks class), drawing the long hand that indicates the selected value (via the LongHand class) and handling the clicks of each individual value.

The componentDidMount and componentWillUnmount methods are used to listen to when the clock face's fade animation has completed. When that occurs, the onTransitionEnd method checks to see if the event matches up with what we're looking for (multiple transitionend events fire) and then executes the onHidden prop, which hooks up to the onHoursHidden or onMinutesHidden methods of the Clock class.

Note: I'm not a huge fan of how this code turned out, as my CSS is leaking into my JavaScript. I really don't want to have to check that the transition is for the opacity style, and I really, really don't want to have to check for a class name. If anyone has a better way of accomplishing this, I'm all ears - let me know in the comments!

The render method iterates over the values prop and spits out a div for each one using the map method. It then creates instances of the LongHand and Ticks classes, as well, to make things look a little more like a clock face.

LongHand

var LongHand = React.createClass({
    render: function render() {
        var deg = (this.props.selected / (this.props.type === "hours" ? 12 : 60) * 360);
        return <div>
            <div className="long-hand" style={{ transform: "rotate(" + deg + "deg) scale(1, 0.8)", WebkitTransform: "rotate(" + deg + "deg) scale(1, 0.8)" }}></div>
            <div className="long-hand-attachment"></div>
        </div>;
    }
});

The LongHand class is used purely to make things look a little nicer. It draws the line from centre of the clock face to the selected value, looking vaguely like the long hand of an analog clock. I'm doing this by drawing a line that's one pixel wide from the top centre to the middle centre of the clock face, and then rotating it the appropriate number of degrees using CSS transforms. Note that we have to use the "WebkitTransform" CSS property name as well as the normal "transform" because Safari doesn't yet support the non-prefixed version.

Ticks

var Ticks = React.createClass({
    buildTick: function buildTick(index) {
        return React.createElement(
            "div",
            { key: index, className: "tick " + (index % 5 === 0 ? "big " : ""), style: { transform: "rotate(" + index * 6 + "deg)", WebkitTransform: "rotate(" + index * 6 + "deg)" } },
            React.createElement("div", null)
        );
    },

    render: function render() {
        var ticks = [];
        for (var i = 0; i < 60; i++)
            ticks.push(this.buildTick(i));

        return <div className="ticks">
            {ticks}
        </div>;
    }
});

Similar to the LongHand class, the Ticks class is primarily used as makeup to allow the clock face to look a little closer to an actual clock face. Most of the magic here happens in the CSS (which you can see in the source code). Basically, I'm drawing a line from the centre top to the centre middle much like in the LongHand class, but this time, I'm doing it sixty times. Each of the lines is rotated around its bottom point to give the illusion of spokes on a wheel. Those lines are drawn in white, and a second div is position above and layered on top in a grey colour to give the appearance of etches in a clock face. Every fifth tick is a little larger than the rest, as they correspond to a selectable value.

Conclusion

As I said at the beginning of the post, if you're interested in using the time picker in your application, I've published it to bower. While the code isn't completely full of bugs, it's probably not production ready, and there are no unit tests to back it up, so user beware!

If you're interested in seeing the time picker in action, I've put together a demo page for you to take a look at. The source code is also available on GitHub.

If you have any questions about what I've written, please let me know in the comments. Thanks for reading!

Discover and read more posts from Chris Harrington
get started
post comments3Replies
Michael
8 years ago

Looks great, is there a way to combine both the date and time into a single field? i.e. Choose your date first and to the RHS pops out a time selector?

אופיר עדני
8 years ago

thank you , great one

xgqfrms
8 years ago

not bad!