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
npm install moment moment-timezone --savenpm 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 --savenpm 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
In the rails form we’ll replace the usual text_field helper used with jquery datepickers with
- 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
Step 2) The JavaScript
Add some organizational structure:
cd app/assets/javascriptstouch precompiled_app.jsmkdir -p react/componentstouch react/app.js.jsxtouch react/components/datepicker.js.jsx
The file
### 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.jsrequire ‘app.js.jsx’
### app/assets/javascripts/react/app.js.jsximport React from 'react'import DatePicker from './components/datepicker'import 'moment-timezone'import ReactDOM from 'react-dom'attachToElementsWithData('.datepicker_initializer', (data) => {return(<DatePickerdate={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
### app/assets/javascripts/react/constants/date_formatsexport 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.jsximport 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
### 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
### 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><DateTimeFielddateTime={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 prop is used to display the initial date passed in by Rails. It is not used in subsequent renders.defaultText
- 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 property.inputValue
- 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 inside thesetState()function in order to update the form field’s value as well as the date to display.handleOnChange
If you’ve noticed, we haven’t used any React Lifecycle methods such as
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.