Repository: mozilla-services/react-jsonschema-form
Pull request #1125 (2019-01-06 22:44:07)

Handle sort arrays better
### Reasons for making this change

The current sortfunctions for arrayFields will only work if you shuffle an item 1 step up or down. If you create your own ArrayFieldTemplate with drag and drop, users can move items in larger steps, and this library will at this time not handle this. 

This pull simply sorts the moved array item from its old index to its new index.


### 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



Patch:
diff --git a/README.md b/README.md
index b59790607..156567213 100644
--- a/README.md
+++ b/README.md
@@ -1063,7 +1063,9 @@ The following props are passed to each `ObjectFieldTemplate`:
 - `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it).
 - `title`: A string value containing the title for the object.
 - `description`: A string value containing the description for the object.
+- `disabled`: A boolean value stating if the object is disabled.
 - `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below.
+- `readonly`: A boolean value stating if the object is read-only.
 - `required`: A boolean value stating if the object is required.
 - `schema`: The schema object for this object.
 - `uiSchema`: The uiSchema object for this object field.
@@ -1088,15 +1090,13 @@ An error list template is basically a React stateless component being passed err
 function ErrorListTemplate(props) {
   const {errors} = props;
   return (
-    <div>
-      {errors.map((error, i) => {
-        return (
-          <li key={i}>
+    <ul>
+      {errors.map(error => (
+          <li key={error.stack}>
             {error.stack}
           </li>
-        );
-      })}
-    </div>
+        ))}
+    </ul>
   );
 }
 
@@ -1261,7 +1261,7 @@ render((
 
 #### Customizing widgets text input
 
-All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individially, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration).
+All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration).
 
 ### Custom field components
 
@@ -1869,7 +1869,7 @@ $ git commit -m "Bump version $VERSION"
 $ git tag v$VERSION
 $ npm run dist
 $ npm publish
