Feb 23 2016

How to use a React DateTimePicker with Rails forms

Everyone in Boulder has a dog. Everyone in Boulder who has a dog and works at MojoTech brings their dog to work. However, not everyone that has a dog works at MojoTech, leading to a need for dog walkers and ultimately an application to schedule outings.

Imagine for a moment that DogWalker is a real application. We’ve analyzed the business model, and are expecting High Usership in the future. To reduce server loads and deliver a snappy user experience we plan on using a JavaScript front end. Since we want to add JavaScript components over time, we’re going to use the React JS library.

Later, in Phase 2, we’ll configure Webpack to use hot-loading (for happy development) and code-splitting (for performance). For now the app is small, we’re not noticing any problems bundling all of our React code with Browserify, so we’ll keep the configuration simple.

Our first component is a Bootstrap React Datepicker, to use inside a Rails form. Because we are setting the state within the component, it’s considered a Controlled Component (or ‘smart’ component).

The datepicker we used may be cloned from https://github.com/quri/react-bootstrap-datetimepicker.git. There are also several forks of this original project, which was in turn a port of https://github.com/Eonasdan/bootstrap-datetimepicker for React.js. We chose to use the original fork in order to add timezone functionality without modifying the plugin.

Step 1) Rails set-up

Set up your Rails project, and brew install node.js if necessary (on macs). Add

gem ‘browserify-rails’
to your gemfile. From the command line install the necessary dependencies. First, run
npm init
to insert a package.json in the root directory. Then run

npm install moment moment-timezone --save
npm install react-bootstrap-datetimepicker --save

Our Datepicker component is written using ES6 syntax, so we’ll need to add additional dependencies to transpile the code.

npm install babelify eslint babel-eslint --save
npm install eslint-plugin-react --save-dev

For your reference, the Dogwalker code is available at https://github.com/mojotech/dogwalker. If you’d like to follow along you may clone the repo, set up your preferred db (we’re using postgresql) and run

bundle && npm install
. You should be good to go.

In the rails form we’ll replace the usual text_field helper used with jquery datepickers with

<div class=”datepicker-initializer”></div>
. Passing information to the React component via data-attributes allows the component code to be re-usable. For our purposes we want to pass in:

  • The Rails model (data-model)
  • The form label name (data-label)
  • The form field (data-field)
  • The date and time to be edited, or other date/time (data-date).
  • A specific timezone (data-timezone) -- someone could be traveling to Boulder with their Miniature Schnauzer.

schnauzer pups

In the _form partial, the code now resembles:

<div class="datepicker-initializer"
data-model= "daily_schedule"
data-field= "date_and_time"
data-label= "Start Time"
data-timezone= <%= @dog_walker.time_zone %>
data-date= <%= @daily_schedule.start_time %>>
</div>

By default the datepicker uses the timezone of the browser, which could be confusing in some cases. Consequently, dog walkers are able to set the timezone for their location in their user profile, which is what we’re passing as

data-timezone
above. This is all we need to do in our Rails form, and now we’re ready to organize some JavaScript.

Step 2) The JavaScript

Add some organizational structure:

cd app/assets/javascripts
touch precompiled_app.js
mkdir -p react/components
touch react/app.js.jsx
touch react/components/datepicker.js.jsx

The file

precompiled_app.js
needs to be added to your JavaScript manifest in order to be compiled by the Rails asset pipeline:

### app/assets/javascripts/application.js
### . . . . .
//=require precompiled_app

and in turn requires the bundled (transpiled, browserified) react js.jsx files:

### app/assets/javascripts/precompiled_app.js
require ‘app.js.jsx’

app.js.jsx
is the main application “root” for all of the React code, and this is where we’ve put our initialization code. If we were to use Redux, we would also add the
<Provider>
inside this file.

### app/assets/javascripts/react/app.js.jsx
import React from 'react'
import DatePicker from './components/datepicker'
import 'moment-timezone'
import ReactDOM from 'react-dom'
attachToElementsWithData('.datepicker_initializer', (data) => {
return(
<DatePicker
date={data.date}
field={data.field}
model={data.model}
label={data.label}
currentTimeZone={data.timezone} />
)
});
export default function attachToElementsWithData(selector, callback) {
$(selector).each( function(index, item){
ReactDOM.render(callback($(item).data()), item)
})
}

