This library was heavily inspired by useFormState and Formik, which are great libraries on their own! react-fluent-form
aimes to provide a different API and additional features.
Check out the full API here. It's written for typescript!
- Form state handling: Storing field values and validation state
- Fluent API: Configure forms with fluent API syntax
- Integrated yup validation: Create validation schemes fluently
- HTML support: Support for all reasonable HTML
input
types,textarea
andselect
- Customizable: Add custom fields, also from third party libraries like react-select or attach a self-implented validator
This library supports react hooks only, so react v16.8
or greater is required.
npm i react-fluent-form
Following is a simple example for a registration form containing a username, gender and password field:
import { createForm, field, useFluentForm } from "react-fluent-form";
const formConfig = createForm()({
username: field.text(),
gender: field.radio("gender").unselectable(), // allows to select nothing
password: field.password().validateOnSubmit()
});
function RegistrationForm() {
const { values, fields, handleSubmit } = useFluentForm(formConfig);
const handleSubmitSuccess = () => console.log(values);
return (
<form onSubmit={handleSubmit(handleSubmitSuccess)}>
<label>
Username*:
<input {...fields.username} />
</label>
<div>
Gender:
<label>
male
<input {...fields.radio("male")} />
</label>
<label>
female
<input {...fields.radio("female")} />
</label>
</div>
<label>
Password*:
<input {...fields.password} />
</label>
<button type="submit">Submit</button>
</form>
);
}
Using withValidation
either a yup.Schema
or a validate function
can be provided for each field. Providing a yup.Schema
will result in a string[]
error type. In contrast to that you can return any type of data when using validate function
's:
formConfig.withValidation({
username: yup.string().required(),
password: value => {
if (value.length < 8) {
// return *any* custom error here (e.g. also complex objects or numbers)
return "Password is to short";
}
}
});
function RegistrationForm() {
const {
values,
touched,
validity,
errors,
fields,
handleSubmit
} = useFluentForm(formConfig);
const handleSubmitSuccess = () => console.log(values);
const handleSubmitFailure = () => console.log(errors);
return (
<form onSubmit={handleSubmit(handleSubmitSuccess, handleSubmitFailure)}>
<label>
Username*:
<input {...fields.username} />
{touched.username && validity.username && (
<div>{errors.username[0]}</div>
)}
</label>
<label>
Password*:
<input {...fields.password} />
{touched.password && validity.password && (
<div>{errors.password[0]}</div>
)}
</label>
<button type="submit">Submit</button>
</form>
);
}
In some cases it's required to work with values outside of your form.
This is where validation context
comes into place.
formConfig.withContext({
// It's recommend to wrap your context values in a "context" field (s. "Conditional validation" section below)
context: {
x: 1,
y: 2
}
});
If you want to update your context as soon as your context values have changed, you can take advandage of useEffect
:
const { setContext } = useFluentForm(formConfing);
useEffect(() => {
setContext({ context: coordinates });
}, [coordinates]);
You can trigger validation of all fields on context changes:
formConfig.validateOnContextChange();
formConfig.withValidation({
username: yup.string().when("$context.x", {
is: 0,
then: yup.string().required()
}),
password: (value, values, context) => {
if (context.x < context.y) return "error";
}
});
Often it's necessary to adapt validations for a field based on the values of other fields in your form (and also the context). This can be done via yup.Schema
's or via validate function
's.
It's very important to note that validate function
's can also return yup.Schema
's conditionally. The returned yup.Schema
will not be treated as an error type, it will be evaluated, thus the error type will be string[]
.
IMPORTANT:
When usingyup.Schema
's other form fields need to be accessed with a leading$
(here$lastName
) which usually means the value is comming from the context. In fact other form values are passed as context to theyup.Schema
instances for each field during validation execution.
To clearly seperate context values from field values it's recommened to wrap actual context values in acontext
field (as mentioned in Initial context)
formConfig.withValidation({
username: yup.string().required(),
firstName: yup.string().when("$lastName", {
is: "",
otherwise: yup.string().required()
}),
lastName: yup.string(),
password: (value, values) => {
if (value.includes(values.username)) {
return "Password should not contain username";
} else {
// the error type will be string[] here
return yup
.string()
.required()
.matches(/[a-zA-Z]/, "Password can only contain letters.");
}
}
});
When working with forms HTML elements are seldom enough to create beatufil and intuitive UI's.
That's why react-fluent-form
was build to be customizable, so custom field types can be added.
In some cases it's enought to use field.raw
(s. below).
If you maybe have your own validation library or you just don't like yup
, also a custom validator can be provided.
For components like react-datepicker it's not necessary to implement a custom field.
react-fluent-form
comes with a raw field type which works for components with following characteristics:
- it has
value
and aonChange
prop value
has the same type as the first parameter ofonChange
handler- it optionally has a
onBlur
prop to indicate when the field is touched
For raw fields it's required to pass an initial value:
const formConfig = createForm()({
dateOfBirth: field.raw(new Date())
});
const MyForm = () => {
const { fields } = useFluentForm(formConfig);
};
The type of fields
object would look like this:
type FieldsType = {
dateOfBirth: {
value: Date;
onChange: (newValue: Date) => void;
onBlur: () => void; // will just set the "touched" state to true
};
};
First of all a new class needs to be implemented which extends Fields
, the base class of every field. It's required to implement a function called mapToComponentProps
which receives a parameter with following properties:
value: ValueType
: the current value stored in state ofuseFluentForm
. Map this to the value prop of your component.setValue(v: ValueType)
: whenever your component changed its value, this function should be called (often it's anonChange
-like event)setTouched(value: boolean = true)
: call this function when your component has been touched. For most cases this function should be called when theonBlur
event was triggered.
Imagine you have implemented a custom input field that has an additional prop called onClear
which is called when the input should be cleared. On top of that you have an option to disable this functionality using the clearable
prop:
import { Field } from "react-fluent-form";
export class ClearableTextField extends Field {
constructor(initialValue = "") {
super(initialValue);
this.clearable = true;
}
// add functions to configure your field
// NOTE: configuration functions should always return "this" to stay conform to the fluent API syntax
isClearable = (value = true) => {
this.clearable = value;
return this;
};
mapToComponentProps = ({ value, setValue, setTouched }) => ({
value,
clearable: this.clearable,
onBlur: () => {
setTouched();
},
onChange: e => {
setValue(e.target.value);
},
onClear: () => {
setValue("");
}
});
}
For convenience purposes there is also a utility function named addField
that adds a custom field to the field
instance exported by react-fluent-form
(which is actually adding a new function to FieldCreator.prototype
). addField
should be called in a top level file:
import { addField } from "react-fluent-form";
addField("clearableText", intialValue => new ClearableTextField(initialValue));
The newly added field can then be used e.g. like so:
const formConfig = createForm()({
username: field.clearableText("initial value").isClearable(false)
});
To add a custom validator a class need to be implemented which extends Validator
. The only function that needs to be implemented is validateField
, which is called with following parameters:
field: KeyType
: name of the field that should be validatedvalues: ValuesType
: current values of the formcontext: any
: current context value
For the sake of simplicity lets assume you just want to have an optional required check on your fields. An implementation could look like following:
import { Validator } from "react-fluent-form";
export class RequiredValidator extends Validator {
constructor(requiredFields) {
super();
this.requiredFields = requiredFields;
}
public validateField(
field,
values,
_context // not relevant for this example
) {
if (this.requiredFields[field] && !values[field]) {
return "field is required";
}
}
}
Using withCustomValidator
a custom validator can be added to your form config:
NOTE: Attaching a custom validator will remove the
DefaultValidator
.
const formConfig = createForm()({
username: field.text(),
email: field.email(),
phone: field.tel()
}).withCustomValidator(new RequiredValidator({
username: true,
email: true
});
Not enough details? Check out the full API here. It's written for typescript!