-$ git push --tags origin
+$ git push --tags origin master
 ```
 
 ## FAQ
diff --git a/package.json b/package.json
index 1db7ca591..d9d93b62a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-jsonschema-form",
-  "version": "1.0.7",
+  "version": "1.1.0",
   "description": "A simple React component capable of building HTML forms out of a JSON schema.",
   "scripts": {
     "build:readme": "toctoc README.md -w",
diff --git a/playground/app.js b/playground/app.js
index bf4f868c3..2a8dffdb6 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -357,6 +357,9 @@ class App extends Component {
   load = data => {
     // Reset the ArrayFieldTemplate whenever you load new data
     const { ArrayFieldTemplate, ObjectFieldTemplate } = data;
+    // uiSchema is missing on some examples. Provide a default to
+    // clear the field in all cases.
+    const { uiSchema = {} } = data;
     // force resetting form component instance
     this.setState({ form: false }, _ =>
       this.setState({
@@ -364,6 +367,7 @@ class App extends Component {
         form: true,
         ArrayFieldTemplate,
         ObjectFieldTemplate,
+        uiSchema,
       })
     );
   };
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index a85bd6fa0..9537a635d 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -103,7 +103,7 @@ function DefaultArrayItem(props) {
 
 function DefaultFixedArrayFieldTemplate(props) {
   return (
-    <fieldset className={props.className}>
+    <fieldset className={props.className} id={props.idSchema.$id}>
       <ArrayFieldTitle
         key={`array-field-title-${props.idSchema.$id}`}
         TitleField={props.TitleField}
@@ -139,7 +139,7 @@ function DefaultFixedArrayFieldTemplate(props) {
 
 function DefaultNormalArrayFieldTemplate(props) {
   return (
-    <fieldset className={props.className}>
+    <fieldset className={props.className} id={props.idSchema.$id}>
       <ArrayFieldTitle
         key={`array-field-title-${props.idSchema.$id}`}
         TitleField={props.TitleField}
@@ -276,20 +276,19 @@ class ArrayField extends Component {
           }
         }
       }
-      onChange(
-        formData.map((item, i) => {
-          // i is string, index and newIndex are numbers,
-          // so using "==" to compare
-          if (i == newIndex) {
-            return formData[index];
-          } else if (i == index) {
-            return formData[newIndex];
-          } else {
-            return item;
-          }
-        }),
-        newErrorSchema
-      );
+
+      function reOrderArray() {
+        // Copy item
+        let newFormData = formData.slice();
+
+        // Moves item from index to newIndex
+        newFormData.splice(index, 1);
+        newFormData.splice(newIndex, 0, formData[index]);
+
+        return newFormData;
+      }
+
+      onChange(reOrderArray(), newErrorSchema);
     };
   };
 
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 982aa3aaf..813273962 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -30,7 +30,7 @@ function DefaultObjectFieldTemplate(props) {
 
   const { TitleField, DescriptionField } = props;
   return (
-    <fieldset>
+    <fieldset id={props.idSchema.$id}>
       {(props.uiSchema["ui:title"] || props.title) && (
         <TitleField
           id={`${props.idSchema.$id}__title`}
@@ -82,22 +82,17 @@ class ObjectField extends Component {
 
   onPropertyChange = (name, addedByAdditionalProperties = false) => {
     return (value, errorSchema) => {
-      let newFormData;
-      //section below sets zero value of input field to empty string
-      //instead of undefined, so that value input in additionalProperties
-      //doesn't disappear when emptied
       if (!value && addedByAdditionalProperties) {
-        newFormData = {
-          ...this.props.formData,
-          [name]: "",
-        };
-      } else {
-        newFormData = {
-          ...this.props.formData,
-          [name]: value,
-        };
+        // Don't set value = undefined for fields added by
+        // additionalProperties. Doing so removes them from the
+        // formData, which causes them to completely disappear
+        // (including the input field for the property name). Unlike
+        // fields which are "mandated" by the schema, these fields can
+        // be set to undefined by clicking a "delete field" button, so
+        // set empty values to the empty string.
+        value = "";
       }
-
+      const newFormData = { ...this.props.formData, [name]: value };
       this.props.onChange(
         newFormData,
         errorSchema &&
@@ -109,7 +104,7 @@ class ObjectField extends Component {
     };
   };
 
-  onDropIndexClick = key => {
+  onDropPropertyClick = key => {
     return event => {
       event.preventDefault();
       const { onChange, formData } = this.props;
@@ -250,7 +245,7 @@ class ObjectField extends Component {
               registry={registry}
               disabled={disabled}
               readonly={readonly}
-              onDropIndexClick={this.onDropIndexClick}
+              onDropPropertyClick={this.onDropPropertyClick}
             />
           ),
           name,
@@ -259,6 +254,8 @@ class ObjectField extends Component {
           required,
         };
       }),
+      readonly,
+      disabled,
       required,
       idSchema,
       uiSchema,
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 1d10983c0..2afb66f47 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -121,7 +121,7 @@ function DefaultTemplate(props) {
     required,
     displayLabel,
     onKeyChange,
-    onDropIndexClick,
+    onDropPropertyClick,
   } = props;
   if (hidden) {
     return children;
@@ -164,7 +164,7 @@ function DefaultTemplate(props) {
               tabIndex="-1"
               style={{ border: "0" }}
               disabled={props.disabled || props.readonly}
-              onClick={onDropIndexClick(props.label)}
+              onClick={onDropPropertyClick(props.label)}
             />
           )}
         </div>
@@ -208,7 +208,7 @@ function SchemaFieldRender(props) {
     idPrefix,
     name,
     onKeyChange,
-    onDropIndexClick,
+    onDropPropertyClick,
     required,
     registry = getDefaultRegistry(),
   } = props;
@@ -308,7 +308,7 @@ function SchemaFieldRender(props) {
     label,
     hidden,
     onKeyChange,
-    onDropIndexClick,
+    onDropPropertyClick,
     required,
     disabled,
     readonly,
diff --git a/src/validate.js b/src/validate.js
index 3c8273e7a..12e0a2fca 100644
--- a/src/validate.js
+++ b/src/validate.js
@@ -162,6 +162,8 @@ export default function validateFormData(
   }
 
   let errors = transformAjvErrors(ajv.errors);
+  // Clear errors to prevent persistent errors, see #1104
+  ajv.errors = null;
 
   if (typeof transformErrors === "function") {
     errors = transformErrors(errors);


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index 40e7f90b3..299b67090 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -41,7 +41,9 @@ describe("ArrayField", () => {
     it("should render a fieldset", () => {
       const { node } = createFormComponent({ schema });
 
-      expect(node.querySelectorAll("fieldset")).to.have.length.of(1);
+      const fieldset = node.querySelectorAll("fieldset");
+      expect(fieldset).to.have.length.of(1);
+      expect(fieldset[0].id).eql("root");
     });
 
     it("should render a fieldset legend", () => {
@@ -270,6 +272,50 @@ describe("ArrayField", () => {
       expect(inputs[2].value).eql("bar");
     });
 
+    it("should move from first to last in the list", () => {
+      function moveAnywhereArrayItemTemplate(props) {
+        const buttons = [];
+        for (let i = 0; i < 3; i++) {
+          buttons.push(
+            <button
+              key={i}
+              className={"array-item-move-to-" + i}
+              onClick={props.onReorderClick(props.index, i)}>
+              {"Move item to index " + i}
+            </button>
+          );
+        }
+        return (
+          <div key={props.index} className={"item-" + props.index}>
+            {props.children}
+            {buttons}
+          </div>
+        );
+      }
+
+      function moveAnywhereArrayFieldTemplate(props) {
+        return (
+          <div className="array">
+            {props.items.map(moveAnywhereArrayItemTemplate)}
+          </div>
+        );
+      }
+
+      const { node } = createFormComponent({
+        schema,
+        formData: ["foo", "bar", "baz"],
+        ArrayFieldTemplate: moveAnywhereArrayFieldTemplate,
+      });
+
+      const button = node.querySelector(".item-0 .array-item-move-to-2");
+      Simulate.click(button);
+
+      const inputs = node.querySelectorAll(".field-string input[type=text]");
+      expect(inputs[0].value).eql("bar");
+      expect(inputs[1].value).eql("baz");
+      expect(inputs[2].value).eql("foo");
+    });
+
     it("should disable move buttons on the ends of the list", () => {
       const { node } = createFormComponent({
         schema,
diff --git a/test/Form_test.js b/test/Form_test.js
index 85a39d7f8..793e3a075 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -127,6 +127,7 @@ describe("Form", () => {
         ids.push(input.getAttribute("id"));
       }
       expect(ids).to.eql(["rjsf_count"]);
+      expect(node.querySelector("fieldset").id).to.eql("rjsf");
     });
   });
 
@@ -151,6 +152,7 @@ describe("Form", () => {
         ids.push(input.getAttribute("id"));
       }
       expect(ids).to.eql(["rjsf_count"]);
+      expect(node.querySelector("fieldset").id).to.eql("rjsf");
     });
 
     it("should work with oneOf", function() {
diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js
index b546ba55b..2e115813d 100644
--- a/test/ObjectField_test.js
+++ b/test/ObjectField_test.js
@@ -40,7 +40,9 @@ describe("ObjectField", () => {
     it("should render a fieldset", () => {
       const { node } = createFormComponent({ schema });
 
-      expect(node.querySelectorAll("fieldset")).to.have.length.of(1);
+      const fieldset = node.querySelectorAll("fieldset");
+      expect(fieldset).to.have.length.of(1);
+      expect(fieldset[0].id).eql("root");
     });
 
     it("should render a fieldset legend", () => {