Themes are probably the most central piece of React Reform. Use the createTheme
function to pass all necessary information about how you want to render your forms.
To give you a quick impression of how things work let's start with a simple example:
createTheme({
renderForm: (FormContainer, children) => (
<FormContainer>
<div>{children}</div>
<button>Submit</button>
</FormContainer>
),
renderField: (Field, {name, validations}) => {
const errors = validations
.filter(({isValid}) => isValid === false)
.map(({errorMessage, name}) => <span key={name}>{errorMessage} </span>)
return (
<div>
<label>{name}</label>
<Field/>
{errors.length > 0 && <span>{errors}</span>}
</div>
)
}
})
This results in an unstyled form with it's fields getting basic error messages.
Now let's take a look at a more complete Example. While the lines below certainly offer nothing production ready, they certainly should hint at most of what React Reform is capable of and how it's supposed to work.
createTheme({
renderForm: (FormContainer, children, {directProps: {buttonLabel, style, ...remainingDirectProps}, isValid, status, globalErrors}) => (
<FormContainer {...remainingDirectProps} style={{background: isValid ? "green" : "red", ...style}}>
{globalErrors.length ? (
globalErrors.map((error, i) => <div style={{color: "red"}} key={i}>{error}</div>)
) : null}
<div>{children}</div>
<button disabled={status === "pending"}>Submit</button>
</FormContainer>
),
renderField: (Field, {name, validations, directProps, wrapperProps, id, isDirty, isTouched, isFocused, formStatus}) => {
const errors = validations
.filter(({isValid}) => isValid === false)
.map(({errorMessage, name}) => <span key={name}>{errorMessage} </span>)
const hints = validations.map(({hintMessage, name}) => <span key={name}>{hintMessage} </span>)
const label = (
<label htmlFor={id}
style={{color: errors.length > 0 && isDirty ? "red" : "black"}}
>{directProps.label || name}</label>
)
const field = (
<Field id={id}
disabled={formStatus === "pending"}
style={{border: `1px solid ${isFocused ? "blue" : "black"}`}}
/>
)
return (
<div>
{wrapperProps.type === "Checkbox" ? (
<div>{field} {label}</div>
) : (
<div>{label} {field}</div>
)}
{hints.length > 0 && <span>{hints}</span>}
{directProps.explanation && <div className="form-explanaition">{directProps.explanation}</div>}
{errors.length > 0 && !isTouched && <span>{errors}</span>}
</div>
)
}
validationLabels: {
required: {
errorMessage: (val, {name, arg}) => `'${name}' is really required`
}
}
})
createTheme(themeDefinition)
createTheme(themeDefinition)
expects a themeDefinition
of this shape
const themeDefinition = {
renderForm: renderFormFn,
renderField: renderFieldFn,
validationLabels: validationLabelsObject (optional)
}
This function returns a theme object which can be used either in the ReformContext
component or can be passed directly to a form's theme
prop.
(FormContainer, children, data) => {...}
This function allows you to define the body of all the forms using your theme. Typically this includes global error messages (i.e messages that are not specific to a field) and the submit button (if any)
FormContainer
The FormContainer
represents the actual <form/>
tag. It's up to the theme-author to decide if users may pass props to this Component. In this case you may write something like <FormContainer {...data.directProps}>
.
children
As the name suggests, this argument contains the form's children. I.e. the fields of the form.
data.directProps
This field contains all the props that the user passes to the form. This could be used to allow overwriting/extending the style
or className
of a form.
You may also define your own contracts and allow the user to pass props like buttonLabel
or noButton
or formWidth
to customise the form according to your theme's capabilities.
Note that directProps
actually doesn't include all props. theme
, children
, onSubmit
, model
, initialModel
and onFieldChange
get exlcuded.
data.globalErrors
When a form's onSubmit
handler returns a promise that gets rejected, you may decide whether to reject with an object describing which fields were the reason, or whether to reject with a string or React element. The latter will be found in the globalErrors
list.
data.globalErrors
's shape looks like this
[
'Your error message as a String (if you reject with reject(string)), you may also reject an React element.',
{fieldName: 'if you reject with `reject({fieldName: string})` and a field with `name="fieldName"` is not present in your form, it'll be here'},
]
data.isValid
If all fields' validations return true
, isValid
will be true
. false
in all other cases.
Note that you may define (async) validations that return strings like "pending"
in this case isValid
will be false
as well.
data.validationsPerField
For more fine grained control you may access all validation objects from all fields. This field will have a shape like this:
{
fieldName1: [
{name: 'required', isValid: true, errorMessage: '...', hintMessage: '...'},
...
],
fieldName2: [
{name: 'unique', isValid: "pending", errorMessage: '...', hintMessage: '...'}
],
}
data.status
This field indicates the current state of the form. status
can have these values:
'unsubmitted'
: the form wasn't submitted yet'preSubmitFail'
: the form was submitted but validation errors were found.'pending'
: the form's onSubmit
handler returned a Promise and was neither resolved nor rejected yet.'success'
: the form's onSubmit
handler returned a Promise and the Promise was resolved.'postSubmitFail'
: the form's onSubmit
handler returned a Promise and the Promise was rejected.'UNCERTAIN STATUS'
: the form's onSubmit
handler returned no promise. This still is a TODO. So please file an issue if you have good generalisable idea for a status here.data.submitForm
If your theme is attempting some non-standard submit behaviours, you get access to the submit function which will validate the form and invoke the form's onSubmit
handler.
Have a look at the Multiple submit buttons recipe to see a use-case.
(Field, data) => {...}
This function let's you describe how each field should be rendered and how to display its different states.
Field
The Field
contains the wrapped input. You may pass any props to it and may access them via WrapInput
's themeProps
. The recommended behaviour is to pass all themeProps
directly to the input. So please filter your props here in the renderField
function.
Typically you want to set className
, style
or id
props. But depending on your requirements you may also set e.g. onBlur
handlers.
data.directProps
This field contains all the non-validation props that the user passes to the input (minus the name
) prop. Use this to pass style or class information to the input. You may also allow custom props such as label
or explanation
and deal with those within your theme.
data.name
Contains the required name
attribute passed to input. Might act as a good fallback if no better label is present.
data.validations
A list comprised of the validation objects for each validation on this input. It has a shape like this:
[
{name: 'required', isValid: true, errorMessage: '...', hintMessage: '...'},
{name: 'unique', isValid: "pending", errorMessage: '...', hintMessage: '...'},
{name: 'minlength', isValid: false, errorMessage: '...', hintMessage: '...'}
}
data.wrapperProps
When wrapping your inputs you may pass props to the WrapInput
component. For example, it's recommended to pass a type
prop to help differenatiate between different inputs. Have a look at the Full example above to see how it may help you.
data.directFormProps
Sometimes it might be useful to set some properties on a form that should affect all fields. Think of e.g. <Form onSubmit={...} fieldHeight="big">
. directFormProps
allows you to access this information.
data.id
Contains a generated and unique id which can be used to bind a label to a input via htmlFor
.
data.isDirty
Is true
if the input's onChange
was called once.
data.isTouched
Is true
if the input's onFocus
was called once.
data.isFocused
Is true
if the input is currently focused.
data.submitForm
As with the renderForm
function above, you have access to the form's submit handler to allow for custom submit behaviour. Have a look at the Submit on blur recipe to see how it might help.
data.formStatus
Contains the form's status described in the renderFormFn
section above. Use it if you would like to disable the inputs while the form is 'pending'
.
Specific themes might want to phrase validation messages differently. To allow for this feature, add a validation object of this shape to overwrite the validation's default messages:
{
required: {
errorMessage: (val, {name, arg}) => `'${name}' is really required`,
hintMessage: (val, {name, arg}) => `'${name}' needs to be set.`,
}
}
Note that you may omit either hintMessage
or errorMessage
to fallback to the validation's default message.