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 browserify browserify-incremental react react-dom –save 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](https://cdn.mojotech.com/2016/02/schnauzer-pups.jpg)

In the _form partial, the code now resembles:

data-date= <%= @daily_schedule.start_time %>>


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( ) });

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.