Pragmatic Forms
A pragmatic approach to forms in React (Web and Native)
- Goals
- Example usage
- The Road to release
- API Documentation
configureForm: function(options:Object)
initFields: Function(props): { [fieldName: string]: any }
submit: Function(formData, props, formProps): Promise
validate?: Function(formData, props, formProps): { [fieldName: string]: any }
onSuccess?: Function(results, props, formProps): void
onError?: Function
onChange?: Function(formData, props, formProps): void
onFirstInteraction?: Function(formData, props, formProps): void
- The
form
Prop
- Reference material and prior art
Goals
- be simple
- be declarative
- be un-magical
- be performant
- be just react (and modern JS)
- work with YOUR state management system
- work in browser and react-native
Example usage
import { configureForm } from '@thinkmill/pragmatic-forms';
const RegistrationForm = configureForm({
initFields: () => ({
name: '',
email: '',
}),
validate: ({ name }) => {
const errors = {};
if (!name) errors.name = 'Please enter a name';
if (!name) errors.email = 'Please enter an email';
return errors;
},
submit: (formData) => {
return fetch('/registration', {
method: 'POST',
body: JSON.stringify(formData),
})
.then(res => res.json());
},
})(({ form }) => (
<form.Form>
{form.hasErrors &&
<div>
<p style={{ color: 'red' }}>Please correct the your input</p>
</div>
}
<input {...form.getInputProps({ name: 'name', type: 'text' })} />
<input {...form.getInputProps({ name: 'email', type: 'email' })} />
<button
type="submit"
disabled={form.hasErrors || form.isLoading}
>
Submit
</button>
</form.Form>
));
API Documentation
When working with pragmatic-forms
you will be interacting with either the
configureForm
method or the form
prop passed to your component.
configureForm: function(options:Object)
The pragmatic-forms
module exports a single named function: configureForm
. This method creates a configured Higher Order Component to wrap a form providing state and event handlers through a single prop named form
.
configureForm
accepts an options object and returns a method for creating a higher order component which will wrap your form to provide state and event handlers.
initFields: Function
submit: Function
validate?: Function
onSuccess?: Function
onReset?: Function
onError?: Function
onFirstInteraction?: Function
initFields: Function(props): { [fieldName: string]: any }
required
The initFields
method will receive props as it's only argument. This method should return an object with key/value pairs that provide the default value for each form field in your form.
Tip: This is a good place to set a default value for a field to avoid the react warning "changing an uncontrolled input of type text to be controlled".
eg.
const withForm = configureForm({
initFields: (props) => ({
name: props.name || '',
email: props.email || '',
}),
...
});
submit: Function(formData, props, formProps): Promise
required
The submit
method will be called with formData
, props
and formProps
and should return a promise.
eg. Trigger a graphql
mutation using react-apollo
import { graphql, gql, compose} from 'react-apollo';
import { configureForm } from 'pragmatic-forms';
const query = gql`
mutation createUser (
$name: String!
$email: String!
) {
createUser (user: {
name: $name
email: $email
})
{
id
}
}
`;
export const MyForm = compose(
graphql(query),
configureForm({
initFields: () => ({ name: '', email: '' }),
submit: (formData, props) => {
return props.mutate({
variables: {
name: formData.name,
email: formData.email,
},
});
}
})
)(({ form }) => (
<form onSubmit={form.onSubmit}>
{'...'}
</form>
));
validate?: Function(formData, props, formProps): { [fieldName: string]: any }
optional
The validate
method receives formData
, props
and props
and returns a map (Object) of errors keyed by the relevant fieldName.
eg.
const withForm = configureForm({
initFields: () => ({ email: '' }),
submit: (formData) => console.log(formData),
validate: (formData, props) => {
const errors = {};
if (!formData.email.includes('@')) {
errors.email = 'Please enter a valid email address';
}
return errors;
},
});
Callbacks
Callbacks provide a way to act on the result of an operation or event within the form. All callbacks are called with an event specific value plus the props
passed to the form and the formProps
object.
onSuccess?: Function(results, props, formProps): void
optional
The onSuccess
method is called after submit
has resolved, the form state has been update and setState
has been called.
It receives the result of the submit
method, props
and formProps
as arguments.
onError?: Function(reason, props, formProps)
optional
The onError
method is called after submit
has rejected, the form state has been update and setState
has been called.
It receives the rejection reason of the submit
method props
and formProps
as arguments.
onReset?: Function(formData, props, formProps): void
optional
The onReset
method is called on a form reset
event after the form has been reinitialised, the form state has been update and setState
has been called.
It receives the newly re-initialised formData
, props
and formProps
as arguments.
onChange?: Function(formData, props, formProps): void
optional
The onChange
method is called after the internal setState
is complete when any form field has fired it's onChange
or onValueChange
event.
It receives the complete formData
object, props
and formProps
as arguments.
onFirstInteraction?: Function(formData, props, formProps): void
optional
The onFirstInteraction
method is called when a form fields onChange
handler is triggered.
It receives the current formData
, props
and formProps
.
The form
Prop
aka formProps
The form
prop provides access to the state
and methods
provided by pragmatic-forms
inside your component. The same object is also passed as the last parameter to each of the configureForm
methods (excluding initFields
).
form.isLoading: boolean
true
if the submit method has been called and the promise has not resolved or rejected. Otherwise false
form.isPristine: boolean
true
until a change
event is triggered on one or more of the forms inputs.
form.submitError: any
submitError
is populated with the rejection reason
if the form submit Promise is rejected
.
form.submitResult: any
submitResult
is populated with the resolved value (if any) once the forms submit Promise has resolved
.
form.errors: { [fieldName: string]: any }
Key-value pairs giving the validation errors for each field by field name.
The value
will be whatever was returned in the validation method.
form.hasErrors: boolean
true
if the validate method returned an error
object with at least one property.
form.fields: Object
Provides access to the form fields as they are stored internally in pragmatic-forms
.
Each field will have the following shape:
[fieldName: string]: {
value: 'field value',
isDirty: false,
error: 'some error',
}
form.submit: Function(): void
Calling form.submit
will trigger the submit handler directly.
Use cases:
- have a form which is not wrapped in a form tag
- trigger form submission programatically (eg. on a timer)
eg. Create a delete button
const withForm = configureForm({
initFields: (props) => ({ id: props.id }),
submit: ({ id }) => {
return fetch(`/item/${id}`, { method: 'delete' });
}
})
const DeleteBtn = withForm(({ form }) => (
<button
type="button"
onClick={form.submit}
disabled={form.isLoading}
>
Delete me
</button>
));
form.reset: Function() :void
Calling form.reset
will reset the form to it's original state.
form.onSubmit: Function(event?: any) :void
The submit
event handler. This should be passed as onSubmit
to a <form>
component.
eg.
const withForm = configureForm({ ... });
const MyForm = withForm(({ form }) => (
<form onSubmit={form.onSubmit}>
...
<button type="submit">Submit</button>
</form>
));
form.onReset: Function(event?: any) :void
The reset
event handler. This should be passed as onReset
to a <form>
component. When the reset
event is triggered, the form will be reset to it's original state.
eg.
const withForm = configureForm({ ... });
const MyForm = withForm(({ form }) => (
<form onReset={form.onReset}>
...
<button type="reset">Reset</button>
</form>
));
form.updateField: Function(name:String, value:any) :void
Directly update the value of a field by name.
form.getInputProps: Function(options): InputProps
Returns an object with props which can be passed to an input
component.
NOTE: For checkboxes it is important to provide the correct type. ie. 'checkbox'. This allows the onChange event handler to check the checked state of the input rather than reading the value.
options
name: string
type?: string
Defaults to "text"
value?:any
TODO: Requires explanation.checked?: boolean
Whether a checkbox is checked. Defaults to false
forceType?: 'string' | 'number' | 'boolean'
undefined by default.
If forceType
is true, pragmatic-forms will attempt to maintain the primitive type of the initial value of a field. For example, if initFields
returns a boolean
for the field isEnabled
and onChange
is called with a string
it will attempt to convert that string back to a boolean. This can be helpful when your Input component converts the value field type to a string.
Currently forceType
supports string
, number
and boolean
.
InputProps
disabled: boolean
true
while the form isLoading
name: string
whatever was provided in options
.type: string
whatever was provided in options
. Defaults to text
onChange: (event) => void
a change handlerchecked: boolean
Only for a checkbox
or radio
value
Not provided for a checkbox
.
form.getFieldProps: Function(options): FieldProps
Takes the same options as form.getInputProps
.
In addition to the props provided by form.getInputProps
this method also returns props which can be used to show more information on a custom component.
error?: string
Either a string error message or null
if there is no error.isDirty: boolean
true
when the field has been modified.onValueChange: (value: any) =>
a special change handler which accepts the value directly rather than via a change event.
Build and release
yarn build
will compile the code which can then be either published or yarn link
ed if you are developing.
To release a new version of run yarn publish
. This will run the build
script and prompt for a new version number
.
Reference material and prior art
Many of the ideas in here are not new. This is a list of some of the places I have taken inspiration from.