Skip to content

Commit 22962b2

Browse files
rhgbn1k0
authored andcommitted
Handle fixed items arrays (rjsf-team#132)
1 parent 891693c commit 22962b2

File tree

7 files changed

+439
-47
lines changed

7 files changed

+439
-47
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
4242
- [Live form data validation](#live-form-data-validation)
4343
- [Styling your forms](#styling-your-forms)
4444
- [Schema definitions and references](#schema-definitions-and-references)
45+
- [JSON Schema supporting status](#json-schema-supporting-status)
4546
- [Troubleshooting](#troubleshooting)
4647
- [Build error wrt missing "buffertools" module](#build-error-wrt-missing-buffertools-module)
4748
- [Contributing](#contributing)
@@ -636,6 +637,13 @@ This library partially supports [inline schema definition dereferencing]( http:/
636637
637638
Note that it only supports local definition referencing, we do not plan on fetching foreign schemas over HTTP anytime soon. Basically, you can only reference a definition from the very schema object defining it.
638639
640+
## JSON Schema supporting status
641+
642+
This component follows [JSON Schema](http://json-schema.org/documentation.html) specs. Due to the limitation of form widgets, there are some exceptions as follows:
643+
644+
* `additionalItems` keyword for arrays
645+
This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported.
646+
639647
## Troubleshooting
640648

641649
### Build error wrt missing "buffertools" module

playground/samples/arrays.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,55 @@ module.exports = {
1818
enum: ["foo", "bar", "fuzz"],
1919
},
2020
uniqueItems: true
21+
},
22+
fixedItemsList: {
23+
type: "array",
24+
title: "A list of fixed items",
25+
items: [
26+
{
27+
title: "A string value",
28+
type: "string",
29+
default: "lorem ipsum"
30+
},
31+
{
32+
title: "a boolean value",
33+
type: "boolean"
34+
}
35+
],
36+
additionalItems: {
37+
title: "Additional item",
38+
type: "number"
39+
}
40+
},
41+
nestedList: {
42+
type: "array",
43+
title: "Nested list",
44+
items: {
45+
type: "array",
46+
title: "Inner list",
47+
items: {
48+
type: "string",
49+
default: "lorem ipsum"
50+
}
51+
}
52+
}
53+
}
54+
},
55+
uiSchema: {
56+
fixedItemsList: {
57+
items: [
58+
{"ui:widget": "textarea"},
59+
{"ui:widget": "select"}
60+
],
61+
additionalItems: {
62+
"ui:widget": "updown"
2163
}
2264
}
2365
},
24-
uiSchema: {},
2566
formData: {
2667
listOfStrings: ["foo", "bar"],
27-
multipleChoicesList: ["foo", "bar"]
68+
multipleChoicesList: ["foo", "bar"],
69+
fixedItemsList: ["Some text", true, 123],
70+
nestedList: [["lorem", "ipsum"], ["dolor"]]
2871
}
2972
};

src/components/fields/ArrayField.js

Lines changed: 146 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import React, { Component, PropTypes } from "react";
33
import {
44
getDefaultFormState,
55
isMultiSelect,
6+
isFixedItems,
7+
allowAdditionalItems,
68
optionsList,
79
retrieveSchema,
810
toIdSchema,
@@ -59,8 +61,12 @@ class ArrayField extends Component {
5961
const {items} = this.state;
6062
const {schema, registry} = this.props;
6163
const {definitions} = registry;
64+
let itemSchema = schema.items;
65+
if (isFixedItems(schema) && allowAdditionalItems(schema)) {
66+
itemSchema = schema.additionalItems;
67+
}
6268
this.asyncSetState({
63-
items: items.concat([getDefaultFormState(schema.items, undefined, definitions)])
69+
items: items.concat([getDefaultFormState(itemSchema, undefined, definitions)])
6470
}, {validate: false});
6571
};
6672

@@ -88,25 +94,22 @@ class ArrayField extends Component {
8894
};
8995

9096
render() {
97+
const {schema} = this.props;
98+
if (isFixedItems(schema)) {
99+
return this.renderFixedArray();
100+
}
101+
if (isMultiSelect(schema)) {
102+
return this.renderMultiSelect();
103+
}
104+
return this.renderNormalArray();
105+
}
106+
107+
renderNormalArray() {
91108
const {schema, uiSchema, errorSchema, idSchema, name} = this.props;
92109
const title = schema.title || name;
93110
const {items} = this.state;
94-
const {fields, definitions} = this.props.registry;
95-
const {SchemaField} = fields;
111+
const {definitions} = this.props.registry;
96112
const itemsSchema = retrieveSchema(schema.items, definitions);
97-
if (isMultiSelect(schema)) {
98-
return (
99-
<SelectWidget
100-
id={idSchema && idSchema.id}
101-
multiple
102-
onChange={this.onSelectChange}
103-
options={optionsList(itemsSchema)}
104-
schema={schema}
105-
title={title}
106-
value={items}
107-
/>
108-
);
109-
}
110113

111114
return (
112115
<fieldset
@@ -119,37 +122,138 @@ class ArrayField extends Component {
119122
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
120123
const itemIdPrefix = idSchema.id + "_" + index;
121124
const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
122-
return (
123-
<div key={index}>
124-
<div className="col-xs-10">
125-
<SchemaField
126-
schema={itemsSchema}
127-
uiSchema={uiSchema.items}
128-
formData={items[index]}
129-
errorSchema={itemErrorSchema}
130-
idSchema={itemIdSchema}
131-
required={this.isItemRequired(itemsSchema)}
132-
onChange={this.onChangeForIndex(index)}
133-
registry={this.props.registry}/>
134-
</div>
135-
<div className="col-xs-2 array-item-remove text-right">
136-
<button type="button" className="btn btn-danger col-xs-12"
137-
tabIndex="-1"
138-
onClick={this.onDropIndexClick(index)}>Delete</button>
139-
</div>
140-
</div>
141-
);
125+
return this.renderArrayFieldItem({
126+
index,
127+
itemSchema: itemsSchema,
128+
itemIdSchema,
129+
itemErrorSchema,
130+
itemData: items[index],
131+
itemUiSchema: uiSchema.items
132+
});
142133
})
143134
}</div>
144-
<div className="row">
145-
<p className="col-xs-2 col-xs-offset-10 array-item-add text-right">
146-
<button type="button" className="btn btn-info col-xs-12"
147-
tabIndex="-1" onClick={this.onAddClick}>Add</button>
148-
</p>
149-
</div>
135+
<AddButton onClick={this.onAddClick}/>
150136
</fieldset>
151137
);
152138
}
139+
140+
renderMultiSelect() {
141+
const {schema, idSchema, name} = this.props;
142+
const title = schema.title || name;
143+
const {items} = this.state;
144+
const {definitions} = this.props.registry;
145+
const itemsSchema = retrieveSchema(schema.items, definitions);
146+
return (
147+
<SelectWidget
148+
id={idSchema && idSchema.id}
149+
multiple
150+
onChange={this.onSelectChange}
151+
options={optionsList(itemsSchema)}
152+
schema={schema}
153+
title={title}
154+
value={items}
155+
/>
156+
);
157+
}
158+
159+
renderFixedArray() {
160+
const {schema, uiSchema, errorSchema, idSchema, name} = this.props;
161+
const title = schema.title || name;
162+
let {items} = this.state;
163+
const {definitions} = this.props.registry;
164+
const itemSchemas = schema.items.map(item =>
165+
retrieveSchema(item, definitions));
166+
const additionalSchema = allowAdditionalItems(schema) ?
167+
retrieveSchema(schema.additionalItems, definitions) : null;
168+
169+
if (!items || items.length < itemSchemas.length) {
170+
// to make sure at least all fixed items are generated
171+
items = items || [];
172+
items = items.concat(new Array(itemSchemas.length - items.length));
173+
}
174+
175+
return (
176+
<fieldset className="field field-array field-array-fixed-items">
177+
{title ? <legend>{title}</legend> : null}
178+
{schema.description ?
179+
<div className="field-description">{schema.description}</div> : null}
180+
<div className="row array-item-list">{
181+
items.map((item, index) => {
182+
const additional = index >= itemSchemas.length;
183+
const itemSchema = additional ?
184+
additionalSchema : itemSchemas[index];
185+
const itemIdPrefix = idSchema.id + "_" + index;
186+
const itemIdSchema = toIdSchema(itemSchema, itemIdPrefix, definitions);
187+
const itemUiSchema = additional ?
188+
uiSchema.additionalItems || {} :
189+
Array.isArray(uiSchema.items) ?
190+
uiSchema.items[index] : uiSchema.items || {};
191+
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
192+
193+
return this.renderArrayFieldItem({
194+
index,
195+
removable: additional,
196+
itemSchema,
197+
itemData: item,
198+
itemUiSchema,
199+
itemIdSchema,
200+
itemErrorSchema
201+
});
202+
})
203+
}</div>
204+
{
205+
additionalSchema ? <AddButton onClick={this.onAddClick}/> : null
206+
}
207+
</fieldset>
208+
);
209+
}
210+
211+
renderArrayFieldItem({
212+
index,
213+
removable=true,
214+
itemSchema,
215+
itemData,
216+
itemUiSchema,
217+
itemIdSchema,
218+
itemErrorSchema
219+
}) {
220+
const {SchemaField} = this.props.registry.fields;
221+
return (
222+
<div key={index}>
223+
<div className={removable ? "col-xs-10" : "col-xs-12"}>
224+
<SchemaField
225+
schema={itemSchema}
226+
uiSchema={itemUiSchema}
227+
formData={itemData}
228+
errorSchema={itemErrorSchema}
229+
idSchema={itemIdSchema}
230+
required={this.isItemRequired(itemSchema)}
231+
onChange={this.onChangeForIndex(index)}
232+
registry={this.props.registry}/>
233+
</div>
234+
{
235+
removable ?
236+
<div className="col-xs-2 array-item-remove text-right">
237+
<button type="button" className="btn btn-danger col-xs-12"
238+
tabIndex="-1"
239+
onClick={this.onDropIndexClick(index)}>Delete</button>
240+
</div>
241+
: null
242+
}
243+
</div>
244+
);
245+
}
246+
}
247+
248+
function AddButton({onClick}) {
249+
return (
250+
<div className="row">
251+
<p className="col-xs-2 col-xs-offset-10 array-item-add text-right">
252+
<button type="button" className="btn btn-info col-xs-12"
253+
tabIndex="-1" onClick={onClick}>Add</button>
254+
</p>
255+
</div>
256+
);
153257
}
154258

155259
if (process.env.NODE_ENV !== "production") {

src/utils.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import URLWidget from "./components/widgets/URLWidget";
1313
import TextareaWidget from "./components/widgets/TextareaWidget";
1414
import HiddenWidget from "./components/widgets/HiddenWidget";
1515

16-
const RE_ERROR_ARRAY_PATH = /(.*)\[(\d+)\]$/;
16+
const RE_ERROR_ARRAY_PATH = /\[\d+]/g;
1717

1818
const altWidgetMap = {
1919
boolean: {
@@ -111,6 +111,8 @@ function computeDefaults(schema, parentDefaults, definitions={}) {
111111
// Use referenced schema defaults for this node.
112112
const refSchema = findSchemaDefinition(schema.$ref, definitions);
113113
defaults = computeDefaults(refSchema, defaults, definitions);
114+
} else if (isFixedItems(schema)) {
115+
defaults = schema.items.map(itemSchema => computeDefaults(itemSchema, undefined, definitions));
114116
}
115117
// Not defaults defined for this node, fallback to generic typed ones.
116118
if (typeof(defaults) === "undefined") {
@@ -193,6 +195,19 @@ export function isMultiSelect(schema) {
193195
return Array.isArray(schema.items.enum) && schema.uniqueItems;
194196
}
195197

198+
export function isFixedItems(schema) {
199+
return Array.isArray(schema.items) &&
200+
schema.items.length > 0 &&
201+
schema.items.every(item => isObject(item));
202+
}
203+
204+
export function allowAdditionalItems(schema) {
205+
if (schema.additionalItems === true) {
206+
console.warn("additionalItems=true is currently not supported");
207+
}
208+
return isObject(schema.additionalItems);
209+
}
210+
196211
export function optionsList(schema) {
197212
return schema.enum.map((value, i) => {
198213
const label = schema.enumNames && schema.enumNames[i] || String(value);
@@ -227,9 +242,11 @@ function errorPropertyToPath(property) {
227242
// Parse array indices, eg. "instance.level1.level2[2].level3"
228243
// => ["instance", "level1", "level2", 2, "level3"]
229244
return property.split(".").reduce((path, node) => {
230-
const match = RE_ERROR_ARRAY_PATH.exec(node);
245+
const match = node.match(RE_ERROR_ARRAY_PATH);
231246
if (match) {
232-
path = path.concat([match[1], parseInt(match[2], 10)]);
247+
const nodeName = node.slice(0, node.indexOf('['));
248+
const indices = match.map(str => parseInt(str.slice(1, -1), 10));
249+
path = path.concat(nodeName, indices);
233250
} else {
234251
path.push(node);
235252
}

0 commit comments

Comments
 (0)