Skip to content

Commit 23fa126

Browse files
committed
add dynamic enum Tutorial
Co-authored-by: Stefan Dirix <[email protected]> closes #270
1 parent 64cd763 commit 23fa126

File tree

9 files changed

+548
-1
lines changed

9 files changed

+548
-1
lines changed

Diff for: content/docs/tutorial/dynamic-enum.mdx

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
id: dynamic-enum
3+
title: Dynamic Renderers
4+
description: This tutorial describes how to create a dynamic enum
5+
---
6+
7+
import { WithRegionRenderer } from '../../../src/components/docs/tutorials/dynamic-enum';
8+
9+
10+
In this tutorial, you will learn how to handle dynamic data in React using [custom renderers](./custom-renderers), React Context, and the `useJsonForms` hook.
11+
This approach allows you to build flexible and interactive forms that adapt to user selections and API responses.
12+
13+
### Scenario
14+
15+
Imagine a form where users need to provide their location by selecting a country, a region and a city.
16+
The options for countries and regions are fetched from an API.
17+
The available regions depend on the selected country.
18+
To address those requirements, we'll create custom renderers for country and region.
19+
20+
<WithRegionRenderer />
21+
22+
23+
#### Schema
24+
25+
To begin, let's introduce the corresponding JSON schema.
26+
We have created an object with properties for country, region, and city.
27+
In our example, the schema also includes a property `x-url`, which specifies the entry point of the corresponding API.
28+
Both `country` and `region` have a property `x-endpoint`, indicating the endpoint from which the data should be fetched.
29+
Additionally, they have a field specifying which fields depend on the input.
30+
In the case of the `country` field, the `region` and `city` fields depend on it and will get reset, if the value of the `country` changes.
31+
The `city` field, in turn, is dependent on the `region` field.
32+
33+
```js
34+
{
35+
"type": "object",
36+
"x-url": "www.api.com",
37+
"properties": {
38+
"country": {
39+
"type": "string",
40+
"x-endpoint": "countries",
41+
"dependencies": ["region", "city"]
42+
},
43+
"region": {
44+
"type": "string",
45+
"x-endpoint": "regions",
46+
"dependencies": ["city"]
47+
},
48+
"city": {
49+
"type": "string"
50+
}
51+
}
52+
}
53+
```
54+
55+
56+
### Accessing Schema Data and Initialising the React Context
57+
58+
In this step we will access the data from the schema and initialize the react context.
59+
60+
#### Accessing the API URL from Schema
61+
62+
To access the URL defined from the schema we can simply access the `x-url` attribute.
63+
64+
```js
65+
const url = schema['x-url'];
66+
```
67+
68+
#### Initializing the React Context
69+
70+
Now that we have access to the API URL, we can use React Context to make this data available across our renderers.
71+
[React Context](https://react.dev/learn/passing-data-deeply-with-context) allows you to share data deep in the component tree to access data without needing to pass additional properties through the component hierarchy.
72+
To set up the React Context for your API service, create it in your application as follows:
73+
74+
```js
75+
export const APIContext = React.createContext(new API(url));
76+
77+
const App = () =>{
78+
79+
...
80+
<JsonForms/>
81+
}
82+
```
83+
84+
#### Accessing the API context
85+
86+
87+
Access the API service using the context:
88+
89+
```js
90+
const api = React.useContext(APIContext);
91+
```
92+
93+
Changing the context's value will trigger a re-render of components that use it.
94+
95+
96+
### The Country Renderer
97+
98+
The core of the country renderer is a dropdown, therefore we can reuse the MaterialEnumControl from the React Material renderer set.
99+
To reuse material renderers, the Unwrapped renderers must be used. (more information regarding reusing renderers can be seen [here](./custom-renderers#reusing-existing-controls))
100+
101+
```js
102+
import { Unwrapped, WithOptionLabel } from '@jsonforms/material-renderers';
103+
104+
const { MaterialEnumControl } = Unwrapped;
105+
106+
...
107+
108+
<MaterialEnumControl
109+
{...props}
110+
options = {options}
111+
handleChange = {handleChange}
112+
/>
113+
...
114+
```
115+
116+
With the `MaterialEnumControl`in place the main question remains how to set the `options` and the `handleChange` attribute.
117+
To determine the available options, we need to access the API.
118+
And to implement the `handleChange` function, we need access to the `dependent` field in the schema.
119+
120+
#### Accessing Schema Data
121+
122+
The `endpoint` and `dependent` fields can be obtained from the schema object provided to the custom renderer via JSON Forms.
123+
Since these fields are not part of the standard JSON schema type in JSON Forms, we must add them to the schema's interface and access them as follows:
124+
125+
```js
126+
type JsonSchemaWithDependenciesAndEndpoint = JsonSchema & {
127+
dependent: string[];
128+
endpoint: string;
129+
};
130+
const CountryControl = (
131+
props: ControlProps & OwnPropsOfEnum & WithOptionLabel & TranslateProps
132+
) => {
133+
...
134+
135+
const schema = props.schema as JsonSchemaWithDependenciesAndEndpoint;
136+
const endpoint = schema.endpoint;
137+
const dependent = schema.dependent
138+
...
139+
}
140+
```
141+
142+
#### Country Renderer Implementation
143+
144+
The country renderer uses the `APIContext` to query the API and fetch the available options.
145+
We utilize the `useEffect` hook to initialize the options.
146+
While waiting for the API response, we set the available options to empty and display a loading spinner.
147+
In the `handleChange` function, we set the new selected value and reset all dependent fields;
148+
When changing the country, both the region and city will be reset to `undefined`.
149+
150+
```js
151+
import { Unwrapped, WithOptionLabel } from '@jsonforms/material-renderers';
152+
153+
const { MaterialEnumControl } = Unwrapped;
154+
155+
type JsonSchemaWithDependenciesAndEndpoint = JsonSchema & {
156+
dependent: string[];
157+
endpoint: string;
158+
};
159+
160+
const CountryControl = (
161+
props: ControlProps & OwnPropsOfEnum & WithOptionLabel & TranslateProps
162+
) => {
163+
const { handleChange } = props;
164+
const [options, setOptions] = useState<string[]>([]);
165+
const api = React.useContext(APIContext);
166+
const schema = props.schema as JsonSchemaWithDependenciesAndEndpoint;
167+
168+
const endpoint = schema.endpoint;
169+
const dependent: string[] = schema.dependent ? schema.dependent : [];
170+
171+
useEffect(() => {
172+
api.get(endpoint).then((result) => {
173+
setOptions(result);
174+
});
175+
}, []);
176+
177+
if (options.length === 0) {
178+
return <CircularProgress />;
179+
}
180+
181+
return (
182+
<MaterialEnumControl
183+
{...props}
184+
handleChange={(path: string, value: any) => {
185+
handleChange(path, value);
186+
dependent.forEach((path) => {
187+
handleChange(path, undefined);
188+
});
189+
}}
190+
options={options.map((option) => {
191+
return { label: option, value: option };
192+
})}
193+
/>
194+
);
195+
};
196+
197+
export default withJsonFormsEnumProps(
198+
withTranslateProps(React.memo(CountryControl)),
199+
false
200+
);
201+
```
202+
203+
Now all that´s left to do is to [create a tester](./custom-renderers#2-create-a-tester) and [register](./custom-renderers#3-register-the-renderer) the new custom renderer in our application.
204+
205+
### The Region Renderer
206+
207+
The region renderer can be implemented similarly to the country renderer.
208+
It also accesses the API via the context and includes `endpoint` and `dependent` fields defined in its schema.
209+
However, the options, on the other hand, are also dependent on the selected country.
210+
JSON Forms provides the `useJsonForms` hook, allowing you to access form data and trigger component rerenders when the data changes.
211+
Let's use this hook in our region renderer to access the selected country:
212+
213+
```js
214+
import { Unwrapped, WithOptionLabel } from '@jsonforms/material-renderers';
215+
const { MaterialEnumControl } = Unwrapped;
216+
217+
type JsonSchemaWithDependenciesAndEndpont = JsonSchema & {
218+
dependent: string[];
219+
endpoint: string;
220+
};
221+
222+
const RegionControl = (
223+
props: ControlProps & OwnPropsOfEnum & WithOptionLabel & TranslateProps
224+
) => {
225+
const schema = props.schema as JsonSchemaWithDependenciesAndEndpont;
226+
const { handleChange } = props;
227+
const [options, setOptions] = useState<string[]>([]);
228+
const api = React.useContext(APIContext);
229+
const country = useJsonForms().core?.data.country;
230+
const [previousCountry, setPreviousCountry] = useState<String>();
231+
232+
const endpoint = schema.endpoint;
233+
const dependent: string[] = schema.dependent ? schema.dependent : [];
234+
235+
if (previousCountry !== country) {
236+
setOptions([]);
237+
setPreviousCountry(country);
238+
api.get(endpoint + '/' + country).then((result) => {
239+
setOptions(result);
240+
});
241+
}
242+
243+
if (options.length === 0 && country !== undefined) {
244+
return <CircularProgress />;
245+
}
246+
247+
return (
248+
<MaterialEnumControl
249+
{...props}
250+
handleChange={(path: string, value: any) => {
251+
handleChange(path, value);
252+
dependent.forEach((path) => {
253+
handleChange(path, undefined);
254+
});
255+
}}
256+
options={options.map((option) => {
257+
return { label: option, value: option };
258+
})}
259+
/>
260+
);
261+
};
262+
263+
export default withJsonFormsEnumProps(
264+
withTranslateProps(React.memo(RegionControl)),
265+
false
266+
);
267+
```
268+
Again we need to create a [create a tester](./custom-renderers#2-create-a-tester) and [register](./custom-renderers#3-register-the-renderer) the new custom renderer.

Diff for: docusaurus.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ module.exports = {
222222
to: '/docs/tutorial/custom-renderers',
223223
from: '/docs/custom-renderers',
224224
},
225+
{
226+
to: '/docs/tutorial/dynamic-enum',
227+
from: '/docs/dynamic-enum',
228+
},
225229
{
226230
to: '/docs/tutorial/multiple-forms',
227231
from: '/docs/multiple-forms',

Diff for: src/components/common/api.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export class API {
2+
url;
3+
4+
constructor(url) {
5+
this.url = url;
6+
}
7+
8+
async get(endpoint){
9+
switch (this.url + '/' + endpoint) {
10+
case 'www.api.com/regions/Germany':
11+
return germanStates;
12+
case 'www.api.com/regions/US':
13+
return usStates;
14+
case 'www.api.com/countries':
15+
return ['Germany', 'US'];
16+
default:
17+
return [];
18+
}
19+
}
20+
}
21+
22+
const germanStates = [
23+
'Berlin',
24+
'Bayern',
25+
'Niedersachsen',
26+
'Baden-Württemberg',
27+
'Rheinland-Pfalz',
28+
'Sachsen',
29+
'Thüringen',
30+
'Hessen',
31+
'Nordrhein-Westfalen',
32+
'Sachsen-Anhalt',
33+
'Brandenburg',
34+
'Mecklenburg-Vorpommern',
35+
'Hamburg',
36+
'Schleswig-Holstein',
37+
'Saarland',
38+
'Bremen',
39+
];
40+
41+
const usStates = [
42+
'Alabama',
43+
'Alaska',
44+
'Arizona',
45+
'Arkansas',
46+
'California',
47+
'Colorado',
48+
'Connecticut',
49+
'Delaware',
50+
'Florida',
51+
'Georgia',
52+
'Hawaii',
53+
'Idaho',
54+
'Illinois',
55+
'Indiana',
56+
'Iowa',
57+
'Kansas',
58+
'Kentucky',
59+
'Louisiana',
60+
'Maine',
61+
'Maryland',
62+
'Massachusetts',
63+
'Michigan',
64+
'Minnesota',
65+
'Mississippi',
66+
'Missouri',
67+
'Montana',
68+
'Nebraska',
69+
'Nevada',
70+
'New Hampshire',
71+
'New Jersey',
72+
'New Mexico',
73+
'New York',
74+
'North Carolina',
75+
'North Dakota',
76+
'Ohio',
77+
'Oklahoma',
78+
'Oregon',
79+
'Pennsylvania',
80+
'Rhode Island',
81+
'South Carolina',
82+
'South Dakota',
83+
'Tennessee',
84+
'Texas',
85+
'Utah',
86+
'Vermont',
87+
'Virginia',
88+
'Washington',
89+
'West Virginia',
90+
'Wisconsin',
91+
'Wyoming',
92+
];

0 commit comments

Comments
 (0)