Since we will probably use

moment
formatting in several areas of the application, we’ll declare the time formats as constants in a separate directory and then import them wherever needed:

### app/assets/javascripts/react/constants/date_formats
export const DATEPICKER_FORMAT =YYYY-MM-DD h:mm A
export const RAILS_FORMAT =YYYY-MM-DD HH:mm:ss Z
export const STANDARD_DATETIME_FORMAT =MMM D, hh:mm a’

The app.js file references the Datepicker Component, passing down the data-attributes as props. Now all we need is to add that component to datepicker.js.jsx (inside the components folder). In addition to the render function we’ll also add an initial state, and a handler for setting the state, when changed.

### app/assets/javascripts/react/components/datepicker.js.jsx
import React from "react"
import moment from 'moment'
import 'moment-timezone'
import DateTimeField from "react-bootstrap-datetimepicker"
import { RAILS_FORMAT, DATEPICKER_FORMAT } from '../../constants/date_formats'
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = this.initialState();
}
initialState() {
let {date, model, field, currentTimeZone} = this.props;
return {
format: this.formatInZone(),
date: this.dateInZone(),
inputProps: {
id: `${model}_${field}`,
name: `${model}[${field}]`
},
defaultText: this.defaultText()
}
}
### . . . .

The datetimepicker plugin takes several props, including

format
,
date
,
inputProps
(an object literal containing any extra information), and
defaultText
(used only in the initial render).

### app/assets/javascripts/react/components/datepicker.js.jsx - cont’d.
### . . . .
formatInZone() {
return this.props.date ? RAILS_FORMAT : DATEPICKER_FORMAT;
}
dateInZone() {
const date = this.props.date;
return date ? moment(date).format(this.formatInZone()) : this.todayInTimeZone();
}
defaultText() {
const date = this.props.date;
return date ? moment(date).format(DATEPICKER_FORMAT) : “”;
}

The functions

formatInZone()
and
dateInZone()
return dates formatted to the passed-in timezone, if it exists. Otherwise they will return the time based on the browser’s timezone.

### app/assets/javascripts/react/components/datepicker.js.jsx - cont’d.
### . . . .
handleChange(newDate) {
return this.setState({
date: newDate,
inputValue: this.dateInCurrentZone(newDate)
});
}
dateInCurrentZone(newDate) {
let {currentTimeZone} = this.props;
return moment.unix(newDate/1000).tz(currentTimeZone).format(DATEPICKER_FORMAT);
}
todayInTimeZone() {
let {currentTimeZone} = this.props;
return moment().tz(currentTimeZone).format(this.formatInZone());
}
render() {
const {date, inputFormat, format, inputProps, defaultText} = this.state;
return (
<div className="form-group">
<label className="control-label">{this.props.label}</label>
<DateTimeField
dateTime={date}
inputProps={inputProps}
inputFormat={DATEPICKER_FORMAT}
format={format}
defaultText={defaultText}
onChange={(newDate) => {this.handleChange(newDate)}} />
</div>
);
}
}
export default DatePicker

Now whenever a new date/time is selected, the onChange function is triggered, which calls the handleChange function, which in turn displays the date and time inside the form field. The field value is also simultaneously updated in a format which can be interpreted correctly by Rails.

There are a few gotchas to be aware of when using the react-bootstrap-datetimepicker module.

  • In the initial render, the
    defaultText
    prop is used to display the initial date passed in by Rails. It is not used in subsequent renders.
  • The plugin uses moment.js in strict mode, which means it’s imperative to match the format used for setting the new value to the
    inputValue
    property.
  • The plugin converts datetimes to timestamps with milliseconds. In order to format the date properly for Rails, we needed to convert to a unix timestamp in seconds.
  • It’s necessary to use
    setState()
    inside the
    handleOnChange
    function in order to update the form field’s value as well as the date to display.

If you’ve noticed, we haven’t used any React Lifecycle methods such as

ComponentDidMount
or
ComponentWillMount
in our code. But in fact the React Lifecycle is maintained; these methods are part of the
DateTimeField
Class, which lives inside the module. And there you have it, a simple re-usable React Datetimepicker.

It’s almost magic. Except it’s not. It’s React!

P.S. We're ramping up our engineering team! We're hiring in Boulder and Providence. Apply now.

Cathy Zoller

Share: