$ npm install preform
Create forms in React the easy way!
Creating this library I focused on the following:
- Efficiency (no unnecessary render calls from react)
- Lightweight (less than 10kb)
- Stability (no mutable state or weird breaking changes)
- Using only React hooks, but with backwards compatibility using higher order components
- TypeScript support
The form context
When creating a form you should first wrap the container component in the asForm
function. This creates a context within which the state of all form fields will live.
import React from 'react';
import { asForm } from "preform";
const MyFormComponent = (props) => (
<div>
<h1>This is my form!</h1>
</div>
);
const MyForm = asForm(MyFormComponent);
export default MyForm;
It is possible to create multiple form contexts (components wrapped in asForm
) in the same page. Child components will only have access to their most direct container.
Creating an input field
Let's create a simple input field in which you can enter your email:
import React from 'react';
import { asForm, useField } from "preform";
const MyForm = (props) => {
const { setValue, value } = useField({
field: "firstName"
});
return (
<div>
<h1>This is my form!</h1>
<label for="firstName">First name</label>
<br />
<input
id="firstName"
type="text"
value={value}
onChange={event => setValue(event.target.value)}
/>
</div>
);
};
export default asForm(MyForm);
So what's going on here is that when calling asForm(MyForm)
a state is being created. When calling useField({ field: "firstName" })
we create an attribute inside that state labelled "firstName"
. useField
will then return value
and setValue
to use this particular attribute. If you call useField({ field: "firstName" })
again inside MyForm
it will return the same value as the one before. So you can think of the field
as being the unique identifier of a form field.
Knowing that we'll probably create more than one input field it would be a good idea to make a seperate Input
component from which will call useField
.
import React from 'react';
import { asForm, useField } from "preform";
const Input = (props) => {
const { setValue, value } = useField({
field: props.field,
initialValue: props.initialValue
});
return (
<div>
<label for={props.field}>{props.label}</label>
<br />
<input
id={props.field}
type={props.type}
value={value}
onChange={event => setValue(event.target.value)}
/>
</div>
)
}
const MyForm = (props) => {
return (
<div>
<h1>This is my form!</h1>
<Input
field="firstName"
type="text"
label="First name"
initialValue="Mickey"
/>
<Input
field="lastName"
type="text"
label="Last name"
initialValue="Mouse"
/>
</div>
);
};
export default asForm(MyForm);
You might notice useField
can also take an initialValue
.
Validation
Now let's make sure the name which you enter doesn't contain any numbers. We can check this using the regex /^[^\d]*$/.test('string value')
. Let's put this in a validator function.
const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';
A validator takes the value of the field and should return a falsy value if it's correct (null, undefined, '', false etc). It should return a string with a description of what went wrong if the value is invalid. The validator can also return a promise resolving or rejecting with these values.
The validator can be used as an argument in useField
along with the field. useField
also returns a validate
and error
variable which are coupled to whatever value the validator returns.
const { setValue, value, validate, error } = useField({
field: "name",
validator: validateName
});
validate
can be called so that the current value of the field gets validated. If the field is invalid, error
will be changed to an Error
object with the result of validator
as its message.
Let's use the code from our example to validate the first name and last name.
import React from 'react';
import { asForm, useField } from "preform";
const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';
const Input = (props) => {
const { setValue, value, validate, error } = useField({
field: props.field,
initialValue: props.initialValue,
validator: props.validator
});
return (
<div>
<label for={props.field}>{props.label}</label>
<br />
<input
id={props.field}
type={props.type}
value={value}
onChange={event => setValue(event.target.value)}
onBlur={validate}
/>
{error && (
<i>{error.message}</i>
)}
</div>
)
}
const MyForm = (props) => {
return (
<div>
<h1>This is my form!</h1>
<Input
field="firstName"
type="text"
label="First name"
initialValue="Mickey"
validator={validateName}
/>
<Input
field="lastName"
type="text"
label="Last name"
initialValue="Mouse"
validator={validateName}
/>
</div>
);
};
export default asForm(MyForm);
Right now we're validating every field on its own (using onBlur
). However, we can also instead choose to validate the whole form at a certain moment. For that we can use the useFormApi
hook.
useFormApi
This hook also brings some variables with which can be used to influence the form state as a whole.
import { useFormApi } from "preform";
const {
validate,
formState,
setValue
} = useFormApi();
validate
This function can be called at any time to validate all fields currently in the context. Each component which did a call to useField
will be updated if needed. This function returns a promise which will resolve as the new formState
.
formState
The formState
is an object with the following signature:
{
values: {
field: value
};
valid: boolean;
invalid: boolean;
loading: boolean;
dirty: boolean;
pristine: boolean;
submitted: boolean;
errors: {
field: Error
};
}
setValue
This function can be called to set the value of one specific field. Useful if you want to do this from outside the component which called useField
for that specific field.
setValue("firstName", "Minnie");
Using the useFormApi
we can create submit functionality in our example
import React from 'react';
import { asForm, useField, useFormApi } from "preform";
const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';
const Input = (props) => {...}
const MyForm = (props) => {
const { validate } = useFormApi();
const handleSubmit = async (event) => {
event.preventDefault();
const formState = await validate();
if (formState.valid) {
console.log(formState.values)
}
};
return (
<form onSubmit={handleSubmit}>
<h1>This is my form!</h1>
<Input
field="firstName"
type="text"
label="First name"
initialValue="Mickey"
validator={validateName}
/>
<Input
field="lastName"
type="text"
label="Last name"
initialValue="Mouse"
validator={validateName}
/>
<input type="submit" />
</form>
);
};
export default asForm(MyForm);
useSubmit
This function has almost the same signature as useCallback, only it makes sure the form has been validated successfully before the callback is being called. So from our example above we can rewrite this:
const handleSubmit = useCallback(async (event) => {
event.preventDefault();
const formState = await validate();
if (formState.valid) {
await request(formState.values);
console.log('Done!');
}
}, [request, validate]);
To this:
const handleSubmit = useSubmit(async (values) => {
await request(values);
console.log('Done!');
}, [request]);
The functionality above requires a whole lot of boilerplate, and it might also be difficult to achieve using React class components. Therefore this library contains a couple of "shortcut" functions.
asField
Instead of calling useField
you might want to wrap your input field component in the asField
wrapper.
import { asField, useField } from "preform";
const Input = (props) => {
const { setValue, value, error, validate } = useField({
field: props.field,
initialValue: props.initialValue,
validator: props.validator
});
return (
<div>
<input
value={value}
onChange={event => setValue(event.target.value)}
onBlur={validate}
/>
{error && (
<i>{error.message}</i>
)}
</div>
)
}
const InputComponent = (props) => {
return (
<div>
<input
value={props.value}
onChange={event => props.setValue(event.target.value)}
onBlur={props.validate}
/>
{props.error && (
<i>{props.error.message}</i>
)}
</div>
)
}
const Input = asField(InputComponent);
<Input
field="myField"
validator={myValidator}
initialValue={myInitialValue}
/>
asForm props and asSubmit
When calling asForm(MyComponent)
it will also pass some variables to MyComponent's props so that you don't have to call useFormApi
. These are formState
, validate
and setValue
. Besides that MyComponent will also receive the prop asSubmit
. This function has the following signature:
const handleSubmit = asSubmit((values) => {
})
Using asSubmit
we can simplify our earlier version of the example a bit more:
import React from 'react';
import { asForm, useField, useFormApi } from "preform";
const validateName = (value, values) => /^[^\d]*$/.test(value) ? null : 'A name should not contain numbers';
const Input = (props) => {...}
const MyForm = (props) => {
const handleSubmit = props.asSubmit((values) => {
console.log(values);
});
return (
<form onSubmit={handleSubmit}>
<h1>This is my form!</h1>
<Input
field="firstName"
type="text"
label="First name"
initialValue="Mickey"
validator={validateName}
/>
<Input
field="lastName"
type="text"
label="Last name"
initialValue="Mouse"
validator={validateName}
/>
<input type="submit" />
</form>
);
};
export default asForm(MyForm);