-
Notifications
You must be signed in to change notification settings - Fork 54
UIForm V3
Today, @talend/ui repository has a form package. It proposes 1 api, based on json schema format. This has been done to fill tcomp/tacokit needs. It allows the backend to control the forms with json documents.
Json is really limited and static, and it introduces limitations in the features. To overcome some of them, and to comply to tcomp/takokit lifecycle, we introduced triggers. It gives an external entry to do custom actions (change of schema, dynamic/async validation, change of suggestions, …). But the implementation is really hard to maintain and synchronize. For example validation has 3 entry points:
- (backend) Json Schema: static validation on single elements
- (frontend) Component custom validation props: complex single element validation
- (frontend/backend) Trigger: static/async validation on global/single element(s)
The result is that
- Frontend developers struggle to create frontend-only forms, writing tons of json instead of components, without being able to fulfill complex cases
- Backend developers struggle too, trying to implement complex validations with json and UIForm limited features.
We need to open this implementation, to let frontend developers write their custom/complex use cases code (component and javascript). For the backend developers, the json validation format must be flexible enough too express complex use cases.
The json schema is too deep in our current implementation, making it very hard to extract. The plan would be to use an existing library to make forms in react, but this time, without json schema in mind.
We would make widgets to fit the form library and to apply the Talend style. Developers could use the library and the common widgets to build frontend only forms.
For backend controlled forms, we would write an extra layer on top of the library, converting the json/ui schema into javascript code to build the form, based on the library and the widgets. This schema layer
won't interfere with the forms implementation itself, and we wil be able to modify the json format to fit our needs.
(TODO)
@talend/react-forms has a custom implementation. The json schema is present at every level of the implementation, from top level form to widget internal code. It’s very hard to extract the schema part to a top level to allow developers to use the widgets as components. A better and less costly solution would be to base the implementation on an existing form library. The developers would be able to use the library (or the wrapper on it), and backend users would still be able to use our custom schema layer on top of it.
1.0 | Github stars | Contributors | Issues | Winner | |
---|---|---|---|---|---|
Formik | Jul 2018 | 19.1k | 268 | 376 | ✅ |
React-hook-forms | Mar 2019 | 4.2k | 38 | 1 |
Maintained by | Issues | Response time | Well tested | Winner | |
---|---|---|---|---|---|
Formik | @jaredpalmer | 376 | From 3 days to no response | Unit tests (react-testing-library) | |
React-hook-forms | @bluebill1049 | 1 | Same day | Unit tests (@testing-library/react-hooks) + E2E tests (Cypress) | ✅ |
Weekly dl | Size | Dependencies | Winner | |
---|---|---|---|---|
Formik | 307k | 12.6kB | 9 | |
React-hook-forms | 30k | 5.2kB | 0 | ✅ |
Comment | Winner | |
---|---|---|
Formik | It produces a lot of rerenders. Everytime a field changes, it rerenders | |
React-hook-forms | It optimises the renders | ✅ |
Comment | Winner | |
---|---|---|
Formik | Great api documentation, migration pages, simple guides (arrays, validation, ...) | ✅ |
React-hook-forms | Great api documentation, advanced guides (custom widget, accessibility, wizard, ...) | ✅ |
Formik | React-hook-forms | |
---|---|---|
Github | ✅ | |
Maintenance | ✅ | |
Bundle | ✅ | |
Performance | ✅ | |
Documentation | ✅ | ✅ |
Formik
Formik was created by @jaredpalmer who is very active in the frontend community. The library is very popular. It has tons of contributors and more than 19k stars.
React-hook-form
React-hook-form is quite young, it was created in march 2019 by @bluebill1049, but has risen quite fast, having now more than 4k stars. It is a very light library, with no dependencies. It has a documentation for advanced cases such as custom widget, accessibility, or wizard.
The goal is to compare the developer experience and the possibilities between the 2 libraries and our current implementation. We will focus on the frontend-only part.
Basis
Scenario | Story | Test |
---|---|---|
B1 | as a developer, I want to create a simple form with email and password, with submit function | Go |
B2 | as a developer, I want to add a custom widget | Go |
Validation
Scenario | Story | Test |
---|---|---|
V1 | as a developer, I want to validate the email pattern and password requirement | Go |
V2 | as a developer, I want to async validate the email, checking it’s availability | Go |
V3 | as a developer, I want to do a complex validation, with values dependencies | Go |
Advanced
Scenario | Story | Test |
---|---|---|
A1 | as a developer, I want to show/hide a new field depending on another value | Go |
A2 | as a developer, I want to set a field required depending on another value | Go |
{
"jsonSchema": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"email": {
"type": "string"
}
}
},
"password": { "type": "string" }
}
},
"uiSchema": [
{
"key": "user.email",
"title": "Email"
},
{
"key": "password",
"title": "Password"
}
],
"properties": {
"user": {
"email": "[email protected]"
}
}
}
import { UIForm } from "@talend/react-forms/lib/UIForm";
import data from "./schema.json";
function ExampleForm() {
const onSubmit = (event, data) => {
console.log(data);
};
return <UIForm data={data} onSubmit={onSubmit} />;
}
On a simple form, it stays simple. You have 3 parts:
- json schema: defines the model
- ui schema: defines the widgets
- properties: defines the initial values
To do a nested structure, we describe it in the json schema. Then, in the ui schema, we refer to nested field via dot notation.
import React from "react";
import { Formik, Form, Field } from "formik";
function ExampleForm() {
const onSubmit = data => {
console.log(data);
};
return (
<Formik
initialValues={{ user: { email: "[email protected]" } }}
onSubmit={onSubmit}
>
{args => {
const { isSubmitting } = args;
return (
<Form>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field id="email" type="email" name="user.email" />
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<Field id="password" type="password" name="password" />
</div>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
);
}}
</Formik>
);
}
Formik offers to write javascript instead of json. Each form and field components come from the library.
Compared to the schema
- the structure is tied to the field names
- the default values are passed at Formik component level
There are some extra features to manage some status. In the example above, the isSubmitting
flag allows to avoid submitting twice the form.
import React from "react";
import useForm from "react-hook-form";
function App() {
const { register, handleSubmit } = useForm();
const onSubmit = data => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="user.email"
defaultValue="[email protected]"
ref={register}
/>
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<input id="password" type="password" name="password" ref={register} />
</div>
<button type="submit">Submit</button>
</form>
);
}
React-hook-form uses native elements (form, input, ...), allowing to write classical html/jsx. To plug the elements to the form system we pass a function via ref. The default values are passed directly to the inputs, instead of a big object on root element like in Formik.
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Almost no js code, simple json description. | ✅ | 😎 |
Formik | Components from the lib. | ✅ | 😎 |
React-hook-form | Native elements to wire with hook. | ✅ | 😎 |
{
"jsonSchema": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"email": {
"type": "string"
}
}
},
"password": { "type": "string" },
+ "extraField": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
},
"uiSchema": [
{
"key": "user.email",
"title": "Email"
},
{
"key": "password",
"title": "Password"
},
+ {
+ "key": "extraField",
+ "title": "Extra field",
+ "items": [
+ {
+ "key": "extraField.key",
+ "title": "Key"
+ },
+ {
+ "key": "extraField.value",
+ "title": "Private value",
+ "type": "password"
+ }
+ ],
+ "widget": "keyValue"
+ }
],
"properties": {
"user": {
"email": "[email protected]"
}
}
}
import { UIForm } from '@talend/react-forms/lib/UIForm';
import data from './schema.json';
+import KeyValue from './KeyValue.component';
function ExampleForm() {
const onSubmit = (event, data) => {
console.log(data);
};
+ const widgets = {
+ keyValue: KeyValue,
+ };
return (
<UIForm
data={data}
onSubmit={onSubmit}
+ widgets={widgets}
/>
);
}
UIForm comes with a set of widgets, loaded by default. This allows to not pass the widgets everytime, but it makes the bundle heavier even if you don't use them.
The widget configuration is still in the json/ui schema. We have to know the widget format, and describe the widget inputs. Sometimes we have a lot of nestings, and multi levels nestings, and it becomes hard to understand and maintain the code.
import { Field } from "formik";
function KeyValue({ field }, id, label, keyProps, valueProps) {
const { name } = field;
const { label: keyLabel, ...restKeyProps } = keyProps;
const { label: valueLabel, ...restValueProps } = valueProps;
const keyName = `${name}.key`;
const valueName = `${name}.value`;
const keyId = `${id}.key`;
const valueId = `${id}.value`;
return (
<fieldset>
<legend>{label}</legend>
<div className="form-group">
<label htmlFor={keyId}>{keyLabel}</label>
<Field id={keyId} type="text" name={keyName} {...restKeyProps} />
</div>
<div className="form-group">
<label htmlFor="email">{valueLabel}</label>
<Field
id={valueId}
type="text"
name={`${name}.key`}
{...restValueProps}
/>
</div>
</fieldset>
);
}
The widget use the same Field
component from Formik for its inputs.
import React from 'react';
import { Formik, Form, Field } from 'formik';
import KeyValue from './KeyValue.component';
function ExampleForm() {
const onSubmit = data => {
console.log(data);
};
return (
<Formik
initialValues={{ user: { email: "[email protected]" } }}
onSubmit={onSubmit}
>
{args => {
const { isSubmitting } = args;
return (
<Form>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field id="email" type="email" name="user.email" />
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<Field id="password" type="password" name="password" />
</div>
<div className="form-group">
<label htmlFor="email">Extra field</label>
<Field id="password" type="password" name="password" />
</div>
+ <Field
+ id="extra-field"
+ name="keyValue"
+ component={KeyValue}
+ label="Extra field"
+ keyProps={{ label: 'Key' }}
+ valueProps={{ label: 'Private value', type: 'password' }}
+ />
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
);
}}
</Formik>
);
}
We use the Field
component, passing the component to render as component
props and name that the widget will manage. The rest of props are just passed to the widget.
import { useEffect } from "react";
function KeyValue({
id,
label,
name,
keyProps,
valueProps,
register,
unregister
}) {
const { label: keyLabel, ...restKeyProps } = keyProps;
const { label: valueLabel, ...restValueProps } = valueProps;
const keyId = `${id}.key`;
const valueId = `${id}.value`;
const keyName = `${name}.key`;
const valueName = `${name}.value`;
useEffect(() => {
return () => {
unregister(keyName);
unregister(valueName);
}, []);
return (
<fieldset>
<legend>{label}</legend>
<div className="form-group">
<label htmlFor={keyId}>{keyLabel}</label>
<input id={keyId} type="text" name={keyName} ref={register} {...restKeyProps} />
</div>
<div className="form-group">
<label htmlFor="email">{valueLabel}</label>
<input
id={valueId}
type="text"
name={valueName}
ref={register}
{...restValueProps}
/>
</div>
</fieldset>
);
}
In the widget, we use native elements again, passing react-hook-form register()
function. But we have to unregister them at widget unmount.
import React from "react";
import useForm from "react-hook-form";
+import KeyValue from "./KeyValue.component";
function App() {
- const { register, handleSubmit } = useForm();
+ const { register, unregister, handleSubmit } = useForm();
const onSubmit = data => {
console.log(data);
};
return (
<form onSubmit={onSubmit} noValidate>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="user.email"
defaultValue="[email protected]"
ref={register}
/>
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<input id="password" type="password" name="password" ref={register} />
</div>
+ <KeyValue
+ id="extra-field"
+ name="keyValue"
+ label="Extra field"
+ keyProps={{ label: 'Key' }}
+ valueProps={{ label: 'Private value', type: 'password' }}
+ register={register}
+ unregister={unregister}
+ />
<button type="submit">Submit</button>
</form>
);
}
In the form, we use the component directly in jsx, passing the register/unregister
functions.
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Pass widgets as form props to register them. UIForm comes with a set of widgets which is good in term of usability, but bad in term of bundle size. | ✅ | 😎(simple structure), 😭 (complex structure) |
Formik | Import widget and use it as Field . |
✅ | 😎 |
React-hook-form | Import widget and use it as component. Wet need to manage unregister. | ✅ | 😎 |
Let's try to add simple validations. By simple, we mean on single field, with common basic use cases.
- email is required and must have the right pattern
- password is required
{
"jsonSchema": {
"type": "object",
"title": "Comment",
"properties": {
"email": {
"type": "string",
+ "pattern": "^\\S+@\\S+$"
},
"password": {
"type": "string"
}
},
+ "required": ["email", "password"]
},
"uiSchema": [
{
"key": "email",
"title": "Email",
+ "validationMessage": "Please enter a valid email address, e.g. [email protected]"
},
{
"key": "password",
"title": "Password",
"type": "password"
}
],
"properties": {}
}
Once again, UIForm comes with a bunch of default features.
- to set a field as required, you just have to gather them in a
required
array. - to validate a pattern, you just have to pass the pattern in the json schema.
There is still a limitation, as you can't pass a custom message for a specific validation. For the email, we customize the validation message, but it will display for the required AND the pattern errors.
If you need to do simple custom validations (not covered by the json schema), you can pass a customValidation
prop to UIForm
import { UIForm } from '@talend/react-forms/lib/UIForm';
import data from './schema.json';
function ExampleForm() {
const onSubmit = (event, data) => {
console.log(data);
};
+ function validate(schema, value, allValues) {
+ if (schema.key === "password" && value === "lol") {
+ return "Password must not be lol";
+ }
+ }
return (
<UIForm
data={data}
onSubmit={onSubmit}
+ customValidation={validate}
/>
);
}
Validation is done
- for the field when user finishes to edit (blur)
- for the entire form (each individual check) on submit
import React from "react";
import { Formik, Form, Field } from "formik";
+function validateEmail(value) {
+ if (!value) {
+ return "The email is required";
+ } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
+ return "Invalid email address";
+ }
+}
+function validatePassword(value) {
+ if (!value) {
+ return "The password is required";
+ }
+}
function ExampleForm() {
const onSubmit = data => {
console.log(data);
};
return (
<Formik
initialValues={{ user: { email: "[email protected]" } }}
onSubmit={onSubmit}
+ validateOnChange={false} // this will trigger validation on blur only
>
{args => {
- const { isSubmitting } = args;
+ const { errors, isSubmitting } = args;
return (
<Form>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field
id="email"
type="email"
name="email"
+ aria-describedby="email-errors"
+ aria-invalid={errors['email']}
+ validate={validateEmail}
+ required
/>
+ <ErrorMessage name="email" component="div" id="email-errors" />
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<Field
id="password"
type="password"
name="password"
+ aria-describedby="password-errors"
+ aria-invalid={errors['password']}
+ validate={validatePassword}
+ required
/>
+ <ErrorMessage name="password" component="div" id="password-errors" />
</div>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
);
}}
</Formik>
);
}
In Formik, you have to implement the validation functions, returning the error message. It has the advantage to give full control to the dev and we're going to see that it's a game changer in more complex validations.
But we can imagine exposing a set of validation functions for common simple checks.
Validation is done
- for the field when user on blur
- for the entire form (each individual check) on submit
import React from "react";
import useForm from "react-hook-form";
+function validateEmailPattern(value) {
+ return (
+ value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)
+ ) || "Invalid email address";
+}
function App() {
- const { register, handleSubmit } = useForm();
+ const { errors, register, handleSubmit } = useForm({ mode: "onBlur" }); // validation on blur
const onSubmit = data => {
console.log(data);
};
return (
<form onSubmit={onSubmit} noValidate>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
- ref={register}
+ ref={register({ required: "Email is required", validate: validateEmailPattern })}
+ aria-describedby="email-errors"
+ aria-invalid={errors['email']}
+ required
/>
+ <div id="email-errors">{errors['email']}</div>
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<input
id="password"
type="password"
name="password"
- ref={register}
+ ref={register({ required: true, validate: validateEmailPattern })}
+ aria-describedby="password-errors"
+ aria-invalid={errors['password']}
+ required
/>
+ <div id="password-errors">{errors['password']}</div>
</div>
<button type="submit">Submit</button>
</form>
);
}
React-hook-form waits for validation in the register function. I comes with some default validation like required, and can be composed with custom validations.
Validation is done
- for the field when user on blur
- for the entire form (each individual check) on submit
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Configuration in json with some natively supported cases. If a use case is not covered, we can implement props.customValidation . |
✅ | 😎 |
Formik | We must implement each validation in js, and set the functions on the fields. | ✅ | 😎 |
React-hook-form | We must implement each validation, and pass them to register() , some cases such as required are natively supported. |
✅ | 😎 |
{
"jsonSchema": {
"type": "object",
"properties": {
"email": {
"type": "string"
}
}
},
"uiSchema": [
{
"key": "email",
"title": "Email",
+ "triggers": [{ "type": "validation" }]
}
],
"properties": {}
}
We add a trigger definition that will be passed as argument to an onTrigger()
function.
import { UIForm } from '@talend/react-forms/lib/UIForm';
import data from './schema.json';
function ExampleForm() {
const onSubmit = (event, data) => {
console.log(data);
};
+ const onTrigger = (event, { schema, value, trigger }) => {
+ if(trigger.type === 'validation' && schema.key === 'email') {
+ return asyncValidation(schema, value)
+ .then(() => ({
+ errors(oldErrors) {
+ const newErrors = {...oldErrors};
+ delete newErrors[schema.key];
+ return newErrors;
+ })
+ )
+ .catch(error => ({
+ errors:(oldErrors)=> ({ ...oldErrors, [schema.key]: error }))
+ });
+ }
+ };
return (
<UIForm
data={data}
onSubmit={onSubmit}
+ onTrigger={onTrigger}
/>
);
}
Triggers allows to do async validation, returning an error modifier as a Promise result. There are some negative points to highlight
- during the call, the form is not blocked, submission is possible
- we have to implement the error add and removal from the UIForms errors
- it makes third entry point for validation (json configuration, customValidation prop, trigger)
The developer experience is quite painful.
function validateEmail(value) {
return asyncValidation(value).catch(error => ({ value: error.message }));
}
<Field
id="email"
type="email"
name="email"
aria-describedby="email-errors"
aria-invalid={errors["email"]}
validate={validateEmail}
required
/>
Formik uses the same validation prop as synchronous validation. But instead of returning the errors, you can return a promise, resolving possible errors.
Submit is suspended until the validation succeeds.
function validateEmail(value) {
return asyncValidation(value).catch(error => error.message);
}
<input
id="email"
type="email"
name="email"
ref={register({ required: true, validate: validateEmail })}
aria-describedby="email-errors"
aria-invalid={errors["email"]}
required
/>
Same comments as Formik, React-hook-form uses the same validation mecanism, but using promises.
Submit is suspended until the validation succeeds.
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Incomplete, workaround via trigger, but it hurts DX. | ❌ | 😭 |
Formik | Same validation mecanism as synchronous validation, via promise. | ✅ | 😎 |
React-hook-form | Same validation mecanism as synchronous validation, via promise. | ✅ | 😎 |
For now, we only did isolated field validation. Let's try to make a non-isolated one. In the example, we have a password field and an extra field that must not be the password value.
What is tricky here is that
- you change the extra field value, it must check it against the password value
- you change the password, it must update the extra field error
We cannot use
- json schema: no built-in validation of this type
- custom validation: it has access to the whole values, but it can only act on an isolated field error. Changing the password value won't update the extra field error.
What is left is the trigger.
{
"jsonSchema": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"extra": {
"type": "string"
}
}
},
"uiSchema": [
{
"key": "email",
"title": "Email",
},
{
"key": "password",
"title": "Password",
+ "triggers": [{ "type": "checkPwdAndExtraDiff" }]
},
{
"key": "extra",
"title": "Extra field",
+ "triggers": [{ "type": "checkPwdAndExtraDiff" }]
},
],
"properties": {}
}
import { UIForm } from '@talend/react-forms/lib/UIForm';
import data from './schema.json';
function ExampleForm() {
const onSubmit = (event, data) => {
console.log(data);
};
const onTrigger = (event, { schema, value, trigger, properties }) => {
if(trigger.type === 'checkPwdAndExtraDiff') {
const {email, extra} = properties;
if(email && extra && email === extra) {
return {
errors: (oldErrors) => {
const newErrors = {
...oldErrors,
extra: 'It must not be your password'
};
return newErrors;
}
}
} else {
return {
errors: (oldErrors) => {
const newErrors = {...oldErrors};
delete newErrors.extra;
return newErrors;
})
}
}
}
};
return (
<UIForm
data={data}
onSubmit={onSubmit}
onTrigger={onTrigger}
/>
);
}
But there are some limitations again. If the extra field is required, and has the "required" error displayed, changing the password will remove this "required" error.
We don't have any way to solve that today.
With formik we have 2 possible validations:
- single field validation (function you pass on the Field component)
- a form global validation (function you pass on the Form component)
Both are triggered on submit and on each field validation.
const validate = values => {
const errors = {};
const { email, extra } = properties;
if (email && extra && email === extra) {
errors.extra = "It must not be your password";
}
return errors;
};
This validation function doesn't need to clean the previous error.
function ExampleForm() {
return (
<Formik
initialValues={{ user: { email: "[email protected]" } }}
onSubmit={onSubmit}
validate={validate}
>
{args => {
/* ... */
}}
</Formik>
);
}
The validations (global and from fields) are merged. So no conflict, when there is a global error, it takes priority, when fixed, we still have the field validation that will display.
const { setError, getValues } = useForm();
function isPwdValueEquals() {
const { password, extra } = getValues();
return mail && extra && email === extra;
}
function validatePassword(pwd) {
if (isPwdValueEquals()) {
// set the error
setError(
"extra", // field
"checkPwdValueDiff", // error id
"Must not be the same as password" // error message
);
} else {
// clean the errors
setError("extra", "checkPwdValueDiff", true);
}
}
function validateExtra(value) {
if (isPwdValueEquals()) {
return "Must not be the same as password";
}
}
import React from "react";
import useForm from "react-hook-form";
function App() {
// ...
return (
<form onSubmit={onSubmit} noValidate>
{/* ... */}
<div className="form-group">
<label htmlFor="email">Password</label>
<input
id="password"
type="password"
name="password"
ref={register({ validate: validatePassword })}
aria-describedby="password-errors"
aria-invalid={errors["password"]}
required
/>
<div id="password-errors">{errors["password"]}</div>
</div>
<div className="form-group">
<label htmlFor="extra">Extra field</label>
<input
id="extra"
type="text"
name="extra"
ref={register({
required: true,
validate: { checkPwdValueDiff: validateExtra } // attach the validation message to "checkPwdValueDiff" error id
})}
aria-describedby="extra-errors"
aria-invalid={errors["extra"]}
required
/>
<div id="extra-errors">{errors["password"]}</div>
</div>
<button type="submit">Submit</button>
</form>
);
}
React-hook-form allows to manage the case. But if we want to interact with a field error, from another field, we need to
- double the validation (here from password and from extra)
- clean the error (here clean extra error from password)
There is no constrains in what validation can be done, but we need to synchronize multiple validations.
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Possible with the triggers, but some big limitations for not so complicated use cases. | ❌ | 😭 |
Formik | Single and global validation points, well managed/merged | ✅ | 😎 |
React-hook-form | The hook provides the functions to get values and set errors, we are free to do complex validations | ✅ | 😐 |
{
"jsonSchema": {
"type": "object",
"title": "Comment",
"properties": {
"email": {
"type": "string"
},
"private": {
"type": "boolean"
},
"password": {
"type": "string"
}
}
},
"uiSchema": [
{
"key": "email",
"title": "Email"
},
{
"key": "private",
"title": "Private",
"widget": "toggle"
},
{
"key": "password",
"title": "Password",
"type": "password",
"condition": {
"===": [{ "var": "private" }, true]
}
}
],
"properties": {}
}
Conditional rendering is a built-in feature in UIForm.
The password ui schema has condition
property. It uses jsonLogic to express conditions based on other values.
function ExampleForm() {
return (
<Formik>
{({ values }) => (
<Form>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field id="email" type="email" name="email" />
</div>
<div className="form-group">
<Field id="private" type="checkbox" name="private" />
<label htmlFor="private">Private</label>
</div>
{values.private && (
<div className="form-group">
<label htmlFor="email">Password</label>
<Field id="password" type="password" name="password" />
</div>
)}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
)}
</Formik>
);
}
The forms values are available as parameters of the child function, and are updated everytime there is a change. We just have to add a condition on the component rendering in jsx.
function App() {
const { register, watch } = useForm();
const privateValue = watch("private");
return (
<form>
<div className="form-group">
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" ref={register} />
</div>
<div className="form-group">
<input id="private" type="checkbox" name="private" ref={register} />
<label htmlFor="private">Private</label>
</div>
{privateValue && (
<div className="form-group">
<label htmlFor="email">Password</label>
<input id="password" type="password" name="password" ref={register} />
</div>
)}
<button type="submit">Submit</button>
</form>
);
}
React-hook-form optimises the renders. So on input value change, the form is not necessarily re-rendered. To make sure the checkbox value triggers a re-render, we have to watch
it. And we display the password depending on its value, in jsx.
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Easy with jsonLogic syntax | ✅ | 😎 |
Formik | Easy, just an if | ✅ | 😎 |
React-hook-form | Easy, an if | ✅ | 😎 |
Let's try a case that requires to change the form structurally. We want to have a non-required input, that turns required when another field is true.
There is no built-in way to change a field configuration. We need to change its schema. This complicates a lot the code
- save the json/ui schema in a state management
- get the json/ui schema from the state management to pass it to the UIForm component
- on value change, if it's a condition to change a field, update the json/ui schema, and replace it in state manager
It involves to have a lot heavier infrastructure.
There is a workaround that is more used: conditional rendering. The idea is to condition parts of the form, and render them depending on values. In the not so complicated example, we would have 2 passwords ui configuration (one required, the other not), that are conditioned depending on the toggle value to display one or the other. But this results in a complicated json, with sometimes a lot of duplications.
<Formik onSubmit={onSubmit}>
{args => {
const { values } = args;
function validate(values) {
if (values.private && !values.password) {
return { password: "Password is required" };
}
}
return (
<Form validate={validate}>
<div className="form-group">
<label htmlFor="email">Email</label>
<Field id="email" type="email" name="email" />
</div>
<div className="form-group">
<label htmlFor="private">Password</label>
<Field id="private" name="private" component={Toggle} />
</div>
<div className="form-group">
<label htmlFor="email">Password</label>
<Field
id="password"
type="password"
name="password"
required={values.private}
/>
</div>
<button type="submit">Submit</button>
</Form>
);
}}
</Formik>
Formik gives all the values, you can add a condition to the required
props to pass to the input field.
function App() {
const { register, handleSubmit, watch } = useForm();
const privateValue = watch("private");
function validatePassword(password) {
if (privateValue && !password) {
return "Password is required";
}
}
return (
<form onSubmit={onSubmit} noValidate>
<div className="form-group">
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" ref={register} />
</div>
<Toggle id="private" name="private" />
<div className="form-group">
<label htmlFor="email">Password</label>
<input
id="password"
type="password"
name="password"
ref={register({ validate: validatePassword })}
required={privateValue}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
As Formik, we can simply add a condition on the required props. To get the value, we have to watch it.
Lib | Summary | Eligible | Complexity |
---|---|---|---|
UIForm | Not possible, workaround via conditional rendering, but it becomes messy | ❌ | 😭 |
Formik | Simply add a condition on required props and implement validation | ✅ | 😎 |
React-hook-form | Simply add a condition on required props and implement validation | ✅ | 😎 |
Possibility
UIForms | Formik | React-hook-form | |
---|---|---|---|
Simple forms | ✅ | ✅ | ✅ |
Custom widget | ❌ for complex structure, ✅ for simple structure | ✅ | ✅ |
Simple validation | ✅ (with the unique error message limitation) | ✅ | ✅ |
Async validation | ❌ | ✅ | ✅ |
Complex validation | ❌ | ✅ | ✅ |
Conditional rendering | ✅ | ✅ | ✅ |
Conditional require | ❌ | ✅ | ✅ |
Complexity
UIForms | Formik | React-hook-form | |
---|---|---|---|
Simple forms | 😎 | 😎 | 😎 |
Custom widget | 😎 | 😎 | 😎 |
Simple validation | 😎 | 😎 | 😎 |
Async validation | 😭 | 😎 | 😎 |
Complex validation | 😭 | 😎 | 😐 |
Conditional rendering | 😎 | 😎 | 😎 |
Conditional require | 😭 | 😎 | 😎 |