Skip to content

Commit 941935d

Browse files
authored
Material ui additional properties (rjsf-team#1993)
* Refactor: Move canExpand in to utils Allow reuse of the canExpand for theme implementation which supports additionalProperties. * Add material-ui support for additionalProperties (rjsf-team#1724) * New onKeyChange and onDropPropertyClick props for FieldTemplate * Add test for object with additionalProperties Co-authored-by: Yuval <valyouw@gmail.com> Co-authored-by: Jeffrey Larson <jeffothy@gmail.com>
1 parent 93330f6 commit 941935d

File tree

10 files changed

+509
-61
lines changed

10 files changed

+509
-61
lines changed

packages/antd/src/templates/ObjectFieldTemplate/index.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Row from 'antd/lib/row';
99
import { withConfigConsumer } from 'antd/lib/config-provider/context';
1010
import PlusCircleOutlined from '@ant-design/icons/PlusCircleOutlined';
1111

12-
const { getUiOptions } = utils;
12+
const { canExpand } = utils;
1313

1414
const DESCRIPTION_COL_STYLE = {
1515
paddingBottom: '8px',
@@ -78,23 +78,6 @@ const ObjectFieldTemplate = ({
7878
const filterHidden = (element) =>
7979
element.content.props.uiSchema['ui:widget'] !== 'hidden';
8080

81-
const canExpand = () => {
82-
if (!schema.additionalProperties) {
83-
return false;
84-
}
85-
86-
const { expandable } = getUiOptions(uiSchema);
87-
if (expandable === false) {
88-
return expandable;
89-
}
90-
91-
if (schema.maxProperties !== undefined) {
92-
return Object.keys(formData).length < schema.maxProperties;
93-
}
94-
95-
return true;
96-
};
97-
9881
return (
9982
<fieldset id={idSchema.$id}>
10083
<Row gutter={rowGutter}>
@@ -123,7 +106,7 @@ const ObjectFieldTemplate = ({
123106
))}
124107
</Row>
125108

126-
{canExpand() && (
109+
{canExpand(schema, uiSchema, formData) && (
127110
<Col span={24}>
128111
<Row gutter={rowGutter} justify="end">
129112
<Col flex="192px">

packages/core/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ declare module '@rjsf/core' {
163163
schema: JSONSchema7;
164164
uiSchema: UiSchema;
165165
formContext: any;
166+
onKeyChange: (value: string) => () => void;
167+
onDropPropertyClick: (value: string) => () => void;
166168
registry: FieldProps['registry'];
167169
};
168170

@@ -282,6 +284,8 @@ declare module '@rjsf/core' {
282284

283285
export const ADDITIONAL_PROPERTY_FLAG: string;
284286

287+
export function canExpand(schema: JSONSchema7, uiSchema: UiSchema, formData: any): boolean;
288+
285289
export function getDefaultRegistry(): FieldProps['registry'];
286290

287291
export function getSchemaType(schema: JSONSchema7): string;

packages/core/src/components/fields/ObjectField.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,11 @@ import {
66
orderProperties,
77
retrieveSchema,
88
getDefaultRegistry,
9-
getUiOptions,
9+
canExpand,
1010
ADDITIONAL_PROPERTY_FLAG,
1111
} from "../../utils";
1212

1313
function DefaultObjectFieldTemplate(props) {
14-
const canExpand = function canExpand() {
15-
const { formData, schema, uiSchema } = props;
16-
if (!schema.additionalProperties) {
17-
return false;
18-
}
19-
const { expandable } = getUiOptions(uiSchema);
20-
if (expandable === false) {
21-
return expandable;
22-
}
23-
// if ui:options.expandable was not explicitly set to false, we can add
24-
// another property if we have not exceeded maxProperties yet
25-
if (schema.maxProperties !== undefined) {
26-
return Object.keys(formData).length < schema.maxProperties;
27-
}
28-
return true;
29-
};
30-
3114
const { TitleField, DescriptionField } = props;
3215
return (
3316
<fieldset id={props.idSchema.$id}>
@@ -47,7 +30,7 @@ function DefaultObjectFieldTemplate(props) {
4730
/>
4831
)}
4932
{props.properties.map(prop => prop.content)}
50-
{canExpand() && (
33+
{canExpand(props.schema, props.uiSchema, props.formData) && (
5134
<AddButton
5235
className="object-property-expand"
5336
onClick={props.onAddClick(props.schema)}

packages/core/src/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ const widgetMap = {
6060
},
6161
};
6262

63+
export function canExpand(schema, uiSchema, formData) {
64+
if (!schema.additionalProperties) {
65+
return false;
66+
}
67+
const { expandable } = getUiOptions(uiSchema);
68+
if (expandable === false) {
69+
return expandable;
70+
}
71+
// if ui:options.expandable was not explicitly set to false, we can add
72+
// another property if we have not exceeded maxProperties yet
73+
if (schema.maxProperties !== undefined) {
74+
return Object.keys(formData).length < schema.maxProperties;
75+
}
76+
return true;
77+
}
78+
6379
export function getDefaultRegistry() {
6480
return {
6581
fields: require("./components/fields").default,

packages/core/test/utils_test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
mergeSchemas,
2828
getDisplayLabel,
2929
schemaRequiresTrueValue,
30+
canExpand,
3031
} from "../src/utils";
3132
import { createSandbox } from "./test_utils";
3233

@@ -3714,4 +3715,52 @@ describe("utils", () => {
37143715
expect(schemaRequiresTrueValue({ type: "string" })).eql(false);
37153716
});
37163717
});
3718+
3719+
describe("canExpand()", () => {
3720+
it("no additional properties", () => {
3721+
expect(canExpand({}, {}, {})).eql(false);
3722+
});
3723+
it("has additional properties", () => {
3724+
const schema = {
3725+
additionalProperties: {
3726+
type: "string",
3727+
},
3728+
};
3729+
expect(canExpand(schema, {}, {})).eql(true);
3730+
});
3731+
it("has uiSchema expandable false", () => {
3732+
const schema = {
3733+
additionalProperties: {
3734+
type: "string",
3735+
},
3736+
};
3737+
const uiSchema = {
3738+
"ui:options": {
3739+
expandable: false,
3740+
},
3741+
};
3742+
expect(canExpand(schema, uiSchema, {})).eql(false);
3743+
});
3744+
it("does not exceed maxProperties", () => {
3745+
const schema = {
3746+
maxProperties: 1,
3747+
additionalProperties: {
3748+
type: "string",
3749+
},
3750+
};
3751+
expect(canExpand(schema, {}, {})).eql(true);
3752+
});
3753+
it("already exceeds maxProperties", () => {
3754+
const schema = {
3755+
maxProperties: 1,
3756+
additionalProperties: {
3757+
type: "string",
3758+
},
3759+
};
3760+
const formData = {
3761+
foo: "bar",
3762+
};
3763+
expect(canExpand(schema, {}, formData)).eql(false);
3764+
});
3765+
});
37173766
});

packages/material-ui/src/FieldTemplate/FieldTemplate.tsx

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,59 @@ import List from "@material-ui/core/List";
88
import ListItem from "@material-ui/core/ListItem";
99
import Typography from "@material-ui/core/Typography";
1010

11+
import WrapIfAdditional from "./WrapIfAdditional";
12+
1113
const FieldTemplate = ({
1214
id,
1315
children,
16+
classNames,
17+
disabled,
1418
displayLabel,
19+
label,
20+
onDropPropertyClick,
21+
onKeyChange,
22+
readonly,
1523
required,
1624
rawErrors = [],
1725
rawHelp,
1826
rawDescription,
27+
schema,
1928
}: FieldTemplateProps) => {
2029
return (
21-
<FormControl
22-
fullWidth={true}
23-
error={rawErrors.length ? true : false}
24-
required={required}>
25-
{children}
26-
{displayLabel && rawDescription ? (
27-
<Typography variant="caption" color="textSecondary">
28-
{rawDescription}
29-
</Typography>
30-
) : null}
31-
{rawErrors.length > 0 && (
32-
<List dense={true} disablePadding={true}>
33-
{rawErrors.map((error, i: number) => {
34-
return (
35-
<ListItem key={i} disableGutters={true}>
36-
<FormHelperText id={id}>{error}</FormHelperText>
37-
</ListItem>
38-
);
39-
})}
40-
</List>
41-
)}
42-
{rawHelp && <FormHelperText id={id}>{rawHelp}</FormHelperText>}
43-
</FormControl>
30+
<WrapIfAdditional
31+
classNames={classNames}
32+
disabled={disabled}
33+
id={id}
34+
label={label}
35+
onDropPropertyClick={onDropPropertyClick}
36+
onKeyChange={onKeyChange}
37+
readonly={readonly}
38+
required={required}
39+
schema={schema}>
40+
<FormControl
41+
fullWidth={true}
42+
error={rawErrors.length ? true : false}
43+
required={required}>
44+
{children}
45+
{displayLabel && rawDescription ? (
46+
<Typography variant="caption" color="textSecondary">
47+
{rawDescription}
48+
</Typography>
49+
) : null}
50+
{rawErrors.length > 0 && (
51+
<List dense={true} disablePadding={true}>
52+
{rawErrors.map((error, i: number) => {
53+
return (
54+
<ListItem key={i} disableGutters={true}>
55+
<FormHelperText id={id}>{error}</FormHelperText>
56+
</ListItem>
57+
);
58+
})}
59+
</List>
60+
)}
61+
{rawHelp && <FormHelperText id={id}>{rawHelp}</FormHelperText>}
62+
</FormControl>
63+
</WrapIfAdditional>
4464
);
4565
};
4666

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from "react";
2+
3+
import { utils } from "@rjsf/core";
4+
import { JSONSchema7 } from "json-schema";
5+
6+
import Grid from "@material-ui/core/Grid";
7+
import FormControl from "@material-ui/core/FormControl";
8+
import Input from "@material-ui/core/Input";
9+
import InputLabel from "@material-ui/core/InputLabel";
10+
11+
import IconButton from "../IconButton/IconButton";
12+
13+
const { ADDITIONAL_PROPERTY_FLAG } = utils;
14+
15+
type WrapIfAdditionalProps = {
16+
children: React.ReactElement;
17+
classNames: string;
18+
disabled: boolean;
19+
id: string;
20+
label: string;
21+
onDropPropertyClick: (index: string) => (event?: any) => void;
22+
onKeyChange: (index: string) => (event?: any) => void;
23+
readonly: boolean;
24+
required: boolean;
25+
schema: JSONSchema7;
26+
};
27+
28+
const WrapIfAdditional = ({
29+
children,
30+
disabled,
31+
id,
32+
label,
33+
onDropPropertyClick,
34+
onKeyChange,
35+
readonly,
36+
required,
37+
schema,
38+
}: WrapIfAdditionalProps) => {
39+
const keyLabel = `${label} Key`; // i18n ?
40+
const additional = schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
41+
const btnStyle = {
42+
flex: 1,
43+
paddingLeft: 6,
44+
paddingRight: 6,
45+
fontWeight: "bold",
46+
};
47+
48+
if (!additional) {
49+
return <>{children}</>;
50+
}
51+
52+
const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) =>
53+
onKeyChange(target.value);
54+
55+
return (
56+
<Grid container={true} key={`${id}-key`} alignItems="center" spacing={2}>
57+
<Grid item={true} xs>
58+
<FormControl fullWidth={true} required={required}>
59+
<InputLabel>{keyLabel}</InputLabel>
60+
<Input
61+
defaultValue={label}
62+
disabled={disabled || readonly}
63+
id={`${id}-key`}
64+
name={`${id}-key`}
65+
onBlur={!readonly ? handleBlur : undefined}
66+
type="text"
67+
/>
68+
</FormControl>
69+
</Grid>
70+
<Grid item={true} xs>
71+
{children}
72+
</Grid>
73+
<Grid item={true}>
74+
<IconButton
75+
icon="remove"
76+
tabIndex={-1}
77+
style={btnStyle as any}
78+
disabled={disabled || readonly}
79+
onClick={onDropPropertyClick(label)}
80+
/>
81+
</Grid>
82+
</Grid>
83+
);
84+
};
85+
86+
export default WrapIfAdditional;

packages/material-ui/src/ObjectFieldTemplate/ObjectFieldTemplate.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import Grid from '@material-ui/core/Grid';
44
import { makeStyles } from '@material-ui/styles';
55

66
import { ObjectFieldTemplateProps } from '@rjsf/core';
7+
import { utils } from '@rjsf/core';
8+
9+
import AddButton from '../AddButton/AddButton';
10+
11+
const { canExpand } = utils;
712

813
const useStyles = makeStyles({
914
root: {
@@ -18,8 +23,13 @@ const ObjectFieldTemplate = ({
1823
title,
1924
properties,
2025
required,
26+
disabled,
27+
readonly,
2128
uiSchema,
2229
idSchema,
30+
schema,
31+
formData,
32+
onAddClick,
2333
}: ObjectFieldTemplateProps) => {
2434
const classes = useStyles();
2535

@@ -49,6 +59,17 @@ const ObjectFieldTemplate = ({
4959
{element.content}
5060
</Grid>
5161
))}
62+
{canExpand(schema, uiSchema, formData) && (
63+
<Grid container justify='flex-end'>
64+
<Grid item={true}>
65+
<AddButton
66+
className='object-property-expand'
67+
onClick={onAddClick(schema)}
68+
disabled={disabled || readonly}
69+
/>
70+
</Grid>
71+
</Grid>
72+
)}
5273
</Grid>
5374
</>
5475
);

0 commit comments

Comments
 (0)