diff --git a/README.md b/README.md
index c504811bd9..02a865bf1d 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
- [Form attributes](#form-attributes)
- [Advanced customization](#advanced-customization)
- [Field template](#field-template)
+ - [Array field template](#array-field-template)
- [Custom widgets and fields](#custom-widgets-and-fields)
- [Custom widget components](#custom-widget-components)
- [Custom component registration](#custom-component-registration)
@@ -700,6 +701,58 @@ The following props are passed to a custom field template component:
> Note: you can only define a single field template for a form. If you need many, it's probably time to look at [custom fields](#custom-field-components) instead.
+### Array Field Template
+
+Similarly to the `FieldTemplate` you can use an `ArrayFieldTemplate` to customize how your
+arrays are rendered. This allows you to customize your array, and each element in the array.
+
+```jsx
+function ArrayFieldTemplate(props) {
+ return (
+
+ {props.items.map(element => element.children)}
+ {props.canAdd && }
+
+ );
+}
+
+render((
+ ,
+), document.getElementById("app"));
+```
+
+Please see [customArray.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customArray.js) for a better example.
+
+The following props are passed to each `ArrayFieldTemplate`:
+
+- `DescriptionField`: The generated `DescriptionField` (if you wanted to utilize it)
+- `TitleField`: The generated `TitleField` (if you wanted to utilize it).
+- `canAdd`: A boolean value stating whether new elements can be added to the array.
+- `className`: The className string.
+- `disabled`: A boolean value stating if the array is disabled.
+- `idSchema`: Object
+- `items`: An array of objects representing the items in the array. Each of the items represent a child with properties described below.
+- `onAddClick: (event) => (event) => void`: Returns a function that adds a new item to the array.
+- `readonly`: A boolean value stating if the array is readonly.
+- `required`: A boolean value stating if the array is required.
+- `schema`: The schema object for this array.
+- `title`: A string value containing the title for the array.
+
+The following props are part of each element in `items`:
+
+- `children`: The html for the item's content.
+- `className`: The className string.
+- `disabled`: A boolean value stating if the array item is disabled.
+- `hasMoveDown`: A boolean value stating whether the array item can be moved down.
+- `hasMoveUp`: A boolean value stating whether the array item can be moved up.
+- `hasRemove`: A boolean value stating whether the array item can be removed.
+- `hasToolbar`: A boolean value stating whether the array item has a toolbar.
+- `index`: A number stating the index the array item occurs in `items`.
+- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`.
+- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`.
+- `readonly`: A boolean value stating if the array item is readonly.
+
### Custom widgets and fields
The API allows to specify your own custom *widget* and *field* components:
diff --git a/playground/app.js b/playground/app.js
index c792554327..649ebd6b77 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -283,9 +283,11 @@ class App extends Component {
}
load = (data) => {
+ // Reset the ArrayFieldTemplate whenever you load new data
+ const { ArrayFieldTemplate } = data;
// force resetting form component instance
this.setState({form: false},
- _ => this.setState({...data, form: true}));
+ _ => this.setState({...data, form: true, ArrayFieldTemplate}));
};
onSchemaEdited = (schema) => this.setState({schema});
@@ -314,7 +316,8 @@ class App extends Component {
liveValidate,
validate,
theme,
- editor
+ editor,
+ ArrayFieldTemplate
} = this.state;
return (
@@ -352,6 +355,7 @@ class App extends Component {
{!this.state.form ? null :
+ );
+}
+
+module.exports = {
+ schema: {
+ title: "Custom array of strings",
+ type: "array",
+ items: {
+ type: "string"
+ }
+ },
+ formData: ["react", "jsonschema", "form"],
+ ArrayFieldTemplate
+};
diff --git a/playground/samples/index.js b/playground/samples/index.js
index 5f6d280dcc..8eba6cc825 100644
--- a/playground/samples/index.js
+++ b/playground/samples/index.js
@@ -12,6 +12,7 @@ import date from "./date";
import validation from "./validation";
import files from "./files";
import single from "./single";
+import customArray from "./customArray";
export const samples = {
Simple: simple,
@@ -27,5 +28,6 @@ export const samples = {
"Date & time": date,
Validation: validation,
Files: files,
- Single: single
+ Single: single,
+ "Custom Array": customArray
};
diff --git a/src/components/Form.js b/src/components/Form.js
index 32819cc75b..89b83cd4fb 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -119,6 +119,7 @@ export default class Form extends Component {
return {
fields: {...fields, ...this.props.fields},
widgets: {...widgets, ...this.props.widgets},
+ ArrayFieldTemplate: this.props.ArrayFieldTemplate,
FieldTemplate: this.props.FieldTemplate,
definitions: this.props.schema.definitions || {},
formContext: this.props.formContext || {},
@@ -184,6 +185,7 @@ if (process.env.NODE_ENV !== "production") {
PropTypes.object,
])),
fields: PropTypes.objectOf(PropTypes.func),
+ ArrayFieldTemplate: PropTypes.func,
FieldTemplate: PropTypes.func,
onChange: PropTypes.func,
onError: PropTypes.func,
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 6239274bec..987724d776 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -43,6 +43,111 @@ function IconBtn(props) {
);
}
+// Used in the two templates
+function DefaultArrayItem(props) {
+ const btnStyle = {flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold"};
+ return (
+
+
+
+ {props.children}
+
+
+ {props.hasToolbar ?
+
+
+
+ {props.hasMoveUp || props.hasMoveDown ?
+
+ : null}
+
+ {props.hasMoveUp || props.hasMoveDown ?
+
+ : null}
+
+ {props.hasRemove ?
+
+ : null}
+
+
+ : null}
+
+
+ );
+}
+
+function DefaultFixedArrayFieldTemplate(props) {
+ return (
+
+
+
+
+ {props.schema.description ? (
+
+ {props.schema.description}
+
+ ) : null}
+
+
+ {props.items && props.items.map(DefaultArrayItem)}
+
+
+ {props.canAdd ? : null}
+
+ );
+}
+
+function DefaultNormalArrayFieldTemplate(props) {
+ return (
+
+
+
+
+ {props.schema.description ? (
+
+ ) : null}
+
+
+ {props.items && props.items.map(p => DefaultArrayItem(p))}
+
+
+ {props.canAdd ? : null}
+
+ );
+}
+
class ArrayField extends Component {
static defaultProps = {
uiSchema: {},
@@ -108,7 +213,9 @@ class ArrayField extends Component {
onDropIndexClick = (index) => {
return (event) => {
- event.preventDefault();
+ if (event) {
+ event.preventDefault();
+ }
this.asyncSetState({
items: this.state.items.filter((_, i) => i !== index)
}, {validate: true}); // refs #195
@@ -117,8 +224,10 @@ class ArrayField extends Component {
onReorderClick = (index, newIndex) => {
return (event) => {
- event.preventDefault();
- event.target.blur();
+ if (event) {
+ event.preventDefault();
+ event.target.blur();
+ }
const {items} = this.state;
this.asyncSetState({
items: items.map((item, i) => {
@@ -173,50 +282,48 @@ class ArrayField extends Component {
disabled,
readonly,
autofocus,
+ registry
} = this.props;
const title = (schema.title === undefined) ? name : schema.title;
- const {items} = this.state;
- const {definitions, fields} = this.props.registry;
+ const {items = []} = this.state;
+ const {ArrayFieldTemplate, definitions, fields} = registry;
const {TitleField, DescriptionField} = fields;
const itemsSchema = retrieveSchema(schema.items, definitions);
const {addable=true} = getUiOptions(uiSchema);
- return (
-
-
- {schema.description ?
- : null}
- {
- items.map((item, index) => {
- const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
- const itemIdPrefix = idSchema.$id + "_" + index;
- const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
- return this.renderArrayFieldItem({
- index,
- canMoveUp: index > 0,
- canMoveDown: index < items.length - 1,
- itemSchema: itemsSchema,
- itemIdSchema,
- itemErrorSchema,
- itemData: items[index],
- itemUiSchema: uiSchema.items,
- autofocus: autofocus && index === 0
- });
- })
- }
- {addable ? : null}
-
- );
+ const arrayProps = {
+ canAdd: addable,
+ items: items.map((item, index) => {
+ const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
+ const itemIdPrefix = idSchema.$id + "_" + index;
+ const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
+ return this.renderArrayFieldItem({
+ index,
+ canMoveUp: index > 0,
+ canMoveDown: index < items.length - 1,
+ itemSchema: itemsSchema,
+ itemIdSchema,
+ itemErrorSchema,
+ itemData: items[index],
+ itemUiSchema: uiSchema.items,
+ autofocus: autofocus && index === 0
+ });
+ }),
+ className: `field field-array field-array-of-${itemsSchema.type}`,
+ DescriptionField,
+ disabled,
+ idSchema,
+ onAddClick: this.onAddClick,
+ readonly,
+ required,
+ schema,
+ title,
+ TitleField
+ };
+
+ // Check if a custom render function was passed in
+ const renderFunction = ArrayFieldTemplate || DefaultNormalArrayFieldTemplate;
+ return renderFunction(arrayProps);
}
renderMultiSelect() {
@@ -274,10 +381,11 @@ class ArrayField extends Component {
disabled,
readonly,
autofocus,
+ registry
} = this.props;
const title = schema.title || name;
let {items} = this.state;
- const {definitions, fields} = this.props.registry;
+ const {ArrayFieldTemplate, definitions, fields} = registry;
const {TitleField} = fields;
const itemSchemas = schema.items.map(item =>
retrieveSchema(item, definitions));
@@ -292,49 +400,48 @@ class ArrayField extends Component {
items = items.concat(new Array(itemSchemas.length - items.length));
}
- return (
-
-
- {schema.description ?
- {schema.description}
: null}
- {
- items.map((item, index) => {
- const additional = index >= itemSchemas.length;
- const itemSchema = additional ?
- additionalSchema : itemSchemas[index];
- const itemIdPrefix = idSchema.$id + "_" + index;
- const itemIdSchema = toIdSchema(itemSchema, itemIdPrefix, definitions);
- const itemUiSchema = additional ?
- uiSchema.additionalItems || {} :
- Array.isArray(uiSchema.items) ?
- uiSchema.items[index] : uiSchema.items || {};
- const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
-
- return this.renderArrayFieldItem({
- index,
- canRemove: additional,
- canMoveUp: index >= itemSchemas.length + 1,
- canMoveDown: additional && index < items.length - 1,
- itemSchema,
- itemData: item,
- itemUiSchema,
- itemIdSchema,
- itemErrorSchema,
- autofocus: autofocus && index === 0
- });
- })
- }
- {
- canAdd ? : null
- }
-
- );
+ // These are the props passed into the render function
+ const arrayProps = {
+ canAdd,
+ className: "field field-array field-array-fixed-items",
+ disabled,
+ idSchema,
+ items: items.map((item, index) => {
+ const additional = index >= itemSchemas.length;
+ const itemSchema = additional ?
+ additionalSchema : itemSchemas[index];
+ const itemIdPrefix = idSchema.$id + "_" + index;
+ const itemIdSchema = toIdSchema(itemSchema, itemIdPrefix, definitions);
+ const itemUiSchema = additional ?
+ uiSchema.additionalItems || {} :
+ Array.isArray(uiSchema.items) ?
+ uiSchema.items[index] : uiSchema.items || {};
+ const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
+
+ return this.renderArrayFieldItem({
+ index,
+ canRemove: additional,
+ canMoveUp: index >= itemSchemas.length + 1,
+ canMoveDown: additional && index < items.length - 1,
+ itemSchema,
+ itemData: item,
+ itemUiSchema,
+ itemIdSchema,
+ itemErrorSchema,
+ autofocus: autofocus && index === 0
+ });
+ }),
+ onAddClick: this.onAddClick,
+ readonly,
+ required,
+ schema,
+ title,
+ TitleField
+ };
+
+ // Check if a custom template template was passed in
+ const renderFunction = ArrayFieldTemplate || DefaultFixedArrayFieldTemplate;
+ return renderFunction(arrayProps);
}
renderArrayFieldItem({
@@ -362,55 +469,33 @@ class ArrayField extends Component {
remove: removable && canRemove
};
has.toolbar = Object.keys(has).some(key => has[key]);
- const btnStyle = {flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold"};
- return (
-
-
-
-
- {
- has.toolbar ?
-
-
- {has.moveUp || has.moveDown ?
-
- : null}
- {has.moveUp || has.moveDown ?
-
- : null}
- {has.remove ?
-
- : null}
-
-
- : null
- }
-
- );
+ return {
+ children: (
+
+ ),
+ className: "array-item",
+ disabled,
+ hasToolbar: has.toolbar,
+ hasMoveUp: has.moveUp,
+ hasMoveDown: has.moveDown,
+ hasRemove: has.remove,
+ index,
+ onDropIndexClick: this.onDropIndexClick,
+ onReorderClick: this.onReorderClick,
+ readonly
+ };
}
}
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 266e183ad6..14aab87add 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -233,6 +233,7 @@ if (process.env.NODE_ENV !== "production") {
])).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
+ ArrayFieldTemplate: PropTypes.func,
FieldTemplate: PropTypes.func,
formContext: PropTypes.object.isRequired,
})
diff --git a/test/ArrayFieldTemplate_test.js b/test/ArrayFieldTemplate_test.js
new file mode 100644
index 0000000000..2fa744ffdf
--- /dev/null
+++ b/test/ArrayFieldTemplate_test.js
@@ -0,0 +1,142 @@
+import React from "react";
+
+import {expect} from "chai";
+import {createFormComponent, createSandbox} from "./test_utils";
+
+describe("ArrayFieldTemplate", () => {
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = createSandbox();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ describe("Custom ArrayFieldTemplate of string array", () => {
+
+ function ArrayFieldTemplate(props) {
+ return (
+
+ {props.canAdd &&
}
+ {props.items.map(element => {
+ return (
+
+ {element.hasMoveUp &&
+ }
+ {element.hasMoveDown &&
+ }
+
+ {element.children}
+
+ );
+ })}
+
+ );
+ }
+
+ const formData = ["one", "two", "three"];
+
+ describe("not fixed items", () => {
+ const schema = {
+ type: "array",
+ title: "my list",
+ description: "my description",
+ items: {type: "string"}
+ };
+
+ let node;
+ beforeEach(() => {
+ node = createFormComponent({
+ ArrayFieldTemplate,
+ formData,
+ schema
+ }).node;
+ });
+
+ it("should render one root element for the array", () => {
+ expect(node.querySelectorAll(".custom-array"))
+ .to.have.length.of(1);
+ });
+
+ it("should render one add button", () => {
+ expect(node.querySelectorAll(".custom-array-add"))
+ .to.have.length.of(1);
+ });
+
+ it("should render one child for each array item", () => {
+ expect(node.querySelectorAll(".custom-array-item"))
+ .to.have.length.of(formData.length);
+ });
+
+ it("should render text input for each array item", () => {
+ expect(node.querySelectorAll(".custom-array-item .field input[type=text]"))
+ .to.have.length.of(formData.length);
+ });
+
+ it("should render move up button for all but one array items", () => {
+ expect(node.querySelectorAll(".custom-array-item-move-up"))
+ .to.have.length.of(formData.length - 1);
+ });
+
+ it("should render move down button for all but one array items", () => {
+ expect(node.querySelectorAll(".custom-array-item-move-down"))
+ .to.have.length.of(formData.length - 1);
+ });
+ });
+
+ describe("fixed items", () => {
+ const schema = {
+ type: "array",
+ title: "my list",
+ description: "my description",
+ items: [
+ {type: "string"},
+ {type: "string"},
+ {type: "string"}
+ ]
+ };
+
+ let node;
+ beforeEach(() => {
+ node = createFormComponent({
+ ArrayFieldTemplate,
+ formData,
+ schema
+ }).node;
+ });
+
+ it("should render one root element for the array", () => {
+ expect(node.querySelectorAll(".custom-array"))
+ .to.have.length.of(1);
+ });
+
+ it("should not render an add button", () => {
+ expect(node.querySelectorAll(".custom-array-add"))
+ .to.have.length.of(0);
+ });
+
+ it("should render one child for each array item", () => {
+ expect(node.querySelectorAll(".custom-array-item"))
+ .to.have.length.of(formData.length);
+ });
+
+ it("should render text input for each array item", () => {
+ expect(node.querySelectorAll(".custom-array-item .field input[type=text]"))
+ .to.have.length.of(formData.length);
+ });
+
+ it("should not render any move up buttons", () => {
+ expect(node.querySelectorAll(".custom-array-item-move-up"))
+ .to.have.length.of(0);
+ });
+
+ it("should not render any move down buttons", () => {
+ expect(node.querySelectorAll(".custom-array-item-move-down"))
+ .to.have.length.of(0);
+ });
+ });
+
+ });
+});