Today, I'd like to share with you one of the items from my tools-belt, which I'm successfully using for years now. It is simply a react component. It is a form. But not just a form, it is a form that allows anyone independently of their React or HTML knowledge to build a sophisticated feature-rich form based on any arbitrary expected data in a consistent manner.
Behold, the React JSON Schema Form, or simply RJSF. Originally started and built as an Open Source project by the Mozilla team. Evolved into a separate independent project.
Out of the box, RJSF provides us with rich customization of different form levels, extensibility, and data validation. We will talk about each aspect separately.
Configuration
JSON Schema
The end goal of any web form is to capture expected user input. The RJSF will capture the data as a JSON object. Before capturing expected data we need to define how the data will look like. The rest RJSF will do for us. To define and annotate the data we will use another JSON object. Bear with me here...
We will be defining the shape (or the schema) of the JSON object (the data) with another JSON object. The JSON object that defines the schema for another JSON object is called -drumroll- JSON Schema and follows the convention described in the JSON Schema standard.
To make things clear, we have two JSON objects so far. One representing the data we are interested in, another representing the schema of the data we are interested in. The last one will help RJSF to decide which input to set for each data attribute.
A while ago in one of my previous articles I've touched base on the JSON Schema.
I'm not going to repeat myself, I'll just distill to what I think is the most valuable aspect of it.
JSON Schema allows us to capture changing data and keep it meaningful. Think of arbitrary address data in the international application. Address differs from country to country, but the ultimate value doesn't. It represents a point in the world that is described with different notations. Hence even though the address format in the USA, Spain, Australia, or China is absolutely different, the ultimate value -from an application perspective- is the same-- a point on the Globe. It well might be employee home address, parcel destination or anything else and notation does not change this fact.
So if we want to capture, let's say, the first and last name and telephone number of a person. The expected data JSON object will look like
{
"firstName": "Chuck",
"lastName": "Norris",
"telephone": "123 456 789"
}
And the JSON Schema object to define the shape of the data object above will look like
{
"title": "A person information",
"description": "A simple person data.",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"title": "First name",
},
"lastName": {
"type": "string",
"title": "Last name"
},
"telephone": {
"type": "string",
"title": "Telephone",
"minLength": 10
}
}
}
Something to keep in mind.
JSON Schema is following a permissive model. Meaning out of the box everything is allowed. The more details you specify, the more restrictions you put in place. So it is worth sometimes religiously define the expected data.
This is the bare minimum we need to start. Let's look at how the JSON Schema from the above will look like as a form. Just before let's also look at the code...
import Form from "@rjsf/core";
// ...
<Form schema={schema}>
<div />
</Form>
// ...
Yup, that's it, now let's check out the form itself
UI Schema
Out of the box, the RJSF makes a judgment on how to render one field or another. Using JSON Schema you primarily control what to render, but using UI Schema you can control how to render.
UI Schema is yet another JSON that follows the tree structure of the JSON data, hence form. It has quite some stuff out of the box.
You can be as granular as picking a color for a particular input or as generic as defining a template for all fields for a string
type.
Let's try to do something with our demo form and say disable the first name and add help text for the phone number.
{
"firstName": {
"ui:disabled": true
},
"telephone": {
"ui:help": "The phone number that can be used to contact you"
}
}
Let's tweak our component a bit
import Form from "@rjsf/core";
// ...
<Form
schema={schema}
uiSchema={uiSchema}
>
<div />
</Form>
// ...
And here is the final look
Nice and easy. There's a lot of built-in configurations that are ready to be used, but if nothing suits your needs, you can build your own...
Customization
The API allows to specify your own custom widget and field components:
- A widget represents a HTML tag for the user to enter data, eg. input, select, etc.
- A field usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels.
Another way to think of it is field includes label and other stuff around, while widget only the interaction component or simply input.
For the sake of example let's create a simple text widget that will make the input red and put a dash sign (-) after every character.
To keep things light and simple let's imagine that the whole form will be a single red field. The JSON Schema will look as follows
const schema = {
title: "Mad Field",
type: "string"
};
Forgot to say that widgets are just components, that will be mounted in and will receive a standard set of props
. No limits, just your imagination ;)
const MadTextWidget = (props) => {
return (
<input type="text"
style={{backgroundColor: "red"}}
className="custom"
value={props.value}
required={props.required}
onChange={(event) => props.onChange(event.target.value + " - ")} />
);
};
The next step is to register the widget so that we can use it in the UI Schema
const widgets = {
madTextWidget: MadTextWidget
}
Finally, we can define the UI Schema
const uiSchema = {
"ui:widget": "madTextWidget"
};
And the full code with the RJSF
const schema = {
title: "Mad Field",
type: "string"
};
const MadTextWidget = (props) => {
return (
<input type="text"
style={{backgroundColor: "red"}}
className="custom"
value={props.value}
required={props.required}
onChange={(event) => props.onChange(event.target.value + " - ")} />
);
};
const widgets = {
madTextWidget: MadTextWidget
}
const uiSchema = {
"ui:widget": "madTextWidget"
};
ReactDOM.render((
<Form schema={schema}
uiSchema={uiSchema}
widgets={widgets}
/>
), document.getElementById("app"));
It will look like this
Here, try it yourself. The field will be pretty similar but will have a wider impact area so to speak. As been said the field will include labels and everything around the input itself.
Custom templates allows you to re-define the layout for certain data types (simple field, array or object) on the form level.
Finally, you can build your own Theme which will contain all your custom widgets, fields, template other properties available for a Form
component.
Validation
As was mentioned before the JSON Schema defines the shape of the JSON data that we hope to capture with the form. JSON Schema allows us to define the shape fairly precisely. We can tune the definition beyond the expected type, e.g. we can define a length of the string or an email regexp or a top boundary for a numeric value and so forth.
Check out this example
const Form = JSONSchemaForm.default;
const schema = {
type: "string",
minLength: 5
};
const formData = "Hi";
ReactDOM.render((
<Form schema={schema} formData={formData} liveValidate />
), document.getElementById("app"));
Will end up looking like this
Of course, we can re-define messages, configure when, where, and how to show the error messages.
Out of the box our data will be validated against the JSON Schema using the (Ajv) A JSON Schema validator library. However, if we want to, we can implement our own custom validation process.
Dependencies
Dependencies allow us to add some action to the form. We can dynamically change form depending on the user input. Basically, we can request extra information depending on what the user enters.
Before we will get into dependencies, we need to get ourselves familiar with dynamic schema permutation. Don't worry, it is easier than it sounds. We just need to know what four key-words mean
-
allOf
: Must be valid against all of the subschemas -
anyOf
: Must be valid against any of the subschemas -
oneOf
: Must be valid against exactly one of the subschemas -
not
: Must not be valid against the given schema ___
Although dependencies have been removed in the latest JSON Schema standard versions, RJSF still supports it. Hence you can use it, there are no plans for it to be removed so far.
Property dependencies
We may define that if one piece of the data has been filled, the other piece becomes mandatory. There are two ways to define this sort of relationship: unidirectional and bidirectional. Unidirectional as you might guess from the name will work in one direction. Bidirectional will work in both, so no matter which piece of data you fill in, the other will be required as well.
Let's try to use bidirectional dependency to define address in the shape of coordinates. The dependency will state that if one of the coordinates has been filled, the other one has to be filled in either. But if none is filled, none is required.
{
"type": "object",
"title": "Longitude and Latitude Values",
"description": "A geographical coordinate.",
"properties": {
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"dependencies": {
"latitude": [
"longitude"
],
"longitude": [
"latitude"
]
},
"additionalProperties": false
}
See lines 17 to 24. That's all there is to it, really. Once we will pass this schema to the form, we will see the following (watch for an asterisk (*) near the label, it indicates whether the field is mandatory or not).
Schema dependencies
This one is more entertaining, we can actually control visibility through the dependencies. Let's follow up on the previous example and for the sake of the example show longitude only if latitude is filled in.
{
"type": "object",
"title": "Longitude and Latitude Values",
"description": "A geographical coordinate.",
"properties": {
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90
}
},
"dependencies": {
"latitude": {
"properties": {
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180
}
}
}
},
"additionalProperties": false
}
No code changes are required, just a small dependency configuration tweak (lines 12 to 22).
Dynamic schema dependencies
So far so good, pretty straightforward. We input the data, we change the expected data requirements. But we can go one step further and have multiple requirements. Not only based on whether the data is presented or not but on the value of presented data.
Once again, no code, only JSON Schema modification
{
"title": "How many inputs do you need?",
"type": "object",
"properties": {
"How many inputs do you need?": {
"type": "string",
"enum": [
"None",
"One",
"Two"
],
"default": "None"
}
},
"required": [
"How many inputs do you need?"
],
"dependencies": {
"How many inputs do you need?": {
"oneOf": [
{
"properties": {
"How many inputs do you need?": {
"enum": [
"None"
]
}
}
},
{
"properties": {
"How many inputs do you need?": {
"enum": [
"One"
]
},
"First input": {
"type": "number"
}
}
},
{
"properties": {
"How many inputs do you need?": {
"enum": [
"Two"
]
},
"First input": {
"type": "number"
},
"Second input": {
"type": "number"
}
}
}
]
}
}
}
Bottom line
Even though we went through some major concepts and features, we are far away from covering everything that RJSF empowers us to do.
I'd encourage you to check out official documentation for more insights and examples, GitHub repository for undocumented goodies and live playground to get your hands dirty. Finally, worth mentioning that the Open Source community keeps things going, so look outside these resources, there are quite a few good things over there.
RJSF is a ridiculously powerful thing if you need to customize and capture meaningful data. Enjoy!
Top comments (0)