FAQ
How can I listen to form changes in the React standalone component?
When using JSON Forms within your react app, at some point you'll need to access the current form data.
For this purpose you can use the onChange
prop, which returns the form data and possible validation errors provided by AJV.
To get you started, we built two simple examples for you.
The first example showcases a class component:
import React from 'react';
import { JsonForms } from '@jsonforms/react';
import { person } from '@jsonforms/examples';
import { materialRenderers } from '@jsonforms/material-renderers';
const schema = person.schema;
const uischema = person.uischema;
const initialData = person.data;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: initialData
};
}
render() {
return (
<div className="App">
<JsonForms
schema={schema}
uischema={uischema}
data={initialData}
renderers={materialRenderers}
onChange={({ errors, data }) => this.setState({data})}
/>
</div>
);
}
}
export default App;
The second example uses the functional component approach.
Therefore we use the useState
hook, which provides the data (first parameter) and a function to update the data (second parameter).
import React, { useState } from 'react';
import { JsonForms } from '@jsonforms/react';
import { person } from '@jsonforms/examples';
import { materialRenderers } from '@jsonforms/material-renderers';
const schema = person.schema;
const uischema = person.uischema;
const initialData = person.data;
function App() {
const [data, setData] = useState(initialData);
return (
<div className="App">
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={materialRenderers}
onChange={({ errors, data }) => setData(data)}
/>
</div>
);
}
export default App;
How can I integrate a custom renderer?
Providing a custom renderer gives you full control of the rendered output, enabling you to integrate any custom behavior or pre-built component you'd like. To integrate a custom renderer follow these 3 steps:
- Create a custom renderer
- Create a tester (which tells JSON Forms for which UI schema element this renderer shall be used)
- Register the custom renderer and the tester
Using JSON Forms standalone we register the custom renderer directly on the JSON Forms component:
import { rankWith, scopeEndsWith } from '@jsonforms/core';
import { Rating } from './Rating';
const RatingControl = ({ data, handleChange, path }) => (
<Rating
value={data}
onClick={ev => handleChange(path, Number(ev.value))}
/>
);
// renderers should be static or memoized
const renderers = [
...materialRenderers,
//register custom renderer
{ tester: rankWith(3,scopeEndsWith('rating')), renderer: RatingControl }
]
...
<JsonForms
schema={schema}
uischema={uischema}
data={stateData}
renderers={renderers}
/>
For more information about custom renderers, have a look here.
How can I minimize re-rendering?
JSON Forms uses React.memo
to avoid any unnecessary re-rendering.
Therefore props should in general be stable, for example by memoizing them.
There are three exceptions: JSON Forms can handle "onChange" and "middleware" changes, for example to support anonymous functions. Also new "i18n" objects will be ignored as long as their properties are stable.
JSON Forms is able to recognize data
objects it emitted via onChange
, so handing them over is safe too.
However whenever a different data
object is handed over, JSON Forms will revalidate and rerender.
const [data, setData] = useState(initialData);
<JsonForms
data={data}
onChange={({ data }) => setData(data)}
/>
In this scenario, onChange will set the data in the parent component.
Afterwards the data is passed on to JSON Forms, but JSON Forms will not revalidate and render again, since the data prop is the object emitted by the onChange method.
On the other hand an anti pattern can be seen when looking at the data
prop in the following example:
const [data, setData] = useState(initialData);
<JsonForms
data={data}
onChange={({ data }) => setData({ ...data })}
/>
Updating the state with a new object in the onChange
function leads to a new render cycle, in which JSON Forms will revalidate the data and retrigger the onChange method, resulting in an endless loop.
Only provide a new object to JSON Forms if necessary, for example if the data was modified outside of JSON Forms.
How can I use default values within JSON Forms?
We use Ajv for handling JSON Schema's default values. To enable the creation of default values, you need to create a custom Ajv instance and hand it over to JSON Forms.
The following example shows the code for React, however the process is the same for Angular and Vue.
import { createAjv } from '@jsonforms/core';
const handleDefaultsAjv = createAjv({useDefaults: true});
<JsonForms
data={data}
schema={schema}
uischema={uischema}
renderers={renderers}
cells={cells}
onChange={({ errors, data }) => setData(data)}
ajv={handleDefaultsAjv}
/>
Now default values within the schema file can be used:
{
"type": "string",
"title": "Name",
"default": "Max"
}
How to update a field dependent on another field in JSON Forms?
Consider a carwash application offering various services and calculating the resulting price. The schema includes a multiselect field called services and a price attribute, with the price field set as readonly.
- Demo
- Schema
- UI Schema
- Data
{"type": "object","properties": {"services": {"type": "array","uniqueItems": true,"items": {"oneOf": [{"const": "Wash (15$)"},{"const": "Polish (15$)"},{"const": "Interior (15$)"}]}},"price": {"type": "number","readOnly": true}}}
{}
{"services": ["Wash (15$)","Polish (15$)"]}
There are three approaches to update a field in JSON Forms based on the value of another field: Utilizeing the JSONF Forms middleware, using the onChange method or create a custom render.
Approach 1: JSON Forms middleware
We can utilize the JSON Forms middleware to compute and set the price.
JSON Forms utilizes the reducer pattern and various actions to update its state.
The middleware intercepts the call to the JSON Forms reducers and calls your custom code instead.
For detailed insights into the JSON Forms middleware, the reducer pattern, and JSON Forms actions, refer to the documentation here.
In this scenario, we want to customize the behavior associated with the UPDATE_DATA
action, which is triggered when the form's data is changed by the user.
We initially invoke JSON Forms default reducer to update the data and identify any errors.
Subsequently, we adjust the price fields based on the selected services and update the state with the newly calculated data.
We additionally override the INIT
and UPDATE_CORE
actions, in case the data prop passed to JSON Forms doesn't have the correct price set yet.
import { INIT, UPDATE_CORE, UPDATE_DATA } from '@jsonforms/core'
...
const middleware = useCallback((state, action, defaultReducer) => {
const newState = defaultReducer(state, action);
switch (action.type) {
case INIT:
case UPDATE_CORE:
case UPDATE_DATA: {
if (newState.data.services.length * 15 !== newState.data.price) {
newState.data.price = newState.data.services.length * 15;
}
return newState;
}
default:
return newState;
}
});
...
<JsonForms
data={data}
schema={schema}
renderers={materialRenderers}
middleware={middleware}
/>
Approach 2: Implementing functionality in the onChange method of JSON Forms
In this approach, you can implement the logic in the onChange method of JSON Forms. The onChange method triggers whenever a user changes the form data.
It's more performant to use the middleware approach, but this is a good approach for JSON Forms before version v3.2.0
export const CarWash = () => {
const [formData, setFormData] = useState(inputData);
const onChange = ({ data, _errors }) => {
const price = data.services.length*15;
if (data.price === price){
setFormData(data);
} else {
setFormData({...data, price: price})
}
}
return (
<JsonForms
data={formData}
schema={schema}
renderers={renderers}
onChange={onChange}
/>
);
};
In the onChange method, the price is calculated based on the selected services. If the calculated price matches the existing value, the data remains unchanged. However, if the price differs, a new data object is created with the updated price and passed to JSON Forms.
It's important to only create a new object when necessary to avoid infinite loops.
Approach 3: Create a custom renderer
Another possibility is to implement a custom renderer. A custom renderer allows you to define and use your rendering logic instead of relying on the default renderers provided by JSON Forms. For a comprehensive guide and additional information on creating custom renderers, refer to our Custom Renderers Tutorial.
In this tutorial, we'll create the ServicesRenderer designed to display offered services. While the default renderer will still handle displaying the price, we'll calculate and set the price within our custom renderer.
The basis of the ServiceRenderer is the MaterialEnumArrayRenderer, which is the default renderer of JSON Forms for this data type. JSON Forms renderers usually use the handleChange, addItem, or removeItem functions to set data, so we adjust these functions to not only add or remove items but also dynamically calculate and set the current price based on the selected services.
import { Unwrapped } from '@jsonforms/material-renderers';
const { MaterialEnumArrayRenderer } = Unwrapped;
const ServiceRenderer: React.FC<
ControlProps & OwnPropsOfEnum & DispatchPropsOfMultiEnumControl
> = (props) => {
return (
<MaterialEnumArrayRenderer
{...props}
addItem={(path, value) => {
const currentLength = props.data ? props.data.length : 0;
props.handleChange('price', (currentLength + 1) * 15);
props.addItem(path, value);
}}
removeItem={(path, value) => {
if (props.removeItem) {
const currentLength = props.data ? props.data.length : 0;
props.removeItem(path, value);
props.handleChange('price', (currentLength - 1) * 15);
}
}}
/>
);
};
export default withJsonFormsMultiEnumProps(ServiceRenderer);
To apply the custom renderer in JSON Forms, add the renderer and a tester to JSON Forms, as shown below:
const renderers = [
...materialRenderers,
//register custom renderer
{ tester: rankWith(4,scopeEndsWith('rating')), renderer: ServiceRenderer }
]
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={renderers}
/>
How can I use multipleOf with values smaller than 1?
JSON Forms uses AJV for the validation of data.
By default, the multipleOf
option of AJV only works with integers.
If you are using fractional dividers, you also need to set the multipleOfPrecision
option in AJV and pass your AJV instance to JSON Forms.
To create the AJV instance with suitable options, you can call the createAjv(options)
method exported by JSON Forms Core.
AJV suggests setting the multipleOfPrecision
to some positive integer N using this formula:
Math.abs(Math.round(multipleOf) - multipleOf) < 1e-N
In the following example multipleOf
is set to 0.1
and multipleOfPrecision
is set to 2
.
const schema = {
price: {
type: 'number',
multipleOf: 0.1
},
};
import { createAjv } from '@jsonforms/core';
import Ajv from 'ajv';
const [ajvInstance] = useState<Ajv>(createAjv({ multipleOfPrecision: 2 }));
<JsonForms
data={data}
schema={schema}
uischema={uischema}
renderers={renderers}
cells={cells}
onChange={({ errors, data }) => setData(data)}
ajv={ajvInstance}
/>