simpler-redux-form
v0.3.73
Published
Just like `redux-form` but much simpler
Downloads
15
Readme
simpler-redux-form
Just like redux-form but much simpler.
Install
npm install simpler-redux-form --saveUsage
Add form reducer to the main Redux reducer
export { default as reducer_1 } from './reducer 1'
export { default as reducer_2 } from './reducer 2'
...
export { reducer as form } from 'simpler-redux-form'Create the form
import React, { Component } from 'react'
import Form, { Field, Submit } from 'simpler-redux-form'
import { connect } from 'react-redux'
// `redux-thunk` example.
// When `dispatch()`ed returns a `Promise`
function submitAction(values) {
return (dispatch) => {
dispatch({ type: 'SUBMIT_REQUEST' })
return ajax.post('/save', values).then(
(result) => dispatch({ type: 'SUBMIT_SUCCESS', result }),
(error) => dispatch({ type: 'SUBMIT_FAILURE', error })
)
}
}
@Form
@connect(
// Get user's current phone number from Redux state
state => ({ phone: state.user.phone }),
// Redux `bindActionCreators`
{ submitAction }
)
export default class PhoneNumberForm extends Component {
validatePhone(phone) {
if (!phone) {
return 'Phone number is required'
}
}
render() {
const { phone, submit, submitAction } = this.props
return (
<form
onSubmit={submit(submitAction)}>
<Field
name="phone"
component={ TexInput }
type="tel"
// Initial value for this field
value={ phone }
validate={ this.validatePhone }
placeholder="Enter phone number"/>
<Submit component={ SubmitButton }>Save</Submit>
</form>
)
}
}
function TextInput({ error, indicateInvalid, ...rest }) {
return (
<div>
<input type="text" { ...rest }/>
{/* Shows this form field validation error */}
{ indicateInvalid && <div className="error">{ error }</div> }
</div>
)
}
// `children` is button text ("Save")
function SubmitButton({ children }) {
return (
<button type="submit">
{ children }
</button>
)
}And use it anywhere on a page
import FormComponent from './form'
function Page() {
return <FormComponent/>
}API
@Form
@Form decorator ("higher order component") decorates a React component of the original form injecting the following props into it:
submit : Function— creates form submit handler, call it in your<form/>'sonSubmitproperty:<form onSubmit={submit(this.submitForm)}/>, where thesubmitForm(values)argument is your form submission function. If the form submission function returns aPromisethen the form'ssubmittingflag will be set totrueupon submit until the returnedPromiseis either resolved or rejected.submitting : boolean— "Is this form currently being submitted?" flag, complements thesubmit()functionPromiseabovefocus(fieldName : String)— focuses on a fieldscroll(fieldName : String)— scrolls to a field (if it's not visible on the screen)clear(fieldName : String)— clears field valueget(fieldName : String)— gets form field valueset(fieldName : String, value : String)— sets form field valuereset()— resets the formresetInvalidIndication()— resetsinvalidIndicationfor all fields in this form
Upon form submission, if any of its fields is invalid, then that field will be automatically scrolled to and focused, and the actual form submission won't happen.
Always extract the <form/> into its own component — it's much cleaner that way.
Field
<Field/> takes the following required props:
name— the field name (ID)component— the actual React component for this field
And also <Field/> takes the following optional props:
value- the initial value of the fieldvalidate(value, allFormValues) : String— form field value validation function returning an error message if the field value is invaliderror : String- an error message which can be set outside of thevalidate()function. Can be used for advanced validation, e.g. setting a"Wrong password"error on a password field after the form is submitted. Thiserrorwill be passed directly to the underlying field component.required : String or Boolean— adds "this field is required" validation for the<Field/>with theerrormessage equal torequiredproperty value if it's aStringdefaulting to"Required"otherwise (required={true}is passed to the underlying component). Note thatfalsevalue passes therequiredvalidation requirement becausefalseis a value (e.g. "Yes"/"No" dropdown), therefore usevalidateinstead for checkboxes that are required to be checked.
All other props are passed through to the underlying field component.
These additional props are passed to the underlying component:
error : String— either theerrorproperty passed to the<Field/>or a validation error in casevalidateproperty was set for this<Field/>indicateInvalid : boolean— tells thecomponentwhether it should render itself as being invalid
The indicateInvalid algorythm is as follows:
Initially
indicateInvalidfor a field isfalseWhenever the user submits the form,
indicateInvalidbecomestruefor the first found invalid form fieldWhenever the user edits a field's value,
indicateInvalidbecomesfalsefor that fieldWhenever the new
errorproperty is manually set on the<Field/>component,indicateInvalidbecomestruefor that field
Therefore, the purpose of indicateInvalid is to only show the error message when it makes sense. For example, while the user is inputting a phone number that phone number is invalid until the used inputs it fully, but it wouldn't make sense to show the "Invalid phone number" error to the user while he is in the process of inputting the phone number. Hence the indicateInvalid flag.
Submit
Use <Submit/> component to render the form submit button.
Takes the following required props:
component— the actual React submit button component
All other props are passed through to the underlying button component.
These additional props are passed to the underlying submit button component:
busy : boolean— indicates if the form is currently being submitted
@Form
class Form extends Component {
render() {
const { submit } = this.props
return (
<form onSubmit={ submit(...) }>
<Field component={ Input } name="text"/>
<Submit component={ Button }>Submit</Submit>
</form>
)
}
}
// `children` is button text (e.g. "Submit")
function Button({ busy, children }) {
return (
<button
type="submit"
disabled={ busy }>
{ busy && <Spinner/> }
{ children }
</button>
)
}reducer
The form Redux reducer is plugged into the main Redux reducer under the name of form (by default).
Configuration
simpler-redux-form has a global configure() function
import { configure } from 'simpler-redux-form'
configure({
validateVisitedFields: true,
trim: false,
defaultRequiredMessage: (props) => props.translate('form.field.required'),
defaultErrorHandler: (error, props) => props.dispatch(showErrorNotification(error.message))
})The configurable options are:
validateVisitedFields : boolean– Set totrueto enable form fields validation on "blur" event (i.e. when a user focuses out of a field it gets validated).trim : boolean– Set tofalseto disable field value trimming.defaultRequiredMessage : (props) -> String– The defaulterrormessage when using<Field required/>feature (returns"Required"by default).propsargument is the form's properties. Is only called in a web browser.defaultErrorHandler : (error, props)— The defaulterrorhandler.propsargument is the form's properties (including Reduxdispatch). Given that forms are submitted only in web browser this default error handler could show a popup with error details. Theerrorargument is anything thrown from form submit action (if form submit action returns aPromisethen theerrorwill be the promise's error). Is only called in a web browser.
Field errors
An error property can be set on a <Field/> if this field was the reason form submission failed on the server side.
This must not be a simple client-side validation error because for validation errors there already is validate property. Everything that can be validated up-front (i.e. before sending form data to the server) should be validated inside validate function. All other validity checks which can not be performed without submitting form data to the server are performed on the server side and if an error occurs then this error goes to the error property of the relevant <Field/>.
For example, consider a login form. Username and password <Field/>s both have validate properties set to the corresponding basic validation functions (e.g. checking that the values aren't empty). That's as much as can be validated before sending form data to the server. When the form data is sent to the server, server-side validation takes place: the server checks if the username exists and that the password matches. If the username doesn't exist then an error is returned from the HTTP POST request and the error property of the username <Field/> should be set to "User not found" error message. Otherwise, if the username does exist, but, say, the password doesn't match, then the error property of the password <Field/> should be set to "Wrong password" error message.
One thing to note about <Field/> errors is that they must be reset before form data is sent to the server: otherwise it would always say "Wrong password" even if the password is correct this time. Another case is when the error is set to the same value again (e.g. the entered password is wrong once again) which will not trigger showing that error because the error is shown only when its value changes: nextProps.error !== this.props.error. This is easily solved too by simply resetting errors before form data is sent to the server.
import { connect } from 'react-redux'
import Form, { Field } from 'simpler-redux-form'
@Form({ id: 'example' })
@connect(state => ({ loginError: state.loginForm.error }))
class LoginForm extends Component {
validateNotEmpty(value) {
if (!value) {
return 'Required'
}
}
submit(values) {
// Clears `state.loginForm.error`
dispatch({ type: 'LOGIN_FORM_CLEAR_ERROR' })
// Sends form data to the server
return dispatch(sendHTTPLoginRequest(values))
}
render() {
const { loginError } = this.props
return (
<form onSubmit={submit(this.submit)}>
<Field
component={Input}
name="username"
validate={this.validateNotEmpty}
error={loginError === 'User not found' ? loginError : undefined}/>
<Field
component={Input}
name="password"
validate={this.validateNotEmpty}
error={loginError === 'Wrong password' ? loginError : undefined}/>
<button type="submit">Log in</button>
</form>
)
}
}
function Input({ error, indicateInvalid, ...rest }) {
return (
<div>
<input {...rest}/>
{ indicateInvalid && <div className="error">{error}</div> }
</div>
)
}Advanced
This is an advanced section describing all other miscellaneous configuration options and use cases.
Form instance methods
The decorated form component instance exposes the following instance methods (in case anyone needs them):
getWrappedInstance()— Returns the original form component instance.focus(fieldName : String)— Focuses on a field.scroll(fieldName : String)— Scrolls to a field (if it's not visible on the screen).clear(fieldName : String)— Clears field value.get(fieldName : String)— Returns field value.set(fieldName : String, value : String)— Sets form field value.getValues() : Object?— Collects form field values and returns them as avaluesobject. If the form is invalid or busy then returns nothing.reset()— Resets the form.submit()— Submits the form by clicking the<button type="submit"/>inside this form.
Form decorator options
Besides the default expored Form decorator there is a named exported Form decorator creator which takes options:
// Use the named export `Form`
// instead of the default one
// to pass in options.
import { Form } from 'simpler-redux-form'
@Form({ ... })
class OrderForm extends Component {
...
}This @Form() decorator creator takes the following options:
values : object— initial form field values ({ field: value, ... }), an alternative way of settingvaluefor each<Field/>. Can also bevalues(props) => object.submitting(reduxState, props) => boolean— a function that determines by analysing current Redux state (having access to theprops) if the form is currently being submitted. If this option is specified thensubmitting : booleanproperty will be injected into the decorated form component, and also all<Field/>s will bedisabledwhile the form issubmitting, and also the<Submit/>button will be passedbusy={true}property. Alternativelysubmittingboolean property can be passed to the decorated form component viapropsand it would have the same effect. By default, if the form submission function returns aPromisethen the form'ssubmittingflag will be set totrueupon submit until the returnedPromiseis either resolved or rejected. This extrasubmittingsetting complements thePromisebased one.autofocus : boolean— by default the form focuses itself upon being mountedvalidateVisitedFields : boolean– set totrueto enable form fields validation on "blur" event (i.e. when a user focuses out of a field it gets validated)trim : boolean– set tofalseto disable field value trimmingmethods : [String]— takes an optional array of method names which will be proxied to the decorated component instanceonError: (error, props)— replacesconfiguration.defaultErrorHandlerfor a given form
Decorated form component properties
Decorated form components accept the following optional properties (besides formId):
values : object— initial form field values ({ field: value, ... }), an alternative way of settingvaluefor each<Field/>.validateVisitedFields : boolean– set totrueto enable form fields validation on "blur" event (i.e. when a user focuses out of a field it gets validated)
Form id
Each form has its application-wide globally unique form ID (because form data path inside Redux store is gonna be state.form.${id}). It can be set via a formId property.
@Form
class OrderForm extends Component {
...
}
class Page extends Component {
render() {
<OrderForm formId="orderForm"/>
}
}If no formId is passed then it's autogenerated. Explicitly giving forms their application-wide globally unique formIds may be required for advanced use cases: say, if Redux state is persisted to localStorage for offline work and then restored back when the page is opened again then the form fields won't loose their values if the formId is set explicitly because in case of an autogenerated one it will be autogenerated again and obviously won't match the old one (the one in the previously saved Redux state) so all form fields will be reset on a newly opened page.
preSubmit
If two arguments are passed to the submit(preSubmit, submitForm) form onSubmit handler then the first argument will be called before form submission attempt (before any validation) while the second argument (form submission itself) will be called only if the form validation passes — this can be used, for example, to reset custom form errors (not <Field/> errors) in preSubmit before the form tries to submit itself a subsequent time. For example, this could be used to reset overall form errors like "Form submission failed, try again later" which aren't bound to a particular form field, and if such errors aren't reset in preSubmit then they will be shown even if a user edits a field, clicks the "Submit" button once again, and a form field validation fails and nothing is actually submitted, but the aforementioned non-field errors stays confusing the user. Therefore such non-field errors should be always reset in preSubmit.
Abandoned forms
One day marketing department asked me if I could make it track abandoned forms via Google Analytics. For this reason form component instance has .getLatestFocusedField() method to find out which exact form field the user got confused by. getLatestFocusedField property function is also passed to the decorated form component.
Also the following @Form() decorator options are available:
onSubmitted(props)— is called after the form is submitted (if submit function returns aPromisethenonSubmitted()is called after thisPromiseis resolved)onAbandoned(props, field, value)— is called if the form was focued but was then abandoned (if the form was focused and then either the page is closed, orreact-routerroute is changed, or the form is unmounted), can be used for Google Analytics.
Alternatively the corresponding props may be passed directly to the decorated form component.
Connecting form fields
By default when a form field value changes the whole form is not being redrawn: only the changed form field is. This is for optimisation purposes. In case some form fields depend on the values of other form fields one can add onChange() property to those other form fields for calling this.forceUpdate() to redraw the whole form component.
<form>
<Field
name="gender"
component={ Select }
options={ ['Male', 'Female', 'Other'] }
onChange={ (value) => this.forceUpdate() }/>
{ get('gender') === 'Other' &&
<Field
name="gender_details"
component={ TextInput }/>
}
</form>Reducer name
It's form by default but can be configured by passing reducer parameter to the @Form() decorator.
Contributing and Feature requests
I made this little library because I liked (and almost reinvented myself) the idea of redux-form but still found redux-form to be somewhat bloated with numerous features. I aimed for simplicity and designed this library to have the minimal sane set of features. If you're looking for all the features of redux-form then go with redux-form.
