Skip to content

Commit db62b92

Browse files
authored
Merge pull request #80 from headwirecom/feature/router-and-localstorage
Add URL router and local storage persistence
2 parents d8b8902 + 0b56c8c commit db62b92

File tree

8 files changed

+280
-105
lines changed

8 files changed

+280
-105
lines changed

packages/example/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
"@react-spectrum/table": "^3.0.0-alpha.7",
1212
"@react-spectrum/tabs": "3.0.0-alpha.2",
1313
"@spectrum-icons/workflow": "^3.2.0",
14+
"@types/react-router-dom": "^5.1.6",
1415
"codemirror": "^5.58.3",
1516
"lodash": "^4.17.15",
1617
"react": "^16.12.0",
1718
"react-codemirror2": "^7.2.1",
1819
"react-dom": "^16.12.0",
1920
"react-redux": "^7.2.1",
21+
"react-router-dom": "^5.2.0",
2022
"redux": "^4.0.4"
2123
},
2224
"scripts": {

packages/example/src/App.tsx

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,30 @@
2626
THE SOFTWARE.
2727
*/
2828

29-
import React, { useCallback } from 'react';
29+
import React, { useCallback, useEffect, useRef } from 'react';
3030
import { JsonFormsDispatch, JsonFormsReduxContext } from '@jsonforms/react';
31-
import {
32-
Heading,
33-
Picker,
34-
Item,
35-
Section,
36-
Content,
37-
View,
38-
} from '@adobe/react-spectrum';
31+
import { useParams, useHistory } from 'react-router-dom';
32+
import { Heading, Item, Content, View } from '@adobe/react-spectrum';
3933
import { Tabs } from '@react-spectrum/tabs';
4034
import './App.css';
41-
import { AppProps, initializedConnect } from './reduxUtil';
35+
import {
36+
initializedConnect,
37+
ExampleStateProps,
38+
ExampleDispatchProps,
39+
} from './reduxUtil';
4240
import { TextArea } from './TextArea';
41+
import { ReactExampleDescription } from './util';
42+
import {
43+
getExamplesFromLocalStorage,
44+
setExampleInLocalStorage,
45+
localPrefix,
46+
localLabelSuffix,
47+
} from './persistedExamples';
48+
import { ExamplesPicker } from './ExamplesPicker';
4349

44-
function App(props: AppProps) {
50+
interface AppProps extends ExampleStateProps, ExampleDispatchProps {}
51+
52+
function App(props: AppProps & { selectedExample: ReactExampleDescription }) {
4553
const setExampleByName = useCallback(
4654
(exampleName: string | number) => {
4755
const example = props.examples.find(
@@ -56,30 +64,33 @@ function App(props: AppProps) {
5664

5765
const updateCurrentSchema = useCallback(
5866
(newSchema: string) => {
59-
props.changeExample({
60-
...props.selectedExample,
61-
schema: JSON.parse(newSchema),
62-
});
67+
props.changeExample(
68+
createExample(props.selectedExample, {
69+
schema: JSON.parse(newSchema),
70+
})
71+
);
6372
},
6473
[props.changeExample, props.selectedExample]
6574
);
6675

6776
const updateCurrentUISchema = useCallback(
6877
(newUISchema: string) => {
69-
props.changeExample({
70-
...props.selectedExample,
71-
uischema: JSON.parse(newUISchema),
72-
});
78+
props.changeExample(
79+
createExample(props.selectedExample, {
80+
uischema: JSON.parse(newUISchema),
81+
})
82+
);
7383
},
7484
[props.changeExample, props.selectedExample]
7585
);
7686

7787
const updateCurrentData = useCallback(
7888
(newData: string) => {
79-
props.changeExample({
80-
...props.selectedExample,
81-
data: JSON.parse(newData),
82-
});
89+
props.changeExample(
90+
createExample(props.selectedExample, {
91+
data: JSON.parse(newData),
92+
})
93+
);
8394
},
8495
[props.changeExample, props.selectedExample]
8596
);
@@ -96,7 +107,7 @@ function App(props: AppProps) {
96107
<div className='App-Form'>
97108
<View padding='size-100'>
98109
<Heading>{props.selectedExample.label}</Heading>
99-
{props.getExtensionComponent()}
110+
{props.getComponent(props.selectedExample)}
100111
<JsonFormsDispatch onChange={props.onChange} />
101112
</View>
102113
</div>
@@ -105,7 +116,7 @@ function App(props: AppProps) {
105116
<View padding='size-100'>
106117
<Heading>JsonForms Examples</Heading>
107118
<ExamplesPicker {...props} onChange={setExampleByName} />
108-
<Tabs defaultSelectedKey='schema'>
119+
<Tabs defaultSelectedKey='boundData'>
109120
<Item key='boundData' title='Bound data'>
110121
<Content margin='size-100'>
111122
<TextArea
@@ -148,41 +159,74 @@ function App(props: AppProps) {
148159
);
149160
}
150161

151-
export default initializedConnect(App);
162+
function AppWithExampleInURL(props: AppProps) {
163+
const urlParams = useParams<{ name: string | undefined }>();
164+
const history = useHistory();
165+
const examplesRef = useRef([
166+
...props.examples,
167+
...getExamplesFromLocalStorage(),
168+
]);
169+
const examples = examplesRef.current;
152170

153-
function ExamplesPicker(
154-
props: Omit<AppProps, 'onChange'> & {
155-
onChange: (exampleName: string | number) => void;
156-
}
157-
) {
158-
const options = [
159-
{
160-
name: 'React Spectrum Tests',
161-
children: props.examples
162-
.filter((example) => example.name.startsWith('spectrum-'))
163-
.map((item) => ({ ...item, id: item.name })),
164-
},
165-
{
166-
name: 'JSONForms Tests',
167-
children: props.examples
168-
.filter((example) => !example.name.startsWith('spectrum-'))
169-
.map((item) => ({ ...item, id: item.name })),
171+
const selectedExample = urlParams.name
172+
? examples.find(({ name }) => urlParams.name === name)
173+
: examples[examples.length - 1];
174+
175+
const changeExample = useCallback(
176+
(example: ReactExampleDescription) => {
177+
// If we're trying to modify an item, save it to local storage and update the list of examples
178+
if (example.name.startsWith(localPrefix)) {
179+
setExampleInLocalStorage(example);
180+
examplesRef.current = [
181+
...props.examples,
182+
...getExamplesFromLocalStorage(),
183+
];
184+
}
185+
history.push(`/${example.name}`);
170186
},
171-
];
187+
[props.changeExample, history]
188+
);
189+
190+
// When URL changes, we have to call changeExample to dispatch some jsonforms redux actions
191+
useEffect(() => {
192+
if (selectedExample) {
193+
props.changeExample(selectedExample);
194+
}
195+
}, [selectedExample]);
196+
197+
// If name is invalid, redirect to home
198+
if (!selectedExample) {
199+
console.error(
200+
`Could not find an example with name "${urlParams.name}", redirecting to /`
201+
);
202+
history.push('/');
203+
return null;
204+
}
172205

173206
return (
174-
<Picker
175-
aria-label='JSONForms Examples'
176-
items={options}
177-
width='100%'
178-
defaultSelectedKey={props.selectedExample.name}
179-
onSelectionChange={props.onChange}
180-
>
181-
{(item) => (
182-
<Section key={item.name} items={item.children} title={item.name}>
183-
{(item) => <Item>{item.label}</Item>}
184-
</Section>
185-
)}
186-
</Picker>
207+
<App
208+
{...props}
209+
examples={examples}
210+
selectedExample={selectedExample}
211+
changeExample={changeExample}
212+
/>
187213
);
188214
}
215+
216+
export const ConnectedApp = initializedConnect(AppWithExampleInURL);
217+
218+
function createExample(
219+
example: ReactExampleDescription,
220+
part: Partial<ReactExampleDescription>
221+
): ReactExampleDescription {
222+
return {
223+
...example,
224+
name: example.name.startsWith(localPrefix)
225+
? example.name
226+
: `${localPrefix}${example.name}`,
227+
label: example.label.endsWith(localLabelSuffix)
228+
? example.label
229+
: `${example.label}${localLabelSuffix}`,
230+
...part,
231+
};
232+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
The MIT License
3+
4+
Copyright (c) 2020 headwire.com, Inc
5+
https://github.com/headwirecom/jsonforms-react-spectrum-renderers
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the 'Software'), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
24+
*/
25+
26+
import { Item, Picker, Section } from '@adobe/react-spectrum';
27+
import React, { useRef } from 'react';
28+
import './App.css';
29+
import { localPrefix } from './persistedExamples';
30+
import { ReactExampleDescription } from './util';
31+
32+
export function ExamplesPicker(props: {
33+
examples: ReactExampleDescription[];
34+
selectedExample: ReactExampleDescription;
35+
onChange: (exampleName: string | number) => void;
36+
}) {
37+
// Re-create Picker instance (by changing key) when examples array changes, otherwise selection won't update on Save
38+
const prevExamples = useRef(props.examples);
39+
const keyRef = useRef(0);
40+
if (prevExamples.current !== props.examples) {
41+
prevExamples.current = props.examples;
42+
keyRef.current++;
43+
}
44+
45+
const options = [
46+
{
47+
name: 'Locally Modified Tests',
48+
children: props.examples
49+
.filter((example) => example.name.startsWith(localPrefix))
50+
.map((item) => ({ ...item, id: item.name })),
51+
},
52+
{
53+
name: 'React Spectrum Tests',
54+
children: props.examples
55+
.filter((example) => example.name.startsWith('spectrum-'))
56+
.map((item) => ({ ...item, id: item.name })),
57+
},
58+
{
59+
name: 'JSONForms Tests',
60+
children: props.examples
61+
.filter(
62+
(example) =>
63+
!example.name.startsWith('spectrum-') &&
64+
!example.name.startsWith(localPrefix)
65+
)
66+
.map((item) => ({ ...item, id: item.name })),
67+
},
68+
].filter((category) => category.children.length);
69+
70+
return (
71+
<Picker
72+
key={keyRef.current}
73+
aria-label='JSONForms Examples'
74+
items={options}
75+
width='100%'
76+
defaultSelectedKey={props.selectedExample.name}
77+
onSelectionChange={props.onChange}
78+
>
79+
{(item) => (
80+
<Section key={item.name} items={item.children} title={item.name}>
81+
{(item) => <Item>{item.label}</Item>}
82+
</Section>
83+
)}
84+
</Picker>
85+
);
86+
}

packages/example/src/index.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@
2626
THE SOFTWARE.
2727
*/
2828
import ReactDOM from 'react-dom';
29+
import {
30+
BrowserRouter as Router,
31+
Switch,
32+
Redirect,
33+
Route,
34+
} from 'react-router-dom';
2935
import React from 'react';
3036
import './index.css';
31-
import App from './App';
37+
import { ConnectedApp } from './App';
3238
import { combineReducers, createStore } from 'redux';
3339
import { Provider } from 'react-redux';
3440
import geoschema from './geographical-location.schema';
@@ -80,7 +86,6 @@ const setupStore = (
8086
renderers: renderers,
8187
},
8288
examples: {
83-
selectedExample: exampleData[exampleData.length - 1],
8489
data: exampleData,
8590
},
8691
});
@@ -129,7 +134,14 @@ export const renderExample = (
129134
<Provider store={store}>
130135
<SpectrumThemeProvider colorScheme={colorScheme} theme={defaultTheme}>
131136
<ColorSchemeContext.Provider value={colorScheme}>
132-
<App />
137+
<Router>
138+
<Switch>
139+
<Route exact path='/:name?'>
140+
<ConnectedApp />
141+
</Route>
142+
<Redirect to='/' />
143+
</Switch>
144+
</Router>
133145
</ColorSchemeContext.Provider>
134146
</SpectrumThemeProvider>
135147
</Provider>,

0 commit comments

Comments
 (0)