Skip to content

Instantly share code, notes, and snippets.

@insin
Last active August 7, 2023 15:45
Show Gist options
  • Save insin/bbf116e8ea10ef38447b to your computer and use it in GitHub Desktop.
Save insin/bbf116e8ea10ef38447b to your computer and use it in GitHub Desktop.

Example of implementing form components backed by redux-form, extracted from an application which uses react-bootstrap for layout and react-widgets' date picker, where all the forms redux-form is being used to manage happen to have 2 column layouts.

Live version: http://insin.github.io/redux-form-example/

To run the example with hot reloading via react-heatpack:

git clone git@gist.github.com:bbf116e8ea10ef38447b.git redux-form-example
cd redux-form-example
npm install
npm start

MIT Licensed

var Col = require('react-bootstrap/lib/Col')
var PageHeader = require('react-bootstrap/lib/PageHeader')
var React = require('react')
var Row = require('react-bootstrap/lib/Row')
var {connect} = require('react-redux')
var {reduxForm} = require('redux-form')
var DateInput = require('./DateInput')
var FormField = require('./FormField')
var LoadingButton = require('./LoadingButton')
var StaticField = require('./StaticField')
var TextInput = require('./TextInput')
var {zeroTime} = require('./utils')
var TODAY = zeroTime(new Date())
var mapStateToProps = state => state
var form = reduxForm({
form: 'addTravel',
fields: ['startDate', 'endDate', 'origin', 'destination', 'hotel', 'hasCar'],
touchOnChange: true, // react-widgets DateTimePicker doesn't blur
validate(travel) {
var errors = {}
if (!travel.startDate) errors.startDate = 'Please enter a start date.'
if (!travel.endDate) errors.endDate = 'Please enter an end date.'
if (travel.startDate && travel.endDate &&
zeroTime(travel.endDate) < zeroTime(travel.startDate)) {
errors.endDate = 'End date must not be earlier than start date.'
}
if (!travel.origin) errors.origin = 'Please enter an origin.'
if (!travel.destination) errors.destination = 'Please enter a destination.'
return errors
}
})
var AddTravel = React.createClass({
getInitialState() {
return {
fakeSaving: false,
fakeSubmitted: null
}
},
componentWillMount() {
this.props.initializeForm({
startDate: null,
endDate: null,
origin: '',
destination: '',
hotel: '',
hasCar: 'no'
})
},
/**
* Set endDate to startDate if it's blank or would otherwise be invalid.
*/
handleStartDateChange(startDate) {
var {endDate} = this.props.fields
if (endDate.value == null || endDate.value < startDate) {
endDate.onChange(startDate)
}
},
handleSubmit(data) {
this.setState({fakeSaving: true, fakeSubmitted: data})
setTimeout(() => this.setState({fakeSaving: false}), 2000)
},
render() {
var {fields} = this.props
var {fakeSaving, fakeSubmitted} = this.state
return <div className="container">
<PageHeader>redux-form example</PageHeader>
<form className="form-horizontal" onSubmit={this.props.handleSubmit(this.handleSubmit)}>
<Row>
<StaticField label="First Name:" value="Steve"/>
<StaticField label="Last Name:" value="Test"/>
</Row>
<Row>
<DateInput
afterChange={this.handleStartDateChange}
disabled={fakeSaving}
field={fields.startDate}
id="startDate"
label="Start Date:"
min={TODAY}
/>
<DateInput
disabled={fakeSaving}
field={fields.endDate}
id="endDate"
label="End Date:"
min={fields.startDate.value || TODAY}
/>
</Row>
<Row>
<TextInput
disabled={fakeSaving}
field={fields.origin}
id="origin"
label="Origin:"
/>
<TextInput
disabled={fakeSaving}
field={fields.destination}
label="Destination:"
id="destination"
/>
</Row>
<Row>
<TextInput
disabled={fakeSaving}
field={fields.hotel}
help="Please enter name of hotel here. If no hotel booking exists or unknown put 'N/A'"
id="hotel"
label="Hotel:"
/>
<FormField help="Please select 'Yes' if access to a car (rented or personal) during travel and 'No' if no access to a car during travel"
label="Car:">
<label className="radio-inline">
<input type="radio" name="hasCar" value="yes" onChange={fields.hasCar.onChange} disabled={fakeSaving}/> Yes
</label>
<label className="radio-inline">
<input type="radio" name="hasCar" value="no" onChange={fields.hasCar.onChange} defaultChecked disabled={fakeSaving}/> No
</label>
</FormField>
</Row>
<Row className="form-group">
<Col sm={12} className="text-center">
<LoadingButton
bsSize="large"
bsStyle="primary"
label="Add Travel"
loading={fakeSaving}
loadingLabel="Adding Travel"
type="submit"
/>
</Col>
</Row>
{fakeSubmitted && <pre><code>{JSON.stringify(fakeSubmitted, null, 2)}</code></pre>}
</form>
</div>
}
})
module.exports = connect(mapStateToProps)(form(AddTravel))
var {createStore} = require('redux')
var rootReducer = require('./reducers')
module.exports = function configureStore() {
var store = createStore(rootReducer)
if (process.env.NODE_ENV !== 'production') {
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('./reducers', () => {
var nextRootReducer = require('./reducers')
store.replaceReducer(nextRootReducer)
})
}
}
return store
}
var React = require('react')
var {PropTypes} = React
var DateTimePicker = require('react-widgets/lib/DateTimePicker')
var FormField = require('./FormField')
var DateInput = React.createClass({
propTypes: {
field: PropTypes.object.isRequired
},
shouldComponentUpdate: FormField.shouldFormFieldUpdate,
render() {
var {field, help, label, afterChange, ...inputProps} = this.props
var onChange = field.onChange
if (afterChange) {
onChange = function(...args) {
field.onChange(...args)
afterChange(...args)
}
}
return <FormField field={field} help={help} inputProps={inputProps} label={label}>
<DateTimePicker
{...inputProps}
format="dd/MM/yyyy"
name={field.name}
onChange={onChange}
time={false}
value={field.value}
/>
</FormField>
}
})
module.exports = DateInput
var classNames = require('classnames')
var Col = require('react-bootstrap/lib/Col')
var React = require('react')
var Row = require('react-bootstrap/lib/Row')
var {PropTypes} = React
var Help = require('./Help')
var Loading = require('./Loading')
var FIELD_EVENT_HANDLER = /^(?:on|handle)[A-Z]/
/**
* Perform shallow equals comparison of two redux-form field objects to
* determine if the field has changed.
*/
function fieldShallowEquals(field, nextField) {
for (var prop in field) {
// Ignore event handlers, as they continually get recreated by redux-form
if (!FIELD_EVENT_HANDLER.test(prop) && field[prop] !== nextField[prop]) {
return false
}
}
return true
}
/**
* Perform shallow equals comparison to determine if the props of the context
* form field component have changed, with special-case handling for the "field"
* prop, provided by redux-form.
* Use this as shouldComponentUpdate() on components which compose a
* FormField in their render() method and they will only re-render when
* necessary.
*/
function shouldFormFieldUpdate(nextProps) {
var keys = Object.keys(this.props)
var nextKeys = Object.keys(nextProps)
if (keys.length !== nextKeys.length) return true
var nextHasOwnProperty = Object.prototype.hasOwnProperty.bind(nextProps)
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
if (!nextHasOwnProperty(key) ||
key === 'field' ? !fieldShallowEquals(this.props[key], nextProps[key])
: this.props[key] !== nextProps[key]) {
return true
}
}
return false
}
/**
* A form field in a Bootstrap 3 two column layout.
*
* This component manages:
* - Bootstrap structure and classes
* - A loading indicator
* - A <Label> for the field
* - Label help text
* - Validation error style and display
*
* The form input itself should be passed as content.
*/
var FormField = React.createClass({
statics: {
shouldFormFieldUpdate
},
propTypes: {
// A redux-form field object
field: PropTypes.object,
// Help text to be displayed next to the label
help: PropTypes.string,
// An additional class to be applied to the input container
inputClass: PropTypes.string,
// Props used for the input (id is used to link the label to the input)
inputProps: PropTypes.object,
// Label text
label: PropTypes.string,
// Loading state
loading: PropTypes.bool
},
getDefaultProps() {
return {
field: {},
inputProps: {}
}
},
render() {
var {field, help, inputClass, inputProps, label, loading} = this.props
var error = field.touched && field.error
return <Col sm={6}>
<Row className={classNames('form-group', {'has-error': error})}>
<Col sm={4} className="control-label">
{loading && <Loading inline/>} <label htmlFor={inputProps.id}>{label}</label>
{help && <Help text={help}/>}
</Col>
<Col sm={8} className={inputClass}>
{this.props.children}
{error && <p className="help-block" style={{marginBottom: 0}}>{error}</p>}
</Col>
</Row>
</Col>
}
})
module.exports = FormField
.Help {
cursor: help;
display: inline-block;
font-size: 18px;
margin-left: .33em;
vertical-align: middle;
}
require('./Help.css')
var Glyphicon = require('react-bootstrap/lib/Glyphicon')
var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger')
var React = require('react')
var Tooltip = require('react-bootstrap/lib/Tooltip')
var Help = React.createClass({
propTypes: {
text: React.PropTypes.string.isRequired
},
render() {
var tooltip = <Tooltip>{this.props.text}</Tooltip>
return <OverlayTrigger overlay={tooltip} delayShow={300} delayHide={150}>
<Glyphicon className="Help" glyph="question-sign"/>
</OverlayTrigger>
}
})
module.exports = Help
require('bootstrap/dist/css/bootstrap.min.css')
require('react-widgets/dist/css/react-widgets.css')
require('./react-widgets-overrides.css')
var React = require('react')
var {Provider} = require('react-redux')
var AddTravel = require('./AddTravel')
var configureStore = require('./configureStore')
var store = configureStore()
React.render(<Provider store={store}>{() => <AddTravel/>}</Provider>, document.querySelector('#app'))
.Loading {
text-align: center;
color: #777;
padding: 2em 0;
transition: opacity .25s ease-in;
}
.Loading--inline {
color: inherit;
display: inline;
padding: 0;
}
.Loading--delaying {
visibility: hidden;
width: 0;
height: 0;
opacity: 0.01;
}
.Loading--displaying {
visibility: visible;
height: auto;
width: auto;
opacity: 1;
}
.Loading .glyphicon {
font-size: 60px;
animation: loading-spin 2s infinite linear;
}
/* Avoid height changes when an inline loading indicator appears */
.Loading--inline .glyphicon {
font-size: .75em;
}
.Loading__text {
margin-top: 12px;
}
@keyframes loading-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
require('./Loading.css')
var classNames = require('classnames')
var Glyphicon = require('react-bootstrap/lib/Glyphicon')
var React = require('react')
var Loading = React.createClass({
propTypes: {
delay: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.number
]),
inline: React.PropTypes.bool,
text: React.PropTypes.string
},
getDefaultProps() {
return {
delay: 500,
inline: false
}
},
getInitialState() {
return {
delaying: !!this.props.delay
}
},
componentDidMount() {
if (this.props.delay) {
this.timeout = setTimeout(this.handleDisplay, this.props.delay)
}
},
componentWillUnmount() {
if (this.timeout) {
clearTimeout(this.timeout)
}
},
handleDisplay() {
this.timeout = null
this.setState({delaying: false})
},
render() {
var {delay, inline, text} = this.props
var {delaying} = this.state
var className = classNames('Loading', {
'Loading--delaying': delaying,
'Loading--displaying': delay && !delaying,
'Loading--inline': inline
})
return <div className={className}>
<Glyphicon glyph="refresh"/>
{text && <div className="Loading__text">{text}&hellip;</div>}
</div>
}
})
module.exports = Loading
.LoadingButton__icon {
margin-right: 5px;
}
require('./LoadingButton.css')
var React = require('react')
var Button = require('react-bootstrap/lib/Button')
var Loading = require('./Loading')
var LoadingButton = React.createClass({
propTypes: {
label: React.PropTypes.string.isRequired,
loading: React.PropTypes.bool.isRequired,
icon: React.PropTypes.string,
// Defaults to label + 'ing' if not provided
loadingLabel: React.PropTypes.string
},
render() {
var {icon, label, loading, loadingLabel, ...props} = this.props
if (!loadingLabel) {
loadingLabel = `${label}ing`
}
return <Button disabled={loading} {...props}>
{loading
? <span><Loading inline delay={false}/> {icon && <img src={icon} className="LoadingButton__icon"/>} {loadingLabel}&hellip;</span>
: <span>{icon && <img src={icon} className="LoadingButton__icon"/>} {label}</span>
}
</Button>
}
})
module.exports = LoadingButton
{
"scripts": {
"start": "heatpack index.js"
},
"dependencies": {
"bootstrap": "3.3.5",
"classnames": "2.1.4",
"react": "0.13.3",
"react-bootstrap": "0.25.2",
"react-redux": "3.0.1",
"react-widgets": "2.8.1",
"redux": "3.0.2",
"redux-form": "2.0.0"
},
"devDependencies": {
"react-heatpack": "^1.5.0"
}
}
/* Use Bootstrap error styles for widgets. */
.has-error .rw-widget {
border-color: #A94442;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset;
}
.has-error .rw-widget.rw-state-focus {
border-color: #A94442;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 6px #CE8483;
}
var {combineReducers} = require('redux')
var {reducer: formReducer} = require('redux-form')
module.exports = combineReducers({
form: formReducer
})
var React = require('react')
var FormField = require('./FormField')
var StaticField = React.createClass({
shouldComponentUpdate(nextProps) {
return (this.props.label !== nextProps.label ||
this.props.value !== nextProps.value)
},
render() {
var {label, value} = this.props
return <FormField inputClass="form-control-static" label={label}>{value}</FormField>
}
})
module.exports = StaticField
var React = require('react')
var {PropTypes} = React
var FormField = require('./FormField')
var TextInput = React.createClass({
propTypes: {
field: PropTypes.object.isRequired
},
shouldComponentUpdate: FormField.shouldFormFieldUpdate,
render() {
var {field, help, label, onChange, ...inputProps} = this.props
return <FormField field={field} help={help} inputProps={inputProps} label={label}>
<input
{...inputProps}
className="form-control"
name={field.name}
onBlur={field.onBlur}
onChange={onChange && field.onChange}
/>
</FormField>
}
})
module.exports = TextInput
/**
* Convenience method for setting all time values to zero on a Date.
*/
function zeroTime(date) {
date.setHours(0, 0, 0, 0)
return date
}
module.exports = {
zeroTime
}
@janimattiellonen
Copy link

