Skip to content

Commit 7a344e3

Browse files
committed
add dynamic enum Tutorial
closes #270
1 parent 216087e commit 7a344e3

File tree

11 files changed

+577
-1
lines changed

11 files changed

+577
-1
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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 learned 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 tacks those requirements. we'll create custom renderers for the country and region.
19+
The example is also implemented in the [react-seed](https://github.com/eclipsesource/jsonforms-react-seed) app.
20+
21+
<WithRegionRenderer />
22+
23+
24+
#### Schema
25+
26+
To begin, let's introduce the corresponding JSON schema.
27+
We have created an object with properties for country, region, and city.
28+
In our example, the schema also includes a property `x-url`, which specifies the entry point of the corresponding API.
29+
Both `country` and `region` have a property `endpoint`, indicating the endpoint from which the data should be fetched.
30+
Additionally, they have a field specifying which fields depend on the input.
31+
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.
32+
The `city` field, in turn, is dependent on the `region` field.
33+
34+
```js
35+
{
36+
"type": "object",
37+
"x-url": "www.api.com",
38+
"properties": {
39+
"country": {
40+
"type": "string",
41+
"x-endpoint": "countries",
42+
"dependencies": ["region", "city"]
43+
},
44+
"region": {
45+
"type": "string",
46+
"x-endpoint": "regions",
47+
"dependencies": ["city"]
48+
},
49+
"city": {
50+
"type": "string"
51+
}
52+
}
53+
}
54+
```
55+
56+
57+
### Accessing Schema Data and Initialising the React Context
58+
59+
In this step we will access the data from the schema and initialize the react context.
60+
61+
#### Accessing the API URL from Schema
62+
63+
To access the URL defined from the schema we can simply access the `x-url` attribute.
64+
65+
```js
66+
const url = schema['x-url'];
67+
```
68+
69+
#### Initializing the React Context
70+
71+
Now that we have access to the API URL, we can use React Context to make this data available across our renderers.
72+
React Context allows you to share data globally within your application, enabling components deep in the component tree to access data without needing to pass properties through all parent elements.
73+
To set up the React Context for your API service, create it in your application as follows:
74+
75+
```js
76+
export const APIContext = React.createContext(new API(url));
77+
78+
const App = () =>{
79+
80+
...
81+
<JsonForms/>
82+
}
83+
```
84+
85+
### The Country Renderer
86+
87+
The core of the country renderer is a dropdown, we can reuse the MaterialEnumControl from the material-renderer set.
88+
To reuse material renderers, the Unwrapped renderers must be used. (more information regarding reusing renderers can be seen [here](./custom-renderers#reusing-existing-controls))
89+
90+
```js
91+
import { Unwrapped, WithOptionLabel } from '@jsonforms/material-renderers';
92+
93+
const { MaterialEnumControl } = Unwrapped;
94+
95+
...
96+
97+
<MaterialEnumControl
98+
{...props}
99+
options = {options}
100+
handleChange = {handleChange}
101+
/>
102+
...
103+
```
104+
105+
With the `MaterialEnumControl`in place the main question remains how to set the `options` and the `handleChange` attribute.
106+
To determine the available options, we need to access the API.
107+
And to implement the `handleChange` function, we need access to the `dependened` field in the schema.
108+
109+
#### Accessing the API context
110+
111+
112+
Access the API service using the context:
113+
114+
```js
115+
const api = React.useContext(APIContext);
116+
```
117+
118+
Changing the context's value will trigger a re-render of components that use it, making it a powerful tool for managing dynamic data.
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+
dependened: string[];
128+
endpoint: string;
129+
};
130+
export const Country = (
131+
props: ControlProps & OwnPropsOfEnum & WithOptionLabel & TranslateProps
132+
) => {
133+
...
134+
135+
const schema = props.schema as JsonSchemaWithDependenciesAndEndpoint;
136+
const endponit = schema.endpoint;
137+
const dependened = schema.dependened
138+
...
139+
}
140+
```
141+
142+
#### The Country Renderer
143+
144+
The country renderer uses the `APIContext` to query the API and fetch the available options.
145+
We utilize the `useEffect` hook to reload new options, if API changes.
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+
dependened: string[];
157+
endpoint: string;
158+
};
159+
160+
export const Country = (
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 endponit = schema.endpoint;
169+
const dependened: string[] = schema.dependened ? schema.dependened : [];
170+
171+
useEffect(() => {
172+
setOptions([]);
173+
api.get(endponit).then((result) => {
174+
setOptions(result);
175+
});
176+
}, [api, endponit]);
177+
178+
if (options.length === 0) {
179+
return <CircularProgress />;
180+
}
181+
182+
return (
183+
<MaterialEnumControl
184+
{...props}
185+
handleChange={(path: string, value: any) => {
186+
handleChange(path, value);
187+
dependened.forEach((path) => {
188+
handleChange(path, undefined);
189+
});
190+
}}
191+
options={options.map((option) => {
192+
return { label: option, value: option };
193+
})}
194+
/>
195+
);
196+
};
197+
```
198+
199+
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.
200+
201+
### The Region Renderer
202+
203+
The region renderer can be implemented similarly to the country renderer.
204+
It also accesses the API via the context and includes `endpoint` and `dependent` fields defined in its schema.
205+
However, the options, on the other hand, are also dependent on the selected country.
206+
JSON Forms provides the `useJsonForms` hook, allowing you to access form data and trigger component rerenders when the data changes.
207+
Let's use this hook in our region renderer to access the selected country:
208+
209+
```js
210+
import { Unwrapped, WithOptionLabel } from '@jsonforms/material-renderers';
211+
const { MaterialEnumControl } = Unwrapped;
212+
213+
type JsonSchemaWithDependenciesAndEndpont = JsonSchema & {
214+
dependened: string[];
215+
endpoint: string;
216+
};
217+
218+
export const Region = (
219+
props: ControlProps & OwnPropsOfEnum & WithOptionLabel & TranslateProps
220+
) => {
221+
const schema = props.schema as JsonSchemaWithDependenciesAndEndpont;
222+
const { handleChange } = props;
223+
const [options, setOptions] = useState<string[]>([]);
224+
const api = React.useContext(APIContext);
225+
const country = useJsonForms().core?.data.country;
226+
const [previousCountry, setPreviousCountry] = useState<String>();
227+
228+
const endponit = schema.endpoint;
229+
const dependened: string[] = schema.dependened ? schema.dependened : [];
230+
231+
if (previousCountry !== country) {
232+
setOptions([]);
233+
setPreviousCountry(country);
234+
api.get(endponit + '/' + country).then((result) => {
235+
setOptions(result);
236+
});
237+
}
238+
239+
if (options.length === 0 && country !== undefined) {
240+
return <CircularProgress />;
241+
}
242+
243+
return (
244+
<MaterialEnumControl
245+
{...props}
246+
handleChange={(path: string, value: any) => {
247+
handleChange(path, value);
248+
dependened.forEach((path) => {
249+
handleChange(path, undefined);
250+
});
251+
}}
252+
options={options.map((option) => {
253+
return { label: option, value: option };
254+
})}
255+
/>
256+
);
257+
};
258+
```
259+
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.
260+
A running example of the scenario can also be seen at the [react-seed](https://github.com/eclipsesource/jsonforms-react-seed) app.

docusaurus.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ module.exports = {
225225
to: '/docs/tutorial/custom-renderers',
226226
from: '/docs/custom-renderers',
227227
},
228+
{
229+
to: '/docs/tutorial/dynamic-enum',
230+
from: '/docs/dynamic-enum',
231+
},
228232
{
229233
to: '/docs/tutorial/multiple-forms',
230234
from: '/docs/multiple-forms',

src/components/common/api.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export class API {
2+
private url: string;
3+
4+
constructor(url: string) {
5+
this.url = url;
6+
}
7+
8+
async get(endpoint: string): Promise<string[]> {
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)