@paros-ui/react-forms
v3.1.6
Published
Yet another React form library
Readme
@paros-ui/react-forms
On the web, forms exist everywhere. Here is yet another way to write React forms, vaguely inspired by mobx and redux-form / react-final-form
Building a Form
The first step is to initialize your form object. It will be in charge of state, validation, and event handling.
Once initialized, you can render the Form component and any number of Fields. Their name is specified by accessing properties of the Fields object
import React from "react";
import ReactForm from "@paros-ui/react-forms";
import LoginSchema from "./schemas/login";
class TestForm extends React.Component {
form = new ReactForm({
initialValues: {
rememberMe: true
}
});
onSubmit = data => {
console.log(data); //{ "username": "...", "password": "...", "rememberMe": true }
};
render() {
let { Form, Fields } = this.form;
return (
<Form onSubmit={this.onSubmit}>
{/* This is a field called "username" */}
<Fields.username component="input" type="email" placeholder="Email address" />
{/* This is a field called "password" */}
<Fields.password component="input" type="password" placeholder="Password" />
{/* This is a field called "rememberMe" */}
<Fields.rememberMe template="checkbox" component="input" />
<label for="rememberMe">Remember Me</label>
<button type="submit">Login</button>
</Form>
);
}
}The Form component will automatically intercept the submit event, perform validation (if configured), and call your onSubmit with the validated form data. If you use joi, it will provide the sanitized value from schema.validate(), otherwise it will be the raw form state.
Fields is a JavaScript proxy, like a dynamic object. When you reference a property of it, it'll generate and cache a wrapper component linked to that form, where the name of that field is the name of the property you are accessing. In the above example, it is caching three components and initializing the form state with three values, username, password and rememberMe. On blur, it will validate that field. On change, will reset the validation status for that field. It will also pass onBlur and onChange events through after it handles them internally.
Using a React Hook
If you prefer to use hooks, a useForm hook is available. It is just a wrapper around useState to manage a reference to ReactForm, making the hook functionally identical. All documentation will demonstrate with the ReactForm class for consistency, but can be implemented the same way using the useForm hook.
import React from "react";
import useForm from "@paros-ui/react-forms/hooks/useForm";
const onSubmit = (values, form) => {
// The second parameter gives you access to the ReactForm object
console.log(data); //{ "username": "...", "password": "...", "rememberMe": true }
};
let form = {
initialValues: {
rememberMe: true
}
};
function Component(props) {
let { Form, Fields } = useForm(form);
return (
<Form onSubmit={onSubmit}>
<Fields.username component="input" type="email" placeholder="Email address" />
<Fields.password component="input" type="password" placeholder="Password" />
<Fields.rememberMe template="checkbox" component="input" />
<label for="rememberMe">Remember Me</label>
<button type="submit">Login</button>
</Form>
);
}Default Values
Initial values can be passed into the ReactForm options object, or the <Form /> component.
The <Form /> component has higher priority, so if both are specified, it will defer to that.
let { Form } = new ReactForm({
initialValues: {
rememberMe: true
}
});
<Form initialValues={{ rememberMe: false }}>
...
</Form>Validation
Validation is supported via both joi object schemas and custom validator functions. The validator can be passed into the ReactForm options object, or like with the initialValues it can be passed as a prop to <Form />:
import joi from "joi";
const schema = joi.object({
username: joi.string().email({ tlds: { allow: false } }).required(),
password: joi.string().min(6).max(42).required()
});
let { Form } = new ReactForm({
validation: schema
});
<Form validation={schema}>
...
</Form>Alternatively, for fine-grain control over the validation process, you can pass a function as the validation prop:
let form = new ReactForm({
validation: (data, errors) => {
if(data.password !== "secret")
errors.password = "Invalid password";
}
});The validation function can even be asynchronous!
let form = new ReactForm({
validation: async(data, errors) => {
if(!(await checkToken(data.token)))
errors.token = "Invalid token";
}
});Validation on Blur
When a joi schema is used as validation or is not explicitly configured, when a field blur event fires, it will automatically validate the field. If a function validator is used, or validateOnBlur disabled either at the field or form level, it will disable this functionality.
let { Fields } = new ReactForm({
validateOnBlur: false // Disable at the form level
});
// Enable for this specific field
<Fields.username component="input" type="text" placeholder="Username" validateOnBlur />Simple Inputs
For most projects, you don't use a super complex text input for usernames, passwords, checkboxes, etc.
For this reason, there are a number of templates available. They will autofill certain properties when hooking into the component so you can write less code. Simply provide a template property with the desired template name, and it will take care of the rest.
Checkboxes
const Checkbox = ({ label, id, ...props }) => (
<div className="form-group">
<input type="checkbox" id={id} {...props} />
<label for={id}>{label}</label>
</div>
);
// Without using the template.
<Fields.rememberMe component={Checkbox} valueProp="checked" valueGetter="target.checked" label="Remember me" />
// With the `checkbox` template
<Fields.rememberMe template="checkbox" component={Checkbox} label="Remember me" />Adding Templates
When you want to create reusable form components, you don't need to import it everywhere. Instead, you can extend the form system's built-in templating.
import { addTemplates } from "@paros-ui/react-forms/templates";
addTemplates({
"password": ({ output, template, templateProps }) => {
if(!output.Component) {
output.Component = "input";
}
templateProps.type = "password";
template.forbidNull = true;
}
});
<Fields.fieldName template="password" placeholder="Enter a password" />Complex Inputs
Most of the time, the above will cover most use cases. However, sometimes your inputs are more complex than standard HTML inputs, and/or have more complex data. For this, we can use valueProp, valueGetter, and template to customize exactly how the form interfaces with your inputs.
valueProp (default: "value")
This will determine how the form will set the value property on the component. For checkboxes, valueProp needs to be "checked", because this is the property React expects for setting the value.
valueGetter (default: "target.value")
This determines how to extract the value from the onChange event. For checkboxes, valueGetter needs to be "target.checked". valueGetter can be a string or a synchronous function.
<Fields.fieldName component="input" type="checkbox" valueProp="checked" valueGetter="target.checked" />
<Fields.fieldName component="input" type="checkbox" valueProp="checked" valueGetter={e => e.target.checked} />ArrayFieldAdapter
Sometimes, your form utilizes an array of malleable data. For this, the ArrayFieldAdapter was created.
Give it a keyProp for your data, and a body to render, and it will automatically track changes without updating every item. Very performant!
The function you specify can have three parameters, item, props and index.
props has the value, error, onChange, etc. for that specific item. This gives you complete control over how you render each item.
const ComplexComponent = ({
item, value, onChange
}) => (
<div className="form-group">
<label>{item.name}</label>
<input type="phone"
className="form-control"
placeholder="Enter Phone Number"
value={value}
onChange={onChange} />
</div>
);
<Fields.phoneNumbers template="array" keyProp="id">
{(item, props) => (
<ComplexComponent item={item} {...props}>
)}
</Fields.phoneNumbers>
Complex Data Structures
Forms are rarely always a flat object. Utilities like the FormObject were created just for this.
FormObject
Nesting a form component like a Field inside of a FormObject will change the path to its value.
Take this data structure as an example:
{
"email": "",
"creditCard": {
"number": "",
"exp": "",
"cvv": ""
}
}You could implement the form with no additional effort by utilizing a FormObject wrapper:
<Fields.email template="text" type="email" />
<FormObject field="creditCard">
<Fields.number template="number" format="####-####-####-####" />
<Fields.exp template="number" format="##/##" />
<Fields.cvv template="number" format="###" />
</FormObject>
Monitoring, Field Dependencies and Reactions
Sometimes, complex form interactions between fields are necessary. Because of this, some advanced components and utilities have been implemented.
FieldWatcher
The most simple monitor is a FieldWatcher. Give it a field or fields prop and it'll render a child function when those field(s) change. The parameter given to the function is an object with the keys being each watched field.
import ReactForm from "@paros-ui/react-forms";
let { Form, Fields, FieldWatcher } = new ReactForm({ ... });
<FieldWatcher field="testFieldName">
{({ testFieldName }) => (
// Your JSX here
)}
</FieldWatcher>
<FieldWatcher fields={[ "fieldA", "fieldB" ]}>
{({ fieldA, fieldB }) => (
// Your JSX here
)}
</FieldWatcher>Monitors
Fields sometimes need to reference values from other fields in the same form. To accomplish this, simply add a monitors prop to a field, and a prop will be injected with its value. Additionally, the component will update when the monitored values change. monitors behaves like both field and fields from FieldWatcher, so you can specify either an array or a single field.
const EndDate = ({ startDate, value, onChange }) => (
<DatePicker minDate={startDate} value={value} onChange={onChange} />
);
const OptionsDropdown = ({ startDate, endDate, ...props }) => (
<AvailableOptionsDropdown startDate={startDate} endDate={endDate} {...props} />
);
<Fields.startDate template="datepicker" />
<Fields.endDate template="datepicker"
monitors="startDate"
component={EndDate} />
<Fields.options component={OptionsDropdnw} monitors={[ "startDate", "endDate" ]}>
fieldRequires and fieldExcludedBy
Sometimes there is a hard relationship between two fields. One requires another be populated, or one may entirely disable another. These props for fields enable such relationships.
fieldRequires will automatically disabled the inner component if the specified field is not populated.
fieldExcludedBy will automatically disabled the inner component if the specified field is populated.
Updating Form State
Using functions built into the form object, you can update and fetch information about the form at any time. Performing updates will trigger updates in affected fields.
Get/Set Values
let form = new ReactForm({
initialValues: {
rememberMe: false
}
});
// Fetching a single value
let rememberMe = form.value("rememberMe"); // false
// Fetching multiple values
let [ username, password ] = form.values("username", "password");
// Fetching all values
let { username, password } = form.values();
// Setting a single value
form.value("rememberMe", true);
// Setting multiple values
form.values({
rememberMe: true
});Get/Set Errors
let form = new ReactForm({
initialValues: {
rememberMe: false
}
});
// Fetching a single error
let rememberMe = form.error("rememberMe"); // null
// Fetching multiple errors
let [ username, password ] = form.errors("username", "password");
// Fetching all errors
let { username, password } = form.errors();
// Setting a single error
form.error("rememberMe", "Form error");
// Setting multiple errors
form.errors({
rememberMe: "Form error"
});Get Form State
let form = new ReactForm({
initialValues: {
rememberMe: false
}
});
form.value("rememberMe", true);
// Get all values
let values = form.values();
// Get all errors
let errors = form.errors();
// Or get all errors and values together
let { errors, values } = form.getState();
console.log(values.rememberMe, errors.rememberMe); // true, undefinedReset a Form to Initial Values
let form = new ReactForm({
initialValues: {
rememberMe: false
}
});
form.value("rememberMe", true);
form.resetForm(); //Resets both values and errors
form.resetValues(); //Resets values
form.resetErrors(); //Resets errors
let value = form.value("rememberMe"); // falseReset Form Errors
let form = new ReactForm({
initialValues: {
rememberMe: false
}
});(UNSAFE) Replacing Values / Errors
If you want to force-set values or errors, and not update the form in any way, it can be achieved with the following utility functions. It is not recommended to use.
let form = new ReactForm({
initialValues: {
username: "test_user",
password: "Passw0rd!"
}
});
form.replaceValues({
username: "",
password: ""
});
form.replaceErrors({
username: "Invalid username",
password: "Invalid password"
});If you pass true as a second parameter to replaceValues, it will also set the initialValues to the specified object
Manually Triggering Validation and Updating Components
The form object provides three utility functions for updating components or triggering validation.
let form = new ReactForm({
validation: (values, errors) => {
if(!values.username)
errors.username = "Username is required";
}
});
let { Form, Fields } = form;
<Form>
<Fields.username component="input" type="text" placeholder="Username" />
</Form>
form.validate(); // Will validate the entire form
form.validate("username"); // Will activate the validator and update the component
form.change("username"); // Will trigger the general change and update events, and update the field
form.update("username"); // Will trigger the field-specific change event, updating the fieldChanging the Validator at any Time
The validator can be changed dynamically by accessing the validation property of the form object.
let form = new ReactForm({
validation: () => null
});
// Future validation will use the new function / schema
form.validation = joi.object();Internal Events
ReactForm extends a facsimile of the Node.js EventEmitter, so you can subscribe to any internal event or manually emit events with the familiar .on(), .off(), .emit(), etc. functions.
Additionally, when initializing ReactForm, you can optionally specify a submit and/or change handler. You can do the same when rendering the <Form /> component:
let onChange = ({ field, value }) => {};
let onSubmit = values => {};
let { Form } = new ReactForm({
onChange: onChange,
onSubmit: onSubmit
});
<Form onChange={onChange} onSubmit={onSubmit}>
...
</Form>"change"
Change is fired when any field's value or error changes.
let form = new ReactForm();
// Using helper function
form.onChange(({ field, value }) => {
console.log(`Field "${field}" has value ${value}`);
});
// Using generic event subscription
form.on("change", ({ field, value }) => {
console.log(`Field "${field}" has value ${value}`);
});"update"
Update is fired when the form updates, from validation to field value change to form submit.
let form = new ReactForm();
// Using generic event subscription
form.on("update", ({ errors, values }) => {
...
});"submit"
Fired when the form passes validation and is ready to be processed externally. This event is used to fire any onSubmit provided.
let form = new ReactForm();
// Using helper function
form.onSubmit(values => {
// ...
});
// Using generic event subscription
form.on("submit", values => {
// ...
});"errors"
Fired when the form submits but fails validation.
let form = new ReactForm();
// Using generic event subscription
form.on("errors", errors => {
// ...
});"change-{field name}"
Fired immediately after the generic "change" event, but this allows you to listen to a specific field's updates.
let form = new ReactForm();
// Using generic event subscription
form.on("change-username", (value, error) => {
// ...
});<Fields /> Utilities and Props
The <Fields /> component will pass all props it receives to its component, including refs, but there are a few that will dictate behavior and not be passed through.
component
This determines how to actually render the field.
template
This will let you choose a template to be used internally by the field component builder to autofill some properties. The following templates come built-in, and can be extended/overridden:
"component"- Only adds a default for thevalueGetter, using the direct onChange value."text"- Defaults to<input type="text" />and forbids null values, forcing the form value to at least be"""textarea"- Same astexttemplate, just uses<textarea />instead."checkbox"- Defaults to<input type="checkbox">, sets default value to false, and sets defaults forvalueGetter/valueProp"select"- Sets some convenient defaults for areact-selectintegration. Requires extending to provide the default component."array"- Uses theArrayFieldAdapter
defaultValue
Behaves similarly to initialValues when initializing the form, but this allows you to set it on a per-field basis instead of specifying all at once with initialValues. Has priority over form-level initialValues setting.
validateOnBlur
Behaves similarly to validateOnBlur when initializing the form, but this allows you to toggle validation on a per-field basis. Has priority over form-level setting.
name
Since all fields are controlled components, you shouldn't need to care about the name property, and it'll by default be filtered out. If you absolutely must set it, use the componentProps property defined below.
componentProps
If for some reason you want to use some of these property names for the inner component, you can specify them in a componentProps object.
<Form /> Utilities and Props
<Form /> will pass refs to the internal <form /> component.
ReactForm Utilities and Props
debugUpdates (default: false)
This will log any update events to the console.
debugChanges (default: false)
This will log any change events to the console.
onError
This optional property expects a function, and will subscribe to the errors event that fires when a form submits but fails validation.
onUpdate
This optional property expects a function, and will subscribe to the update event that fires when a form changes state at all.
name (default: "form")
Currently unused except for debugUpdates and debugChanges to prefix update/change logging.
