Package Exports
- react-fluent-form
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (react-fluent-form) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
react-fluent-form
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.
For a quick introduction you can read this blog post!
Check out the full API here. It's written for typescript!
Core Features
- 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
inputtypes,textareaandselect - Customizable: Add custom fields, also from third party libraries like react-select or attach a self-implemented validator
Installation & Prerequisites
This library supports react hooks only, so react v16.8 or greater is required.react-fluent-form already comes with typings, so no other package is needed to run this library with typescript.
npm i react-fluent-formForm State Handling
Following is a simple example for a registration form containing a username, gender and password field.
1. Creating the config
With createForm and field the basic form configuration can be described:
import { createForm, field } from "react-fluent-form";
const formConfig = createForm()({
username: field.text(),
gender: field.radio().name("gender").unselectable(), // unselectable() allows to select nothing
password: field.password().validateOnSubmitOnly()
});2. Initializing the form
Initialize the form with previous formConfig using the useFluentForm hook:
import { useFluentForm } from "react-fluent-form";
const { values, fields, handleSubmit /* and more.. */ } = useFluentForm(
formConfig
);The objects values and fields (and also other objects returned by useFluentForm) will contain properties for each field name e.g. values could look like:
{
username: "user",
gender: "",
password: "sg$!sga86"
}3. Rendering the form
The return value of useFluentForm will provide everything required for form state handling (fields object) and for form submission (handleSubmit function):
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.gender("male")} />
</label>
<label>
female
<input {...fields.gender("female")} />
</label>
</div>
<label>
Password*:
<input {...fields.password} />
</label>
<button type="submit">Submit</button>
</form>
);
}Validation
react-fluent-form comes with a build in validation approach that also enables customization.
Basic Usage
In this example validation will be added for a username and password field.
Adding validation to config
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 too short";
}
}
});Validation properties
touched, validity and errors are properties which are mostly relevant for validation. They are similarirly structured to values and fields:
const { touched, validity, errors } = useFluentForm(formConfig);touched: stores information about touched state of each field. A field is touched once it had focus and then lost it, so from a technical perspective if theonBlurevent of an input field was triggert.- example:
{username: true, password: undefined} - possible values:
true,falseorundefined(undefinedmeans it was not touched yet)
- example:
validity: stores information about validation state of each field.- example:
{username: false, password: undefined}(undefinedmeans it was not validated yet). - possible values:
true,falseorundefined(undefinedmeans it was not validated yet)
- example:
errors: contains the current errors of each field. In case of an error the evaluation ofyupschemes will result in astring[]type.- example:
{username: ["username is a required field"], password: undefined } - possible values:
any custom typeor undefined (undefinedmeans the field was not validated yet or that it's valid).
- example:
Displaying errors
In order to properly display error messages (and maybe also success messages) properties touched, validity and errors can be used. To handle validation failures on submission a callback can be provided as second argument of handleSubmit:
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} />
{/* validity.password stays undefined until the submission (validateOnSubmitOnly) */}
{touched.password && validity.password === false && <div>{errors.password[0]}</div>}
</label>
<button type="submit">Submit</button>
</form>
);
}Validation context
In some cases it's required to work with values outside of your form.
This is where validation context comes into place.
Initial context
Context always need to be an object:
formConfig.withContext({
x: 1,
y: 2
});Setting context dynamically
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]);Triggering validation
You can trigger validation of all fields on context changes:
formConfig.validateOnContextChange();Accessing context
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";
}
});Conditional validation
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.Schemainstances for each field during validation execution.
If a context property is named equal to a field property, the field property will be overriden inyup.Schemas 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.");
}
}
});Customization
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.
Using the raw field
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-like and aonChange-like prop valuehas the same type as the first parameter ofonChangehandler- it optionally has a
onBlur-like prop to indicate when the field is touched
*-like means it must not have the same name, but the same type. E.g. the value prop in react-datepicker is called selected.
For raw fields it's required to pass an initial value, otherwise it will be undefined.
const formConfig = createForm()({
// there is also a withOnChangeProp and withOnBlurProp option!
dateOfBirth: field.raw(new Date()).withValueProp("selected")
});
const MyForm = () => {
const { fields } = useFluentForm(formConfig);
};The type of fields object would look like this:
type FieldsType = {
dateOfBirth: {
selected: Date;
onChange: (newValue: Date) => void;
onBlur: () => void; // will just set the "touched" state to true
};
};Adding custom fields
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 theonBlurevent 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)
});Adding custom validator
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: object: 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
});Advanced Topics
- Form arrays
- More comming soon..
Form arrays
Form arrays are a rather complicated topic, since you need to be able to dynamically add/remove forms on demand. react-fluent-form comes with a build in solution by providing two additional hooks: useFluentFormArray and useFluentFormItem. Keep following image in mind for examples below:
Creating array config
Like for single forms you also need to create a config for form arrays but using createFormArray function instead. It returns similar config as createForm but with additional configuration properties which are only relevant for form arrays:
withInitialArray: specifiy inital values for the form arraywithKeyGenerator: items inside of the form array should be identifiable, which is why each form item has a unique key. On default the key will be generated by a key counter. To override this behaviour you can use this function to generate a key based on values.
NOTE:
withKeyGeneratorgenerates the key just once for each item directly when it's added.
const userRoleConfig = creatForm()({
username: field.text(),
role: field.select()
})
.withInitialArray([
{
id: 0,
username: "user0",
role: "admin"
},
{
id: 1,
username: "user1",
role: "manager"
}
])
.withKeyGenerator(item => item.id);Decalaring form array
With the created array config you have all you need to declare and initialize the form array.
const UserRoleFormArray = () => {
const { formArray, addForm } = useFluentFormArray(arrayConfig);
return (
<form>
{formArray.map(item => (
<UserRoleForm key={item.key} formItem={item} />
))}
<button onClick={addForm}>Add User</button>
</form>
);
};Declaring form item
Form items represent the actual forms inside the form array and can be created via useFluentFormItem hook.
Since react hooks can not be called inside of loops (like map in the example above), a new component for form items needs to be implemented.
useFluentFormItem returns the same properties as useFluentForm, but also following ones:
removeSelf: removes form item from the arraykey: value, which is used to identify form item
const UserRoleForm = ({ formItem }) => {
const { removeSelf, handleSubmit /* and more.. */ } = useFluentFormItem(
formItem
);
return (
<div>
<label>
User:
<input {...fields.user} />
</label>
<label>
Role:
<select {...fields.role.select}>
<option {...fields.role.option("admin")}>Admin</option>
<option {...fields.role.option("manager")}>Manager</option>
</select>
</label>
<button onClick={removeSelf}>Remove</button>
</div>
);
};Adding form items
useFluentFormArray returns a function - addForm - to add new form items. It optionally receives initialValues or a key key.
const { formArray, addForm } = useFluentFormArray(arrayConfig);
// will use initial values from config
// will use key generated by key counter or key generator if specified
addForm();
addForm({
initialValues: {
id: 2,
username: "user2",
role: "admin"
}
});
addForm({
key: 100
});Remove form items
useFluentFormArray returns a function - removeForm - to remove form items, which requires a key as parameter.
const { formArray, removeForm } = useFluentFormArray(arrayConfig);
removeForm(0);
removeForm(100);Reading form item state at top level
useFluentFormArray returns formStates, which is an array that stores the state of each form item. It can be accessed via index or via a helper function called getFormStateByKey.
NOTE: keys are generally not equal to the index!
const { formStates, getFormStateByKey } = useFluentFormArray(arrayConfig);
const firstFormItem = formState[0];
const formItemWithKeyHello = getFormStateByKey("hello");Resetting array values
With resetArray the from array can be resetted. It will either reset to the array passed to withInitialArray or to the array set by setInitialArray.
const userRoleConfig = creatForm()({
username: field.text(),
role: field.select()
}).withInitialArray([
{
id: 0,
username: "user0",
role: "admin"
}
]);
const UserRoleFormArray = () => {
const { formStates, resetArray, setInitialArray } = useFluentFormArray(
userRoleConfig
);
const handleSave = () => {
setInitialArray(formStates.map(state => state.values));
};
return (
<form>
{formArray.map(item => (
<UserRoleForm key={item.key} formItem={item} />
))}
<button onClick={resetArray}>Reset Form</button>
<button onClick={handleSave}>Save Form</button>
</form>
);
};Handling form array submission
Form array submission works just equal to single form submission.
const UserRoleFormArray = () => {
const { formStates, formArray, addForm, handleSubmit } = useFluentFormArray(
arrayConfig
);
const handleSubmitSuccess = () => {
console.log(formStates.map(state => state.values));
};
const handleSubmitFailure = () => {
console.log(formStates.map(state => state.errors));
};
return (
<form onSubmit={handleSubmit(handleSubmitSuccess, handleSubmitFailure)}>
{formArray.map(item => (
<UserRoleForm key={item.key} formItem={item} />
))}
<button onClick={addForm}>add user</button>
<button type="submit">Save</button>
</form>
);
};Not enough details? Check out the full API here. It's written for typescript!