ERROR in ./index.js
Module not found: Error: Cannot resolve 'file' or 'directory' ./react-widgets-overrides.css in /opt/local/javascript/redux-form-example
@ ./index.js 7:0-40

@insin
Copy link
Author

insin commented Sep 30, 2015

Whoops, added the missing file.

@box-turtle
Copy link

this is super helpful!

@jamesblight
Copy link

If you don't want to use touchOnChange then you can use this hack to fix the Date & Time Picker onBlur issue

fixBlur = (event, input) => {
  event.target = {value: input.value};
  input.onBlur(event);  
};

<DateTimePicker
  {...inputProps}
  format="dd/MM/yyyy"
  name={field.name}
  onChange={onChange}
  time={false}
  value={field.value}
  onBlur={(event) => this.fixBlur(event, field)}
 />

This prevents onBlur from overriding the date value with a date string by injecting the date into the event object. Not pretty but it works.

@rockarena
Copy link

Is it possible to include MultiSelect component from react-widgets into the redux-form, using redux-form to manage it's state ?

Thanks

@Danita
Copy link

Danita commented Mar 22, 2016

Oh, yes, please, I'm tearing my hairs out on the Multiselect 😢

@Danita
Copy link

Danita commented Mar 22, 2016

Well, this is working for my basic Multiselect usecase, just in case anybody needs it:

import React from 'react';
import Multiselect from 'react-widgets/lib/Multiselect';

const TagsInput = React.createClass({

    getInitialState() {
        return {
            val: this.props.defaultValue || []
        };
    },

    _create(tag) {
        var val = this.state.val.concat(tag);
        this.setState({
            val
        });
        this.props.onChange(val);
        this.props.onBlur(val);
    },

    _change(val) {
        this.setState({
            val
        });
        this.props.onChange(val);
        this.props.onBlur(val);
    },

    render() {
        return (
            <Multiselect
                {...this.props}
                value={this.state.val}
                onCreate={this._create}
                onChange={this._change}
                onBlur={this._blur}
            />
        )
    }
});

export default TagsInput;

@narendrp
Copy link

Danita,
I am aslo struck here. Can you please post on how you are using the TagsInput component in your form.

@rockarena
Copy link

Multiselect state will be managed in redux-form once the component receives the onChange event via its field name
<Multiselect onChange={thisFieldName.onChange} />

@nielinjie
Copy link

Could you please give a example to populate/initial fields in the form?

@migara
Copy link

migara commented Jul 19, 2017

For onCreate just use onCreate={(e)=>input.onChange(input.value.concat(e))}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment