diff --git a/docs/advanced-customization.md b/docs/advanced-customization.md
index f72613255d..ce0e7c44b9 100644
--- a/docs/advanced-customization.md
+++ b/docs/advanced-customization.md
@@ -3,11 +3,11 @@
_ | Custom Field | Custom Template | Custom Widget
--|---------- | ------------- | ----
-What it does | Overrides all behaviour | Overrides just the layout | Overrides just the input box (not layout, labels, or help, or validation)
-Usage | Global or per-field | Only global | Global or per-field
-Global Example | `
` | `` | ``
-Per-Field Example | `"ui:field": MyField` | N/A | `"ui:widget":MyWidget`
-Documentation | [Field](#field-props) | [Field Template](#field-template) - [Array Template](#array-field-template) - [Object Template](#object-field-template) - [Error List Template](#error-list-template) | [Custom Widgets](#custom-widget-components)
+**What it does** | Overrides all behaviour | Overrides just the layout (not behaviour) | Overrides just the input box (not layout, labels, or help, or validation)
+**Usage** | Global or per-field | Global or per-field | Global or per-field
+**Global Example** | `` | `` | ``
+**Per-Field Example** | `"ui:field": MyCustomField` | `"ui:ArrayFieldTemplate": MyArrayTemplate` | `"ui:widget":MyCustomWidget`
+**Documentation** | [Field](#field-props) | [Field Template](#field-template) - [Array Template](#array-field-template) - [Object Template](#object-field-template) - [Error List Template](#error-list-template) | [Custom Widgets](#custom-widget-components)
### Field template
@@ -35,6 +35,19 @@ render((
), document.getElementById("app"));
```
+You also can provide your own field template to a uiSchema by specifying a `ui:FieldTemplate` property.
+
+```jsx
+const uiSchema = {
+ "ui:FieldTemplate": CustomFieldTemplate
+}
+
+render((
+ ,
+), document.getElementById("app"));
+```
+
If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`.
The following props are passed to a custom field template component:
@@ -59,7 +72,7 @@ The following props are passed to a custom field template component:
- `uiSchema`: The uiSchema object for this field.
- `formContext`: The `formContext` object that you passed to Form.
-> 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.
+> Note: you can only define a single global field template for a form, but you can set individual field templates per property using `"ui:FieldTemplate"`.
### Array Field Template
@@ -82,8 +95,22 @@ render((
), document.getElementById("app"));
```
+You also can provide your own field template to a uiSchema by specifying a `ui:ArrayFieldTemplate` property.
+
+```jsx
+const uiSchema = {
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate
+}
+
+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 `DescriptionField` from the registry (in case you wanted to utilize it)
@@ -117,6 +144,8 @@ The following props are part of each element in `items`:
- `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 read-only.
+> Note: Array and object field templates are always rendered inside of the FieldTemplate. To fully customize an array field template, you may need to specify both `ui:FieldTemplate` and `ui:ArrayFieldTemplate`.
+
### Object Field Template
Similarly to the `FieldTemplate` you can use an `ObjectFieldTemplate` to customize how your
@@ -139,6 +168,19 @@ render((
), document.getElementById("app"));
```
+You also can provide your own field template to a uiSchema by specifying a `ui:ObjectFieldTemplate` property.
+
+```jsx
+const uiSchema = {
+ "ui:ObjectFieldTemplate": ObjectFieldTemplate
+}
+
+render((
+ ,
+), document.getElementById("app"));
+```
+
Please see [customObject.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customObject.js) for a better example.
The following props are passed to each `ObjectFieldTemplate`:
@@ -164,6 +206,9 @@ The following props are part of each element in `properties`:
- `disabled`: A boolean value stating if the object property is disabled.
- `readonly`: A boolean value stating if the property is read-only.
+> Note: Array and object field templates are always rendered inside of the FieldTemplate. To fully customize an object field template, you may need to specify both `ui:FieldTemplate` and `ui:ObjectFieldTemplate`.
+
+
### Error List template
To take control over how the form errors are displayed, you can define an *error list template* for your form. This list is the form global error list that appears at the top of your forms.
diff --git a/package.json b/package.json
index ca28d0ec5c..320767512d 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,8 @@
"build:lib": "rimraf lib && cross-env NODE_ENV=production babel -d lib/ src/",
"build:dist": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.dist.js",
"build:playground": "rimraf build && cross-env NODE_ENV=production webpack --config webpack.config.prod.js && cp playground/index.prod.html build/index.html",
- "cs-check": "prettier -l $npm_package_prettierOptions '{playground,src,test}/**/*.js'",
- "cs-format": "prettier --jsx-bracket-same-line --trailing-comma es5 --use-tabs false --semi --tab-width 2 '{playground,src,test}/**/*.js' --write",
+ "cs-check": "prettier -l $npm_package_prettierOptions \"{playground,src,test}/**/*.js\"",
+ "cs-format": "prettier --jsx-bracket-same-line --trailing-comma es5 --use-tabs false --semi --tab-width 2 \"{playground,src,test}/**/*.js\" --write",
"dist": "npm run build:lib && npm run build:dist",
"lint": "eslint src test playground",
"prepare": "npm run dist",
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index c15b3d768b..7863b94a88 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -487,7 +487,10 @@ class ArrayField extends Component {
};
// Check if a custom render function was passed in
- const Component = ArrayFieldTemplate || DefaultNormalArrayFieldTemplate;
+ const Component =
+ uiSchema["ui:ArrayFieldTemplate"] ||
+ ArrayFieldTemplate ||
+ DefaultNormalArrayFieldTemplate;
return ;
}
@@ -670,7 +673,10 @@ class ArrayField extends Component {
};
// Check if a custom template template was passed in
- const Template = ArrayFieldTemplate || DefaultFixedArrayFieldTemplate;
+ const Template =
+ uiSchema["ui:ArrayFieldTemplate"] ||
+ ArrayFieldTemplate ||
+ DefaultFixedArrayFieldTemplate;
return ;
}
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 8189536516..c9bcce4459 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -212,7 +212,11 @@ class ObjectField extends Component {
);
}
- const Template = registry.ObjectFieldTemplate || DefaultObjectFieldTemplate;
+ const Template =
+ uiSchema["ui:ObjectFieldTemplate"] ||
+ registry.ObjectFieldTemplate ||
+ DefaultObjectFieldTemplate;
+
const templateProps = {
title: uiSchema["ui:title"] || title,
description,
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 0b7038afcf..593a19868e 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -236,12 +236,9 @@ function SchemaFieldRender(props) {
required,
registry = getDefaultRegistry(),
} = props;
- const {
- definitions,
- fields,
- formContext,
- FieldTemplate = DefaultTemplate,
- } = registry;
+ const { definitions, fields, formContext } = registry;
+ const FieldTemplate =
+ uiSchema["ui:FieldTemplate"] || registry.FieldTemplate || DefaultTemplate;
let idSchema = props.idSchema;
const schema = retrieveSchema(props.schema, definitions, formData);
idSchema = mergeObjects(
diff --git a/test/ArrayFieldTemplate_test.js b/test/ArrayFieldTemplate_test.js
index be0f05257d..609c7330b8 100644
--- a/test/ArrayFieldTemplate_test.js
+++ b/test/ArrayFieldTemplate_test.js
@@ -38,6 +38,66 @@ describe("ArrayFieldTemplate", () => {
);
}
+ describe("Stateful ArrayFieldTemplate", () => {
+ class ArrayFieldTemplate extends PureComponent {
+ render() {
+ return (
+
+ {this.props.items.map((item, i) => (
+
item.children
+ ))}
+
+ );
+ }
+ }
+
+ describe("with template globally configured", () => {
+ it("should render a stateful custom component", () => {
+ const { node } = createFormComponent({
+ schema: { type: "array", items: { type: "string" } },
+ formData,
+ ArrayFieldTemplate,
+ });
+
+ expect(
+ node.querySelectorAll(".field-array .field-content div")
+ ).to.have.length.of(3);
+ });
+ });
+ describe("with template configured in ui:ArrayFieldTemplate", () => {
+ it("should render a stateful custom component", () => {
+ const { node } = createFormComponent({
+ schema: { type: "array", items: { type: "string" } },
+ formData,
+ uiSchema: {
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate,
+ },
+ });
+
+ expect(
+ node.querySelectorAll(".field-array .field-content div")
+ ).to.have.length.of(3);
+ });
+ });
+ describe("with template configured globally being overriden in ui:ArrayFieldTemplate", () => {
+ it("should render a stateful custom component", () => {
+ const { node } = createFormComponent({
+ schema: { type: "array", items: { type: "string" } },
+ formData,
+ uiSchema: {
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate,
+ },
+ // Empty field template for proof that we're doing overrides
+ ArrayFieldTemplate: () => ,
+ });
+
+ expect(
+ node.querySelectorAll(".field-array .field-content div")
+ ).to.have.length.of(3);
+ });
+ });
+ });
+
describe("not fixed items", () => {
const schema = {
type: "array",
@@ -46,52 +106,91 @@ describe("ArrayFieldTemplate", () => {
items: { type: "string" },
};
- const uiSchema = {
- classNames: "custom-array",
- };
-
let node;
- beforeEach(() => {
- node = createFormComponent({
- ArrayFieldTemplate,
- formData,
- schema,
- uiSchema,
- }).node;
- });
+ describe("with template globally configured", () => {
+ const uiSchema = {
+ classNames: "custom-array",
+ };
- it("should render one root element for the array", () => {
- expect(node.querySelectorAll(".custom-array")).to.have.length.of(1);
- });
+ beforeEach(() => {
+ node = createFormComponent({
+ ArrayFieldTemplate,
+ formData,
+ schema,
+ uiSchema,
+ }).node;
+ });
- it("should render one add button", () => {
- expect(node.querySelectorAll(".custom-array-add")).to.have.length.of(1);
+ sharedIts();
});
+ describe("with template configured in ui:ArrayFieldTemplate", () => {
+ const uiSchema = {
+ classNames: "custom-array",
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate,
+ };
- it("should render one child for each array item", () => {
- expect(node.querySelectorAll(".custom-array-item")).to.have.length.of(
- formData.length
- );
+ beforeEach(() => {
+ node = createFormComponent({
+ formData,
+ schema,
+ uiSchema,
+ }).node;
+ });
+ sharedIts();
});
+ describe("with template configured globally being overriden in ui:ArrayFieldTemplate", () => {
+ const uiSchema = {
+ classNames: "custom-array",
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate,
+ };
- 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);
+ beforeEach(() => {
+ node = createFormComponent({
+ formData,
+ schema,
+ uiSchema,
+ // Empty field template for proof that we're doing overrides
+ ArrayFieldTemplate: () => ,
+ }).node;
+ });
+ sharedIts();
});
+ function sharedIts() {
+ it("should render one root element for the array", () => {
+ expect(node.querySelectorAll(".custom-array")).to.have.length.of(1);
+ });
- 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 one add button", () => {
+ expect(node.querySelectorAll(".custom-array-add")).to.have.length.of(
+ 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);
- });
+ 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", () => {
@@ -102,52 +201,88 @@ describe("ArrayFieldTemplate", () => {
items: [{ type: "string" }, { type: "string" }, { type: "string" }],
};
- const uiSchema = {
- classNames: "custom-array",
- };
-
let node;
- beforeEach(() => {
- node = createFormComponent({
- ArrayFieldTemplate,
- formData,
- schema,
- uiSchema,
- }).node;
+ describe("with template globally configured", () => {
+ const uiSchema = {
+ classNames: "custom-array",
+ };
+ beforeEach(() => {
+ node = createFormComponent({
+ formData,
+ schema,
+ uiSchema,
+ ArrayFieldTemplate,
+ }).node;
+ });
+ sharedIts();
});
- it("should render one root element for the array", () => {
- expect(node.querySelectorAll(".custom-array")).to.have.length.of(1);
+ describe("with template configured in ui:ArrayFieldTemplate", () => {
+ const uiSchema = {
+ classNames: "custom-array",
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate,
+ };
+ beforeEach(() => {
+ node = createFormComponent({
+ formData,
+ schema,
+ uiSchema,
+ }).node;
+ });
+ sharedIts();
});
-
- it("should not render an add button", () => {
- expect(node.querySelectorAll(".custom-array-add")).to.have.length.of(0);
+ describe("with template configured globally being overriden in ui:ArrayFieldTemplate", () => {
+ const uiSchema = {
+ classNames: "custom-array",
+ "ui:ArrayFieldTemplate": ArrayFieldTemplate,
+ };
+ beforeEach(() => {
+ node = createFormComponent({
+ formData,
+ schema,
+ uiSchema,
+ // Empty field template for proof that we're doing overrides
+ ArrayFieldTemplate: () => ,
+ }).node;
+ });
+ sharedIts();
});
+ function sharedIts() {
+ it("should render one root element for the array", () => {
+ expect(node.querySelectorAll(".custom-array")).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 not render an add button", () => {
+ expect(node.querySelectorAll(".custom-array-add")).to.have.length.of(
+ 0
+ );
+ });
- 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 one child for each array item", () => {
+ expect(node.querySelectorAll(".custom-array-item")).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 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 down buttons", () => {
- expect(
- node.querySelectorAll(".custom-array-item-move-down")
- ).to.have.length.of(0);
- });
+ 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);
+ });
+ }
});
});
diff --git a/test/FieldTemplate_test.js b/test/FieldTemplate_test.js
index cd6d288b5b..372a061f84 100644
--- a/test/FieldTemplate_test.js
+++ b/test/FieldTemplate_test.js
@@ -19,22 +19,62 @@ describe("FieldTemplate", () => {
return ;
}
- it("should render with disabled when ui:disabled is truthy", () => {
- const { node } = createFormComponent({
- schema: { type: "string" },
- uiSchema: { "ui:disabled": true },
- FieldTemplate,
+ describe("with template globally configured", () => {
+ it("should render with disabled when ui:disabled is truthy", () => {
+ const { node } = createFormComponent({
+ schema: { type: "string" },
+ uiSchema: { "ui:disabled": true },
+ FieldTemplate,
+ });
+ expect(node.querySelectorAll(".disabled")).to.have.length.of(1);
+ });
+
+ it("should render with disabled when ui:disabled is falsey", () => {
+ const { node } = createFormComponent({
+ schema: { type: "string" },
+ uiSchema: { "ui:disabled": false },
+ FieldTemplate,
+ });
+ expect(node.querySelectorAll(".disabled")).to.have.length.of(0);
});
- expect(node.querySelectorAll(".disabled")).to.have.length.of(1);
});
+ describe("with template configured in ui:FieldTemplate", () => {
+ it("should render with disabled when ui:disabled is truthy", () => {
+ const { node } = createFormComponent({
+ schema: { type: "string" },
+ uiSchema: { "ui:disabled": true, "ui:FieldTemplate": FieldTemplate },
+ });
+ expect(node.querySelectorAll(".disabled")).to.have.length.of(1);
+ });
+
+ it("should render with disabled when ui:disabled is falsey", () => {
+ const { node } = createFormComponent({
+ schema: { type: "string" },
+ uiSchema: { "ui:disabled": false, "ui:FieldTemplate": FieldTemplate },
+ });
+ expect(node.querySelectorAll(".disabled")).to.have.length.of(0);
+ });
+ });
+ describe("with template configured globally being overriden in ui:FieldTemplate", () => {
+ it("should render with disabled when ui:disabled is truthy", () => {
+ const { node } = createFormComponent({
+ schema: { type: "string" },
+ uiSchema: { "ui:disabled": true, "ui:FieldTemplate": FieldTemplate },
+ // Empty field template to prove that overides work
+ FieldTemplate: () => ,
+ });
+ expect(node.querySelectorAll(".disabled")).to.have.length.of(1);
+ });
- it("should render with disabled when ui:disabled is falsey", () => {
- const { node } = createFormComponent({
- schema: { type: "string" },
- uiSchema: { "ui:disabled": false },
- FieldTemplate,
+ it("should render with disabled when ui:disabled is falsey", () => {
+ const { node } = createFormComponent({
+ schema: { type: "string" },
+ uiSchema: { "ui:disabled": false, "ui:FieldTemplate": FieldTemplate },
+ // Empty field template to prove that overides work
+ FieldTemplate: () => ,
+ });
+ expect(node.querySelectorAll(".disabled")).to.have.length.of(0);
});
- expect(node.querySelectorAll(".disabled")).to.have.length.of(0);
});
});
});
diff --git a/test/ObjectFieldTemplate_test.js b/test/ObjectFieldTemplate_test.js
index d5e5cc2d34..aa1b2b6c44 100644
--- a/test/ObjectFieldTemplate_test.js
+++ b/test/ObjectFieldTemplate_test.js
@@ -45,33 +45,76 @@ describe("ObjectFieldTemplate", () => {
const DescriptionField = ({ description }) =>
description ? : null;
- const { node } = createFormComponent({
- schema: {
- type: "object",
- properties: { foo: { type: "string" }, bar: { type: "string" } },
- },
- uiSchema: { "ui:description": "foobar" },
- formData,
- ObjectFieldTemplate,
- fields: {
- TitleField,
- DescriptionField,
- },
+ let node;
+ describe("with template globally configured", () => {
+ node = createFormComponent({
+ schema: {
+ type: "object",
+ properties: { foo: { type: "string" }, bar: { type: "string" } },
+ },
+ uiSchema: { "ui:description": "foobar" },
+ formData,
+ ObjectFieldTemplate,
+ fields: {
+ TitleField,
+ DescriptionField,
+ },
+ }).node;
+ sharedIts();
});
-
- it("should render one root element", () => {
- expect(node.querySelectorAll(".root")).to.have.length.of(1);
+ describe("with template configured in ui:ObjectFieldTemplate", () => {
+ node = createFormComponent({
+ schema: {
+ type: "object",
+ properties: { foo: { type: "string" }, bar: { type: "string" } },
+ },
+ uiSchema: {
+ "ui:description": "foobar",
+ "ui:ObjectFieldTemplate": ObjectFieldTemplate,
+ },
+ formData,
+ fields: {
+ TitleField,
+ DescriptionField,
+ },
+ }).node;
+ sharedIts();
});
-
- it("should render one title", () => {
- expect(node.querySelectorAll(".title-field")).to.have.length.of(1);
+ describe("with template configured globally overridden by ui:ObjectFieldTemplate", () => {
+ node = createFormComponent({
+ schema: {
+ type: "object",
+ properties: { foo: { type: "string" }, bar: { type: "string" } },
+ },
+ uiSchema: {
+ "ui:description": "foobar",
+ "ui:ObjectFieldTemplate": ObjectFieldTemplate,
+ },
+ formData,
+ ObjectFieldTemplate: () => , // Empty object field template, proof that it's overridden
+ fields: {
+ TitleField,
+ DescriptionField,
+ },
+ }).node;
+ sharedIts();
});
- it("should render one description", () => {
- expect(node.querySelectorAll(".description-field")).to.have.length.of(1);
- });
+ function sharedIts() {
+ it("should render one root element", () => {
+ expect(node.querySelectorAll(".root")).to.have.length.of(1);
+ });
- it("should render two property containers", () => {
- expect(node.querySelectorAll(".property")).to.have.length.of(2);
- });
+ it("should render one title", () => {
+ expect(node.querySelectorAll(".title-field")).to.have.length.of(1);
+ });
+
+ it("should render one description", () => {
+ expect(node.querySelectorAll(".description-field")).to.have.length.of(1);
+ });
+
+ it("should render two property containers", () => {
+ expect(node.querySelectorAll(".property")).to.have.length.of(2);
+ });
+ }
});