Skip to content

Commit 9ac6894

Browse files
authored
Merge pull request #95 from headwirecom/feature/array-control-grid
Create array control using the Grid component
2 parents 882ddc0 + 4648ba8 commit 9ac6894

File tree

12 files changed

+1046
-137
lines changed

12 files changed

+1046
-137
lines changed

packages/spectrum/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,24 @@ JSONForms eliminates the tedious task of writing fully-featured forms by hand by
1010

1111
# Custom options
1212

13-
#### Custom options for Table Array Control
13+
#### Custom options for Grid Array Control and Table Array Control
14+
1415
```js
1516
{
1617
"type": "Control",
1718
"scope": "#/properties/myArray",
1819
"options": {
1920
"addButtonPosition": "top", // "top" or "bottom"
2021
"addButtonLabel": "Add item", // optional custom label for Add button
21-
"addButtonLabelType": "tooltip" // "tooltip" or "inline"
22+
"addButtonLabelType": "tooltip", // "tooltip" or "inline"
23+
"table": true, // When true, uses @react-spectrum/table. When false, uses Grid component from React Spectrum (default: false)
24+
"spacing": [3, 1], // Numbers correspond to proportions of column widths (defaults to 1). Has effect only when table=false
2225
}
2326
}
2427
```
2528

2629
#### Custom options for Horizontal Layout
30+
2731
```js
2832
{
2933
"type": "HorizontalLayout",

packages/spectrum/example/samples.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,60 @@ samples.push({
5151
},
5252
});
5353

54+
samples.push({
55+
name: 'spectrum-array-table',
56+
label: 'Array (using @react-spectrum/table)',
57+
uischema: {
58+
type: 'VerticalLayout',
59+
elements: [
60+
{
61+
type: 'Control',
62+
scope: '#/properties/comments',
63+
"options": {
64+
"table": true
65+
}
66+
},
67+
],
68+
},
69+
schema: {
70+
type: 'object',
71+
properties: {
72+
comments: {
73+
type: 'array',
74+
items: {
75+
type: 'object',
76+
properties: {
77+
date: {
78+
type: 'string',
79+
format: 'date',
80+
},
81+
message: {
82+
type: 'string',
83+
maxLength: 5,
84+
},
85+
enum: {
86+
type: 'string',
87+
const: 'foo',
88+
},
89+
},
90+
},
91+
},
92+
},
93+
},
94+
data: {
95+
comments: [
96+
{
97+
date: '2001-09-11',
98+
message: 'This is an example message',
99+
},
100+
{
101+
date: '2020-12-02',
102+
message: 'Get ready for booohay',
103+
},
104+
],
105+
},
106+
});
107+
54108
samples.push({
55109
name: 'spectrum-categorization-1',
56110
label: 'Categorization',

packages/spectrum/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@headwire/jsonforms-react-spectrum-renderers",
3-
"version": "0.0.1-beta.2",
3+
"version": "0.0.1-beta.3",
44
"description": "React Spectrum Renderer Set for JSONForms",
55
"repository": "https://github.com/headwirecom/jsonforms-react-spectrum-renderers",
66
"bugs": "https://github.com/headwirecom/jsonforms-react-spectrum-renderers/issues",

packages/spectrum/src/cells/SpectrumBooleanCell.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const SpectrumBooleanCell: FunctionComponent<CellProps> = (
6969
autoFocus={autoFocus}
7070
validationState={validationState}
7171
width={width}
72+
aria-label={props.children ? undefined : path}
7273
>
7374
{props.children}
7475
</Checkbox>
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
The MIT License
3+
4+
Copyright (c) 2017-2019 EclipseSource Munich
5+
https://github.com/eclipsesource/jsonforms
6+
7+
Copyright (c) 2020 headwire.com, Inc
8+
https://github.com/headwirecom/jsonforms-react-spectrum-renderers
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy
11+
of this software and associated documentation files (the "Software"), to deal
12+
in the Software without restriction, including without limitation the rights
13+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
copies of the Software, and to permit persons to whom the Software is
15+
furnished to do so, subject to the following conditions:
16+
17+
The above copyright notice and this permission notice shall be included in
18+
all copies or substantial portions of the Software.
19+
20+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26+
THE SOFTWARE.
27+
*/
28+
import React from 'react';
29+
import startCase from 'lodash/startCase';
30+
import {
31+
ArrayControlProps,
32+
ControlElement,
33+
createDefaultValue,
34+
Helpers,
35+
isPlainLabel,
36+
Paths,
37+
RankedTester,
38+
Resolve,
39+
Test,
40+
} from '@jsonforms/core';
41+
import { DispatchCell, withJsonFormsArrayControlProps } from '@jsonforms/react';
42+
import {
43+
ActionButton,
44+
AlertDialog,
45+
DialogTrigger,
46+
Flex,
47+
Text,
48+
Tooltip,
49+
TooltipTrigger,
50+
View,
51+
Grid,
52+
} from '@adobe/react-spectrum';
53+
54+
import Delete from '@spectrum-icons/workflow/Delete';
55+
import {
56+
getUIOptions,
57+
getChildError,
58+
ArrayHeader,
59+
ArrayFooter,
60+
} from './array/utils';
61+
62+
const { createLabelDescriptionFrom } = Helpers;
63+
64+
const {
65+
or,
66+
isObjectArrayControl,
67+
isPrimitiveArrayControl,
68+
rankWith,
69+
and,
70+
} = Test;
71+
72+
const isTableOptionNotTrue: Test.Tester = (uischema) =>
73+
!uischema.options?.table;
74+
75+
export const spectrumArrayControlGridTester: RankedTester = rankWith(
76+
3,
77+
or(
78+
and(isObjectArrayControl, isTableOptionNotTrue),
79+
and(isPrimitiveArrayControl, isTableOptionNotTrue)
80+
)
81+
);
82+
83+
const errorFontSize = 0.8;
84+
const errorStyle = {
85+
color: 'var(--spectrum-semantic-negative-color-default)',
86+
lineHeight: 1.3,
87+
fontSize: `${errorFontSize * 100}%`,
88+
};
89+
90+
// Calculate minimum row height so that it does not change no matter if a call has an error message or not
91+
const rowMinHeight = `calc(var(--spectrum-alias-font-size-default) * ${errorFontSize} * ${errorStyle.lineHeight} + var(--spectrum-alias-single-line-height))`;
92+
93+
function SpectrumArrayControlGrid(props: ArrayControlProps) {
94+
const {
95+
addItem,
96+
uischema,
97+
schema,
98+
rootSchema,
99+
path,
100+
data,
101+
visible,
102+
label,
103+
childErrors,
104+
removeItems,
105+
} = props;
106+
107+
const controlElement = uischema as ControlElement;
108+
const createControlElement = (key?: string): ControlElement => ({
109+
type: 'Control',
110+
label: false,
111+
scope: schema.type === 'object' ? `#/properties/${key}` : '#',
112+
});
113+
114+
const labelObject = createLabelDescriptionFrom(controlElement, schema);
115+
116+
const uioptions = getUIOptions(uischema, labelObject.text);
117+
const spacing: number[] = uischema.options?.spacing ?? [];
118+
const add = addItem(path, createDefaultValue(schema));
119+
const fields = schema.properties ? Object.keys(schema.properties) : ['items'];
120+
121+
return (
122+
<View
123+
isHidden={visible === undefined || visible === null ? false : !visible}
124+
>
125+
<ArrayHeader
126+
{...uioptions}
127+
add={add}
128+
allErrorsMessages={childErrors.map((e) => e.message)}
129+
labelText={isPlainLabel(label) ? label : label.default}
130+
/>
131+
{data && Array.isArray(data) && data.length > 0 && (
132+
<Grid
133+
columns={
134+
spacing.length
135+
? `${fields.map((_, i) => `${spacing[i] || 1}fr`).join(' ')} 0fr`
136+
: `repeat(${fields.length}, 1fr) 0fr`
137+
}
138+
rows='auto'
139+
autoRows={`minmax(${rowMinHeight}, auto)`}
140+
columnGap='size-100'
141+
>
142+
{fields
143+
.map((prop) => (
144+
<View
145+
paddingBottom='size-50'
146+
justifySelf={
147+
schema.properties?.[prop]?.type === 'boolean'
148+
? 'center'
149+
: undefined
150+
}
151+
key={prop}
152+
>
153+
{startCase(prop)}
154+
</View>
155+
))
156+
.concat(<View key='spacer' />)}
157+
{data.map((_, index) => {
158+
const childPath = Paths.compose(path, `${index}`);
159+
const rowCells: JSX.Element[] = schema.properties
160+
? fields
161+
.filter((prop) => schema.properties[prop].type !== 'array')
162+
.map((prop) => {
163+
const childPropPath = Paths.compose(
164+
childPath,
165+
prop.toString()
166+
);
167+
const isCheckbox =
168+
schema.properties[prop].type === 'boolean';
169+
return (
170+
<View
171+
key={childPropPath}
172+
paddingStart={isCheckbox ? 'size-200' : undefined}
173+
>
174+
<Flex
175+
direction='column'
176+
width='100%'
177+
alignItems={isCheckbox ? 'center' : 'start'}
178+
>
179+
<DispatchCell
180+
schema={Resolve.schema(
181+
schema,
182+
`#/properties/${prop}`,
183+
rootSchema
184+
)}
185+
uischema={
186+
isCheckbox
187+
? {
188+
...createControlElement(prop),
189+
options: { trim: true },
190+
}
191+
: createControlElement(prop)
192+
}
193+
path={childPath + '.' + prop}
194+
/>
195+
<View
196+
UNSAFE_style={errorStyle}
197+
isHidden={
198+
getChildError(childErrors, childPropPath) === ''
199+
}
200+
>
201+
<Text>
202+
{getChildError(childErrors, childPropPath)}
203+
</Text>
204+
</View>
205+
</Flex>
206+
</View>
207+
);
208+
})
209+
: [
210+
<View key={Paths.compose(childPath, index.toString())}>
211+
<Flex direction='column' width='100%'>
212+
<DispatchCell
213+
schema={schema}
214+
uischema={createControlElement()}
215+
path={childPath}
216+
/>
217+
<View
218+
UNSAFE_style={errorStyle}
219+
isHidden={getChildError(childErrors, childPath) === ''}
220+
>
221+
<Text>{getChildError(childErrors, childPath)}</Text>
222+
</View>
223+
</Flex>
224+
</View>,
225+
];
226+
return (
227+
<React.Fragment key={index}>
228+
{rowCells}
229+
<DeleteButton
230+
index={index}
231+
path={childPath}
232+
removeItems={removeItems}
233+
/>
234+
</React.Fragment>
235+
);
236+
})}
237+
</Grid>
238+
)}
239+
<ArrayFooter {...uioptions} add={add} />
240+
</View>
241+
);
242+
}
243+
244+
function DeleteButton(props: {
245+
removeItems: ArrayControlProps['removeItems'];
246+
index: number;
247+
path: string;
248+
}) {
249+
const { removeItems, path, index } = props;
250+
const remove = React.useCallback(() => {
251+
const p = path.substring(0, path.lastIndexOf('.'));
252+
removeItems(p, [index])();
253+
}, [removeItems, path, index]);
254+
255+
return (
256+
<View key={`delete-row-${index}`}>
257+
<DialogTrigger>
258+
<TooltipTrigger delay={0}>
259+
<ActionButton aria-label={`Delete row at ${index}`}>
260+
<Delete />
261+
</ActionButton>
262+
<Tooltip>Delete</Tooltip>
263+
</TooltipTrigger>
264+
<AlertDialog
265+
variant='confirmation'
266+
title='Delete'
267+
primaryActionLabel='Delete'
268+
cancelLabel='Cancel'
269+
autoFocusButton='primary'
270+
onPrimaryAction={remove}
271+
>
272+
Are you sure you wish to delete this item?
273+
</AlertDialog>
274+
</DialogTrigger>
275+
</View>
276+
);
277+
}
278+
279+
export default withJsonFormsArrayControlProps(SpectrumArrayControlGrid);

0 commit comments

Comments
 (0)