Repository: mozilla-services/react-jsonschema-form
Pull request #1102 (2018-12-08 18:47:54)

Give ids to array and object fields
This change:
- Gives ids to the `<fieldset>` tag in the default array field template and object field template
- Gives the id of "root" to the topmost `<fieldset>` in the form.

### Reasons for making this change
This way, we are more consistent with the idSchema, so that every id mentioned in the idSchema actually has a corresponding element rendered in the DOM. This allows us to create anchor links that go to specific elements, even if they are not `<input>` or `<select>` elements.

For example, on the "Arrays" example in the [playground](https://mozilla-services.github.io/react-jsonschema-form/), here is the idSchema:
![image](https://user-images.githubusercontent.com/1689183/49689443-32618f80-fad6-11e8-898c-059e242185fa.png)
However, there are no elements with ids equal to "root", "root_fixedItemsList", etc. This change fixes that.

This change was especially motivated by looking into #86 -- sometimes you may have a validation error that refers to an *array* element (such as minimum items not being reached). If there is no corresponding DOM ID corresponding to the idSchema value for that element, then we cannot create anchor links that go to the specific element with the error.

### 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
  - [x] 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/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..05a45f582 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}
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..43fb98f79 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", () => {
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", () => {