Skip to content

Commit 64cd763

Browse files
LukasBollsdirix
andauthored
Add FAQ: update a field depending on another field (#281)
closes #269 Co-authored-by: Stefan Dirix <sdirix@eclipsesource.com>
1 parent 4219520 commit 64cd763

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

content/faq/faq.mdx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ description: Frequently Asked Questions
55
slug: /
66
---
77

8+
import { DependentFieldExample } from '../../src/components/faq/faq';
9+
810
## How can I listen to form changes in the React standalone component?
911

1012
When using JSON Forms within your react app, at some point you'll need to access the current form data.
@@ -182,6 +184,160 @@ Now default values within the schema file can be used:
182184
"default": "Max"
183185
}
184186
```
187+
188+
## How to update a field dependent on another field in JSON Forms?
189+
190+
Consider a carwash application offering various services and calculating the resulting price.
191+
The schema includes a multiselect field called services and a price attribute, with the price field set as readonly.
192+
193+
<DependentFieldExample/>
194+
195+
There are three approaches to update a field in JSON Forms based on the value of another field:
196+
Utilizeing the JSONF Forms middleware, using the onChange method or create a custom render.
197+
198+
#### Approach 1: JSON Forms middleware
199+
200+
We can utilize the JSON Forms middleware to compute and set the price.
201+
JSON Forms utilizes the reducer pattern and various actions to update its state.
202+
The middleware intercepts the call to the JSON Forms reducers and calls your custom code instead.
203+
For detailed insights into the JSON Forms middleware, the reducer pattern, and JSON Forms actions, refer to the documentation [here](/docs/middleware).
204+
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.
205+
We initially invoke JSON Forms default reducer to update the data and identify any errors.
206+
Subsequently, we adjust the price fields based on the selected services and update the state with the newly calculated data.
207+
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.
208+
209+
```js
210+
import { INIT, UPDATE_CORE, UPDATE_DATA } from '@jsonforms/core'
211+
212+
...
213+
const middleware = useCallback((state, action, defaultReducer) => {
214+
const newState = defaultReducer(state, action);
215+
switch (action.type) {
216+
case INIT:
217+
case UPDATE_CORE:
218+
case UPDATE_DATA: {
219+
if (newState.data.services.length * 15 !== newState.data.price) {
220+
newState.data.price = newState.data.services.length * 15;
221+
}
222+
return newState;
223+
}
224+
default:
225+
return newState;
226+
}
227+
});
228+
229+
...
230+
231+
<JsonForms
232+
data={data}
233+
schema={schema}
234+
renderers={materialRenderers}
235+
middleware={middleware}
236+
/>
237+
```
238+
239+
#### Approach 2: Implementing functionality in the onChange method of JSON Forms
240+
241+
In this approach, you can implement the logic in the onChange method of JSON Forms.
242+
The onChange method triggers whenever a user changes the form data.
243+
244+
:::info Note
245+
246+
It's more performant to use the middleware approach, but this is a good approach for JSON Forms before version v3.2.0
247+
248+
:::
249+
250+
```js
251+
export const CarWash = () => {
252+
const [formData, setFormData] = useState(inputData);
253+
254+
const onChange = ({ data, _errors }) => {
255+
const price = data.services.length*15;
256+
if (data.price === price){
257+
setFormData(data);
258+
} else {
259+
setFormData({...data, price: price})
260+
}
261+
}
262+
263+
return (
264+
<JsonForms
265+
data={formData}
266+
schema={schema}
267+
renderers={renderers}
268+
onChange={onChange}
269+
/>
270+
);
271+
};
272+
```
273+
274+
In the onChange method, the price is calculated based on the selected services.
275+
If the calculated price matches the existing value, the data remains unchanged.
276+
However, if the price differs, a new data object is created with the updated price and passed to JSON Forms.
277+
278+
:::caution
279+
280+
It's important to only create a new object when necessary to avoid infinite loops.
281+
282+
:::
283+
284+
#### Approach 3: Create a custom renderer
285+
286+
Another possibility is to implement a custom renderer.
287+
A custom renderer allows you to define and use your rendering logic instead of relying on the default renderers provided by JSON Forms.
288+
For a comprehensive guide and additional information on creating custom renderers, refer to our [Custom Renderers Tutorial](/docs/tutorial/custom-renderers).
289+
290+
In this tutorial, we'll create the ServicesRenderer designed to display offered services.
291+
While the default renderer will still handle displaying the price, we'll calculate and set the price within our custom renderer.
292+
293+
The basis of the ServiceRenderer is the MaterialEnumArrayRenderer, which is the default renderer of JSON Forms for this data type.
294+
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.
295+
296+
```js
297+
import { Unwrapped } from '@jsonforms/material-renderers';
298+
const { MaterialEnumArrayRenderer } = Unwrapped;
299+
300+
const ServiceRenderer: React.FC<
301+
ControlProps & OwnPropsOfEnum & DispatchPropsOfMultiEnumControl
302+
> = (props) => {
303+
return (
304+
<MaterialEnumArrayRenderer
305+
{...props}
306+
addItem={(path, value) => {
307+
const currentLength = props.data ? props.data.length : 0;
308+
props.handleChange('price', (currentLength + 1) * 15);
309+
props.addItem(path, value);
310+
}}
311+
removeItem={(path, value) => {
312+
if (props.removeItem) {
313+
const currentLength = props.data ? props.data.length : 0;
314+
props.removeItem(path, value);
315+
props.handleChange('price', (currentLength - 1) * 15);
316+
}
317+
}}
318+
/>
319+
);
320+
};
321+
export default withJsonFormsMultiEnumProps(ServiceRenderer);
322+
```
323+
To apply the custom renderer in JSON Forms, add the renderer and a tester to JSON Forms, as shown below:
324+
```js
325+
326+
const renderers = [
327+
...materialRenderers,
328+
//register custom renderer
329+
{ tester: rankWith(4,scopeEndsWith('rating')), renderer: ServiceRenderer }
330+
]
331+
332+
<JsonForms
333+
schema={schema}
334+
uischema={uischema}
335+
data={data}
336+
renderers={renderers}
337+
/>
338+
```
339+
340+
185341
## How can I use multipleOf with values smaller than 1?
186342

187343
JSON Forms uses AJV for the validation of data.

src/components/faq/faq.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { Demo } from '../common/Demo';
3+
import { materialRenderers } from '@jsonforms/material-renderers';
4+
5+
const schema = {
6+
type: 'object',
7+
properties: {
8+
services: {
9+
type: 'array',
10+
uniqueItems: true,
11+
items: {
12+
oneOf: [{ const: 'Wash (15$)' }, { const: 'Polish (15$)' }, { const: 'Interior (15$)' }],
13+
},
14+
},
15+
price: {
16+
type: 'number',
17+
readOnly: true,
18+
},
19+
},
20+
};
21+
22+
const inputData = {
23+
services: ['Wash (15$)', 'Polish (15$)'],
24+
};
25+
26+
27+
export const DependentFieldExample = () => {
28+
const [formData, setFormData] = useState(inputData);
29+
30+
const onChange = ({ data, _errors }) => {
31+
const price = data.services.length*15;
32+
if (data.price === price){
33+
setFormData(data);
34+
}else{
35+
setFormData({...data, price: price})
36+
}
37+
}
38+
39+
return (
40+
<Demo
41+
data={formData}
42+
schema={schema}
43+
renderers={[...materialRenderers]}
44+
onChange={onChange}
45+
/>
46+
);
47+
};
48+
49+
export default DependentFieldExample;

0 commit comments

Comments
 (0)