simpler-redux-form
v0.3.73
Published
Just like `redux-form` but much simpler
Downloads
114
Readme
simpler-redux-form
Just like redux-form
but much simpler.
Install
npm install simpler-redux-form --save
Usage
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/>
'sonSubmit
property:<form onSubmit={submit(this.submitForm)}/>
, where thesubmitForm(values)
argument is your form submission function. If the form submission function returns aPromise
then the form'ssubmitting
flag will be set totrue
upon submit until the returnedPromise
is either resolved or rejected.submitting : boolean
— "Is this form currently being submitted?" flag, complements thesubmit()
functionPromise
abovefocus(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()
— resetsinvalidIndication
for 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. Thiserror
will be passed directly to the underlying field component.required : String or Boolean
— adds "this field is required" validation for the<Field/>
with theerror
message equal torequired
property value if it's aString
defaulting to"Required"
otherwise (required={true}
is passed to the underlying component). Note thatfalse
value passes therequired
validation requirement becausefalse
is a value (e.g. "Yes"/"No" dropdown), therefore usevalidate
instead 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 theerror
property passed to the<Field/>
or a validation error in casevalidate
property was set for this<Field/>
indicateInvalid : boolean
— tells thecomponent
whether it should render itself as being invalid
The indicateInvalid
algorythm is as follows:
Initially
indicateInvalid
for a field isfalse
Whenever the user submits the form,
indicateInvalid
becomestrue
for the first found invalid form fieldWhenever the user edits a field's value,
indicateInvalid
becomesfalse
for that fieldWhenever the new
error
property is manually set on the<Field/>
component,indicateInvalid
becomestrue
for 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 totrue
to enable form fields validation on "blur" event (i.e. when a user focuses out of a field it gets validated).trim : boolean
– Set tofalse
to disable field value trimming.defaultRequiredMessage : (props) -> String
– The defaulterror
message when using<Field required/>
feature (returns"Required"
by default).props
argument is the form's properties. Is only called in a web browser.defaultErrorHandler : (error, props)
— The defaulterror
handler.props
argument 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. Theerror
argument is anything thrown from form submit action (if form submit action returns aPromise
then theerror
will 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/>
error
s 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 error
s 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 avalues
object. 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 settingvalue
for 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 : boolean
property will be injected into the decorated form component, and also all<Field/>
s will bedisabled
while the form issubmitting
, and also the<Submit/>
button will be passedbusy={true}
property. Alternativelysubmitting
boolean property can be passed to the decorated form component viaprops
and it would have the same effect. By default, if the form submission function returns aPromise
then the form'ssubmitting
flag will be set totrue
upon submit until the returnedPromise
is either resolved or rejected. This extrasubmitting
setting complements thePromise
based one.autofocus : boolean
— by default the form focuses itself upon being mountedvalidateVisitedFields : boolean
– set totrue
to enable form fields validation on "blur" event (i.e. when a user focuses out of a field it gets validated)trim : boolean
– set tofalse
to disable field value trimmingmethods : [String]
— takes an optional array of method names which will be proxied to the decorated component instanceonError: (error, props)
— replacesconfiguration.defaultErrorHandler
for 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 settingvalue
for each<Field/>
.validateVisitedFields : boolean
– set totrue
to 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 formId
s 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/>
error
s) 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 aPromise
thenonSubmitted()
is called after thisPromise
is 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-router
route 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
.