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.
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 thehandleOnChange
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.