Create Redux Form
Redux Form is a great library for creating and managing forms and their state in redux applications. But, it leaves a lot of common boilerplate code up for the user to implement. This library aims to remove this duplication of code while at the same time keeping the ability to define your own custom form components.
To install, run yarn add create-redux-form
or npm install create-redux-form
.
First, you'll need redux-form
compatible input components. See Redux Form Field Usage.
Once you have the components you need you can go ahead and create your custom generator like this:
/* form-generator.js */
import createReduxForm from 'create-redux-form'
import FormHeader from './components/form-header'
import FormInfo from './components/form-info'
import TextInput from './components/text-input'
import NumberInput from './components/number-input'
export const { form, wizardForm } = createReduxForm({
header: FormHeader,
info: FormInfo,
text: TextInput,
number: NumberInput
})
The object passed in as the first parameter will be the UIKit
used to generate your forms.
This is how you would then use your generator to generate a form. Both form
and wizardForm
take two parameters, a formName
and a schema
.
/* validation.js */
export const required = name => v => (v ? undefined : `${name} is required.`)
export const number = name => v =>
Number.isNaN(Number(v)) ? `${name} must be a number.` : undefined
/* buy-pnk-form.js */
import { form } from './form-generator'
import { required, number } from './validation'
export const {
Form: BuyPNKForm, // The form react component
isInvalid: getBuyPNKFormIsInvalid, // A redux state selector that returns true if the form's validation passed and false otherwise
submit: submitBuyPNKForm // An action creator that returns an action object that submits the form when dispatched
} = form('buyPNKForm', {
header: {
type: 'header',
props: { title: 'BUY PNK' }
},
rate: {
type: 'info'
},
amount: {
type: 'text',
validate: [required, number]
}
})
If you wanted a wizard form instead, all you need to do is nest the schema one level deeper with components grouped in "steps". E.g.
/* buy-pnk-wizard-form.js */
import { form } from './form-generator'
import { required, number } from './validation'
export const {
Form: BuyPNKForm,
isInvalid: getBuyPNKFormIsInvalid,
submit: submitBuyPNKForm
} = form('buyPNKForm', {
// The names of the steps are irrelevant
info: {
header: {
type: 'header',
props: { title: 'BUY PNK' }
},
rate: {
type: 'info'
}
},
pay: {
amount: {
type: 'text',
validate: [required, number]
}
}
})
These are all the properties you can have in a schema.
const schema: {
// The key gets passed to your component as the prop `placeholder` after being converted from `camelCase` to `TitleCase`. E.g. accountUsername => Account Username
[placeholder]: {
type: string, // This is the component you want to use from the `UIKit` passed in to `createReduxForm`
// Validator functions or functions that take the field's placeholder and return a validator function, like the examples above, (`required`, `number`). Validator functions should return undefined if the checks passed or a string with an error message for the failure
validate: (
| ((placeholder: string) => (value: string) => undefined | string)
| ((value: string) => undefined | string)
)[],
visibleIf: string, // Only render this field if the value of the specified field is truthy or falsy. I.e. `visibleIf: 'email'` or `visibleIf: '!email'`
// `redux-form` [formValues](https://redux-form.com/7.2.3/docs/api/formvalues.md/)
formValues: boolean,
props: object, // Props to pass down to your component
reduxFormFieldProps: object // Props to pass to `redux-form`'s [Field](https://redux-form.com/7.2.3/docs/api/field.md/) component
}
} = {
email: {
type: 'text',
validate: [v => (v ? undefined : `${name} is required.`)],
visibleIf: 'username',
formValues: { currentUsername: 'username' },
props: { placeholder: 'Secondary Email' }, // You can override the default placeholder, `camelToTitleCase(key)`, like this too
reduxFormFieldProps: { normalize: v => v.replace(' ', '') }
}
}
This is how you would render and use the Form.
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
BuyPNKForm,
getBuyPNKFormIsInvalid,
submitBuyPNKForm
} from './buy-pnk-form'
const Component = ({ buyPNKFormIsInvalid, submitBuyPNKForm }) => (
<div>
{/* In addition to these props, you can also pass in any prop that a `redux-form` [form](https://redux-form.com/7.2.3/docs/api/reduxform.md/) takes */}
<BuyPNKForm
onSubmit={formData => `We submitted! ${formData}`} // Do what you need to do with the data
className="BuyPNKForm" // Inner `fieldset` will have class name `BuyPNKForm-fieldset` and inner `fields` will have class name `BuyPNKForm-fieldset-fields`
disabled={false}
/>
<button onClick={submitBuyPNKForm} disabled={buyPNKFormIsInvalid}>
Submit
</button>
</div>
)
export default connect(
state => (
{
buyPNKFormIsInvalid: getBuyPNKFormIsInvalid(state)
},
{
submitBuyPNKForm
}
)
)(Component)
This is how you would render and use the WizardForm.
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
BuyPNKForm,
getBuyPNKFormIsInvalid,
submitBuyPNKForm
} from './buy-pnk-form'
class Component extends PureComponent {
state = {
currentPage: 0,
hasPrevPage: false,
hasNextPage: false,
totalPages: 0
}
getBackHandlerRef = ref => (this.backHandlerRef = ref)
handlePageChange = (wizardFormState, formData) => {
console.log(`This is what we have entered so far ${formData}!`)
this.setState(wizardFormState)
}
render() {
const { buyPNKFormIsInvalid, submitBuyPNKForm } = this.props
const { hasPrevPage, hasNextPage } = this.state
return (
<div>
{/* In addition to these props, you can also pass in any prop that a `create-redux-form` Form takes and they will be applied to the current page in the wizard */}
<BuyPNKForm
onSubmit={formData => `We submitted! ${formData}`} // Do what you need to do with the data
backHandlerRef={this.getBackHandlerRef}
onPageChange={this.handlePageChange}
transitionName="someCSSAnimation" // Default is `carousel` and can be imported from the module, i.e. `@import '~create-redux-form/animations/carousel.css';`
className="BuyPNKWizardForm" // Inner `Form` will have class name `BuyPNKWizardForm-form`
/>
{hasPrevPage && <button onClick={this.backHandlerRef}>Go Back</button>}
<button onClick={submitBuyPNKForm} disabled={buyPNKFormIsInvalid}>
{hasNextPage ? 'Next' : 'Submit'}
</button>
</div>
)
}
}
export default connect(
state => (
{
buyPNKFormIsInvalid: getBuyPNKFormIsInvalid(state)
},
{
submitBuyPNKForm
}
)
)(Component)
Because you create your own UIKit
to pass into the createReduxForm
, it is very easy to extend your schema to support purely decorational components. Here is how you would make a simple info box.
import React from 'react'
import PropTypes from 'prop-types'
import './form-info.css'
const FormInfo = ({ input: { value } }) => (
<div className="FormInfo">
<h5 className="FormInfo-text">{value}</h5>
</div>
)
FormInfo.propTypes = {
input: PropTypes.shape({ value: PropTypes.string.isRequired }).isRequired
}
export default FormInfo
Then, when rendering the form that contains this field, you pass in the value in the redux-form
initialValues
prop.
<SomeForm
enableReinitialize // Enable this if the value is dynamic, so the form rerenders when it changes
keepDirtyOnReinitialize // This lets you keep the value of the other fields when the form reinitializes
initialValues={{
yourFieldSchemaKey: `Here is your info: ${someDynamicInfo}`
}}
/>