Repository: mozilla-services/react-jsonschema-form
Pull request #876 (2018-03-26 16:25:25)

added passing of raw errors to ArrayField template and components
### Reasons for making this change

When I made change #826 I overlooked the fact that raw errors weren't being passed down from ArrayFields. I only came accross this when trying to integrate these errors into my custom components and ArrayFieldTemplate and seeing array level errors not being displayed (validation errors like min/max items etc).

If this is related to existing tickets, include links to them as well.

### Checklist

* [ ] **I'm updating documentation**
  - [ ] I've checked the rendering of the Markdown text I've added
  - [ ] If I'm adding a new section, I've updated the Table of Content
* [x] **I'm adding or updating code**
  - [x] I've added and/or updated tests
  - [ ] I've updated docs 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


llamamoray: @glasserc would really appreciate if you could take a look at this PR... something I overlooked in my change a few weeks ago - sorry!

Patch:
diff --git a/README.md b/README.md
index 1d4ea3050..aeb3048c0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ react-jsonschema-form
 
 [![Build Status](https://travis-ci.org/mozilla-services/react-jsonschema-form.svg)](https://travis-ci.org/mozilla-services/react-jsonschema-form)
 
-A simple [React](http://facebook.github.io/react/) component capable of building HTML forms out of a [JSON schema](http://jsonschema.net/) and using [Bootstrap](http://getbootstrap.com/) semantics by default.
+A simple [React](http://facebook.github.io/react/) component capable of building HTML forms out of a [JSON schema](http://json-schema.org/) and using [Bootstrap](http://getbootstrap.com/) semantics by default.
 
 A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) is hosted on gh-pages.
 
@@ -559,6 +559,51 @@ This will be rendered using a select box that way:
 
 Note that string representations of numbers will be cast back and reflected as actual numbers into form state.
 
+#### Alternative JSON-Schema compliant approach
+
+The JSON Schema team concluded an alternative approach instead of enumNames and react-jsonschema-form supports it as well.
+
+```js
+const schema = {
+  "type": "number",
+  "anyOf": [
+    {
+      "type": "number",
+      "title": "one",
+      "enum": [
+        1
+      ]
+    },
+    {
+      "type": "number",
+      "title": "two",
+      "enum": [
+        2
+      ]
+    },
+    {
+      "type": "number",
+      "title": "three",
+      "enum": [
+        3
+      ]
+    }
+  ]
+};
+```
+
+As above this will be rendered using a select box as so:
+
+```html
+<select>
+  <option value="1">one</option>
+  <option value="2">two</option>
+  <option value="3">three</option>
+</select>
+```
+
+A live example of both approaches side-by-side can be found in the **Alternatives** playground preset.
+
 ### Disabled attribute for `enum` fields
 
 This library supports the 'disabled' attribute for `enum` options. Enum disabled allows disabling options for 'enum' fields.This attribute can be added as a part of uiSchema.
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 97a04a5de..94fbabac6 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -366,6 +366,7 @@ class ArrayField extends Component {
       onBlur,
       onFocus,
       idPrefix,
+      rawErrors,
     } = this.props;
     const title = schema.title === undefined ? name : schema.title;
     const { ArrayFieldTemplate, definitions, fields, formContext } = registry;
@@ -411,6 +412,7 @@ class ArrayField extends Component {
       TitleField,
       formContext,
       formData,
+      rawErrors,
     };
 
     // Check if a custom render function was passed in
@@ -430,6 +432,7 @@ class ArrayField extends Component {
       onBlur,
       onFocus,
       registry = getDefaultRegistry(),
+      rawErrors,
     } = this.props;
     const items = this.props.formData;
     const { widgets, definitions, formContext } = registry;
@@ -454,6 +457,7 @@ class ArrayField extends Component {
         readonly={readonly}
         formContext={formContext}
         autofocus={autofocus}
+        rawErrors={rawErrors}
       />
     );
   }
@@ -470,6 +474,7 @@ class ArrayField extends Component {
       onBlur,
       onFocus,
       registry = getDefaultRegistry(),
+      rawErrors,
     } = this.props;
     const title = schema.title || name;
     const items = this.props.formData;
@@ -491,6 +496,7 @@ class ArrayField extends Component {
         readonly={readonly}
         formContext={formContext}
         autofocus={autofocus}
+        rawErrors={rawErrors}
       />
     );
   }
@@ -511,6 +517,7 @@ class ArrayField extends Component {
       registry = getDefaultRegistry(),
       onBlur,
       onFocus,
+      rawErrors,
     } = this.props;
     const title = schema.title || name;
     let items = this.props.formData;
@@ -579,6 +586,7 @@ class ArrayField extends Component {
       title,
       TitleField,
       formContext,
+      rawErrors,
     };
 
     // Check if a custom template template was passed in
@@ -600,6 +608,7 @@ class ArrayField extends Component {
       autofocus,
       onBlur,
       onFocus,
+      rawErrors,
     } = props;
     const {
       disabled,
@@ -636,6 +645,7 @@ class ArrayField extends Component {
           disabled={this.props.disabled}
           readonly={this.props.readonly}
           autofocus={autofocus}
+          rawErrors={rawErrors}
         />
       ),
       className: "array-item",


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index 9dc22e21c..4dba969d6 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -7,7 +7,9 @@ import { createFormComponent, createSandbox } from "./test_utils";
 
 describe("ArrayField", () => {
   let sandbox;
-  const CustomComponent = () => <div id="custom" />;
+  const CustomComponent = props => {
+    return <div id="custom">{props.rawErrors}</div>;
+  };
 
   beforeEach(() => {
     sandbox = createSandbox();
@@ -98,6 +100,29 @@ describe("ArrayField", () => {
       expect(node.querySelector("#custom")).to.exist;
     });
 
+    it("should pass rawErrors down to custom array field templates", () => {
+      const schema = {
+        type: "array",
+        title: "my list",
+        description: "my description",
+        items: { type: "string" },
+        minItems: 2,
+      };
+
+      const { node } = createFormComponent({
+        schema,
+        ArrayFieldTemplate: CustomComponent,
+        formData: [1],
+        liveValidate: true,
+      });
+
+      const matches = node.querySelectorAll("#custom");
+      expect(matches).to.have.length.of(1);
+      expect(matches[0].textContent).to.eql(
+        "should NOT have less than 2 items"
+      );
+    });
+
     it("should contain no field in the list by default", () => {
       const { node } = createFormComponent({ schema });
 
@@ -660,6 +685,23 @@ describe("ArrayField", () => {
 
         expect(node.querySelector("select").id).eql("root");
       });
+
+      it("should pass rawErrors down to custom widgets", () => {
+        const { node } = createFormComponent({
+          schema,
+          widgets: {
+            SelectWidget: CustomComponent,
+          },
+          formData: ["foo", "foo"],
+          liveValidate: true,
+        });
+
+        const matches = node.querySelectorAll("#custom");
+        expect(matches).to.have.length.of(1);
+        expect(matches[0].textContent).to.eql(
+          "should NOT have duplicate items (items ## 0 and 1 are identical)"
+        );
+      });
     });
 
     describe("CheckboxesWidget", () => {
@@ -729,6 +771,35 @@ describe("ArrayField", () => {
 
         expect(node.querySelectorAll(".checkbox-inline")).to.have.length.of(3);
       });
+
+      it("should pass rawErrors down to custom widgets", () => {
+        const schema = {
+          type: "array",
+          title: "My field",
+          items: {
+            enum: ["foo", "bar", "fuzz"],
+            type: "string",
+          },
+          minItems: 3,
+          uniqueItems: true,
+        };
+
+        const { node } = createFormComponent({
+          schema,
+          widgets: {
+            CheckboxesWidget: CustomComponent,
+          },
+          uiSchema,
+          formData: [],
+          liveValidate: true,
+        });
+
+        const matches = node.querySelectorAll("#custom");
+        expect(matches).to.have.length.of(1);
+        expect(matches[0].textContent).to.eql(
+          "should NOT have less than 3 items"
+        );
+      });
     });
   });
 
@@ -809,6 +880,33 @@ describe("ArrayField", () => {
 
       expect(node.querySelector("input[type=file]").id).eql("root");
     });
+
+    it("should pass rawErrors down to custom widgets", () => {
+      const schema = {
+        type: "array",
+        title: "My field",
+        items: {
+          type: "string",
+          format: "data-url",
+        },
+        minItems: 5,
+      };
+
+      const { node } = createFormComponent({
+        schema,
+        widgets: {
+          FileWidget: CustomComponent,
+        },
+        formData: [],
+        liveValidate: true,
+      });
+
+      const matches = node.querySelectorAll("#custom");
+      expect(matches).to.have.length.of(1);
+      expect(matches[0].textContent).to.eql(
+        "should NOT have less than 5 items"
+      );
+    });
   });
 
   describe("Nested lists", () => {
@@ -840,6 +938,51 @@ describe("ArrayField", () => {
 
       expect(node.querySelectorAll("fieldset fieldset")).to.have.length.of(1);
     });
+
+    it("should pass rawErrors down to every level of custom widgets", () => {
+      const CustomItem = props => <div id="custom-item">{props.children}</div>;
+      const CustomTemplate = props => {
+        return (
+          <div id="custom">
+            {props.items &&
+              props.items.map((p, i) => <CustomItem key={i} {...p} />)}
+            <div id="custom-error">
+              {props.rawErrors && props.rawErrors.join(", ")}
+            </div>
+          </div>
+        );
+      };
+
+      const schema = {
+        type: "array",
+        title: "A list of arrays",
+        items: {
+          type: "array",
+          title: "A list of numbers",
+          items: {
+            type: "number",
+          },
+          minItems: 3,
+        },
+        minItems: 2,
+      };
+
+      const { node } = createFormComponent({
+        schema,
+        ArrayFieldTemplate: CustomTemplate,
+        formData: [[]],
+        liveValidate: true,
+      });
+
+      const matches = node.querySelectorAll("#custom-error");
+      expect(matches).to.have.length.of(2);
+      expect(matches[0].textContent).to.eql(
+        "should NOT have less than 3 items"
+      );
+      expect(matches[1].textContent).to.eql(
+        "should NOT have less than 2 items"
+      );
+    });
   });
 
   describe("Fixed items lists", () => {