Repository: mozilla-services/react-jsonschema-form (js)

Issue:
"ui:template" schema option
### Prerequisites

- [x] I have read the [documentation](https://github.com/mozilla-services/react-jsonschema-form/blob/master/README.md#readme);
- [ ] In the case of a bug report, I understand that providing a [SSCCE](http://sscce.org/) example is tremendously useful to the maintainers.

### Description

For more theming control a `"ui:template"` option would be helpful. From what I can tell this would be a fairly simple matter to integrate three places:
 - `SchemaField`
 - `ObjectField`
 - `ArrayField`

This would build on the work in #304.

#### Expected behavior

People could use custom templates to customize layout. This would be the most pertinent for objects and arrays, but can only be useful for leaf nodes too.

```
const uiSchema = {
  "ui:template": GroupedObjectTemplate,
  title: {
    "ui:template": InlineField
  }
  done: {
    "ui:template": ErrorsInTooltip
  },
  comments: {
    "ui:template": InlineArray
  }
};
```

### Alternative 1 - External plugin

Currently as a workaround this behaviour can be (almost) achieved externally. The biggest blocker to making it easier to achieve externally is that the default template objects are not exported.

```
import React from "react";
// TODO: Would be useful to 
import {
  DefaultObjectFieldTemplate,
  DefaultFieldTemplate,
  DefaultArrayItem
} from "./DefaultTemplates";

export function FieldTemplate(props) {
  const Template = props.uiSchema["ui:template"];
  if (
    Template &&
    // TODO: LV: Seems strange that this is required to be plugged in... oh well
    props.schema.type !== "object" &&
    props.schema.type !== "array"
  ) {
    return <Template {...props} />;
  } else {
    return <DefaultFieldTemplate {...props} />;
  }
}

export const ObjectFieldTemplate = defaultOrComponent(
  DefaultObjectFieldTemplate
);
export const ArrayFieldTemplate = defaultOrComponent(DefaultArrayItem);

function defaultOrComponent(DefaultTemplate) {
  return function(props) {
    const Template = props.uiSchema["ui:template"];
    if (Template) {
      console.error("Can't render", Template, props);
      return <Template {...props} />;
    } else {
      return <DefaultTemplate {...props} />;
    }
  };
}
```
And then use that on your `Form`
```
  import * as UiTemplate from "...";
  
  const fm = <Form
    schema={schema}
    uiSchema={uiSchema}
    onChange={log("changed")}
    onSubmit={log("submitted")}
    onError={log("errors")}
    transformErrors={log("transformErrors")}
    liveValidate={true}
    formContext={{
      templates: Templates.GroupTemplates
    }}
    {...UiTemplate}
  >
```

### Alternative 2 - "ui:field"

"ui:field" is very powerful, but it requires too much re-implementation of core behaviour to get right, particularly for things like arrays and objects.


## Recommendation

I would recommend tackling this in phases, getting simpler functionality merged in earlier, and complex functionality merged in later.

*Phase 1* allow templates as objects

 - Before: `const Template = registry.ObjectFieldTemplate || DefaultObjectFieldTemplate;`
 - After: `const Template = uiSchema["ui:template"] || registry.ObjectFieldTemplate || DefaultObjectFieldTemplate;`

*Phase 2* allow templates as strings, add `templates` to registry, allow custom templates map, add default templates to default registry

*Phase 3* themes for other ui libraries

## Resolution

If the maintainers of this repository are on-board, then I can work on a pull request.

Pull request description:
Added `ui:*Template` properties
### Reasons for making this change

Makes customizing templates for a form a more granular experience.

Resolves #1010

### Checklist

* [ ] **I'm updating documentation**
  - [ ] I've [checked the rendering](https://react-jsonschema-form.readthedocs.io/en/latest/#contributing) of the Markdown text I've added
* [x] **I'm adding or updating code**
  - [x] I've added and/or updated tests
  - [ ] I've updated [docs](https://react-jsonschema-form.readthedocs.io/) if needed
  - [x] I've run `npm run cs-format` on my branch to conform my code to [prettier](https://github.com/prettier/prettier) coding style
* [ ] **I'm adding a new feature**
  - [ ] I've updated the playground with an example use of the feature


Patch:
diff --git a/docs/advanced-customization.md b/docs/advanced-customization.md
index f72613255..ce0e7c44b 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 | `<Form fields={MyCustomFields} />` |  `<Form ArrayFieldTemplate={ArrayFieldTemplate} />` | `<Form widgets={MyCustomWidgets} />`
-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** | `<Form fields={MyCustomField} />` |  `<Form ArrayFieldTemplate={MyArrayTemplate} />` | `<Form widgets={MyCustomWidget} />`
+**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((
+  <Form schema={schema}
+        uiSchema={uiSchema} />,
+), 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((
+  <Form schema={schema}
+        uiSchema={uiSchema} />,
+), 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((
+  <Form schema={schema}
+        uiSchema={uiSchema} />,
+), 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 ca28d0ec5..320767512 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/playground/app.js b/playground/app.js
index 54ad5dca6..b2a240d58 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -27,6 +27,7 @@ const liveSettingsSchema = {
     validate: { type: "boolean", title: "Live validation" },
     disable: { type: "boolean", title: "Disable whole form" },
     omitExtraData: { type: "boolean", title: "Omit extra data" },
+    liveOmit: { type: "boolean", title: "Live omit" },
   },
 };
 const cmOptions = {
@@ -334,6 +335,7 @@ class App extends Component {
         validate: true,
         disable: false,
         omitExtraData: false,
+        liveOmit: false,
       },
       shareURL: null,
     };
@@ -475,6 +477,7 @@ class App extends Component {
               liveValidate={liveSettings.validate}
               disabled={liveSettings.disable}
               omitExtraData={liveSettings.omitExtraData}
+              liveOmit={liveSettings.liveOmit}
               schema={schema}
               uiSchema={uiSchema}
               formData={formData}
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index c15b3d768..7863b94a8 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 <Component {...arrayProps} />;
   }
 
@@ -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 <Template {...arrayProps} />;
   }
 
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 818953651..c9bcce445 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 0b7038afc..593a19868 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(


Test patch:
diff --git a/test/ArrayFieldTemplate_test.js b/test/ArrayFieldTemplate_test.js
index be0f05257..609c7330b 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 (
+            <div className="field-content">
+              {this.props.items.map((item, i) => (
+                <div key={i}>item.children</div>
+              ))}
+            </div>
+          );
+        }
+      }
+
+      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: () => <div />,
+          });
+
+          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: () => <div />,
+          }).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: () => <div />,
+          }).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 cd6d288b5..372a061f8 100644
--- a/test/FieldTemplate_test.js
+++ b/test/FieldTemplate_test.js
@@ -19,22 +19,62 @@ describe("FieldTemplate", () => {
       return <div className={props.disabled ? "disabled" : "foo"} />;
     }
 
-    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: () => <div />,
+        });
+        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: () => <div />,
+        });
+        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 d5e5cc2d3..aa1b2b6c4 100644
--- a/test/ObjectFieldTemplate_test.js
+++ b/test/ObjectFieldTemplate_test.js
@@ -45,33 +45,76 @@ describe("ObjectFieldTemplate", () => {
   const DescriptionField = ({ description }) =>
     description ? <div className="description-field" /> : 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: () => <div />, // 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);
+    });
+  }
 });