Repository: mozilla-services/react-jsonschema-form
Pull request #498 (2017-02-28 21:17:02)

Show description for boolean fields
### Reasons for making this change

A fix for #435 . I chose to present the description in a similar way like the other fields, with an extra text above the input. The suggested `title` looked a bit "strange" since it was the only input with that kind of behavior and it would probably have issues in a mobile view. 

### 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**
  - [ ] I've added and/or updated tests
  - [ ] I've updated docs if needed
* [x] **I'm adding a new feature**
  - [x] I've updated the playground with an example use of the feature



Patch:
diff --git a/README.md b/README.md
index 121c9cd48..22661da3f 100644
--- a/README.md
+++ b/README.md
@@ -1321,7 +1321,7 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html)
  - Conditional fields: https://jsfiddle.net/69z2wepo/68259/
  - Use radio list for enums: https://jsfiddle.net/f2y3fq7L/2/
  - Reading file input data: https://jsfiddle.net/f9vcb6pL/1/
- 
+ - Custom errors messages with transformErrors : https://jsfiddle.net/revolunet/5r3swnr4/
  
  
 ## Contributing
diff --git a/playground/samples/widgets.js b/playground/samples/widgets.js
index e02da6a06..4327fe4b1 100644
--- a/playground/samples/widgets.js
+++ b/playground/samples/widgets.js
@@ -26,15 +26,18 @@ module.exports = {
         properties: {
           default: {
             type: "boolean",
-            title: "checkbox (default)"
+            title: "checkbox (default)",
+            description: "This is the checkbox-description"
           },
           radio: {
             type: "boolean",
-            title: "radio buttons"
+            title: "radio buttons",
+            description: "This is the radio-description"
           },
           select: {
             type: "boolean",
-            title: "select box"
+            title: "select box",
+            description: "This is the select-description"
           }
         }
       },
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 5344b2f80..4dff3f486 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -11,7 +11,6 @@ import {
   optionsList,
   retrieveSchema,
   toIdSchema,
-  shouldRender,
   getDefaultRegistry
 } from "../../utils";
 
@@ -159,10 +158,6 @@ class ArrayField extends Component {
     autofocus: false,
   };
 
-  shouldComponentUpdate(nextProps, nextState) {
-    return shouldRender(this, nextProps, nextState);
-  }
-
   get itemTitle() {
     const {schema} = this.props;
     return schema.items.title || schema.items.description || "Item";
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 97bb12966..4b048c806 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -3,7 +3,6 @@ import React, {Component, PropTypes} from "react";
 import {
   orderProperties,
   retrieveSchema,
-  shouldRender,
   getDefaultRegistry
 } from "../../utils";
 
@@ -19,10 +18,6 @@ class ObjectField extends Component {
     readonly: false,
   }
 
-  shouldComponentUpdate(nextProps, nextState) {
-    return shouldRender(this, nextProps, nextState);
-  }
-
   isRequired(name) {
     const schema = this.props.schema;
     return Array.isArray(schema.required) &&
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 141d7fc88..47e6422a8 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -4,7 +4,8 @@ import {
   isMultiSelect,
   retrieveSchema,
   getDefaultRegistry,
-  isFilesArray
+  isFilesArray,
+  deepEquals
 } from "../../utils";
 import UnsupportedField from "./UnsupportedField";
 
@@ -128,7 +129,7 @@ DefaultTemplate.defaultProps = {
   displayLabel: true,
 };
 
-function SchemaField(props) {
+function SchemaFieldRender(props) {
   const {uiSchema, errorSchema, idSchema, name, required, registry} = props;
   const {definitions, fields, formContext, FieldTemplate = DefaultTemplate} = registry;
   const schema = retrieveSchema(props.schema, definitions);
@@ -182,7 +183,7 @@ function SchemaField(props) {
     "form-group",
     "field",
     `field-${type}`,
-    errors && errors.length > 0 ? "field-error has-error" : "",
+    errors && errors.length > 0 ? "field-error has-error has-danger" : "",
     uiSchema.classNames,
   ].join(" ").trim();
 
@@ -211,6 +212,21 @@ function SchemaField(props) {
   return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
 }
 
+class SchemaField extends React.Component {
+  shouldComponentUpdate(nextProps, nextState) {
+    // if schemas are equal idSchemas will be equal as well,
+    // so it is not necessary to compare
+    return !deepEquals(
+      {...this.props, idSchema: undefined},
+      {...nextProps,  idSchema: undefined}
+    );
+  }
+
+  render() {
+    return SchemaFieldRender(this.props);
+  }
+}
+
 SchemaField.defaultProps = {
   uiSchema: {},
   errorSchema: {},
diff --git a/src/components/widgets/CheckboxWidget.js b/src/components/widgets/CheckboxWidget.js
index d65c66355..db742ced4 100644
--- a/src/components/widgets/CheckboxWidget.js
+++ b/src/components/widgets/CheckboxWidget.js
@@ -1,5 +1,5 @@
 import React, {PropTypes} from "react";
-
+import DescriptionField from "../fields/DescriptionField.js";
 
 function CheckboxWidget({
   schema,
@@ -13,6 +13,7 @@ function CheckboxWidget({
 }) {
   return (
     <div className={`checkbox ${disabled ? "disabled" : ""}`}>
+      { schema.description && <DescriptionField description={ schema.description }/> }
       <label>
         <input type="checkbox"
           id={id}


Test patch:
diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js
index 54831dc95..6c84d6573 100644
--- a/test/BooleanField_test.js
+++ b/test/BooleanField_test.js
@@ -47,6 +47,16 @@ describe("BooleanField", () => {
       .to.have.length.of(1);
   });
 
+  it("should render a description", () => {
+    const {node} = createFormComponent({schema: {
+      type: "boolean",
+      description: "my description"
+    }});
+
+    const description = node.querySelector(".field-description");
+    expect(description.textContent).eql("my description");
+  });
+
   it("should assign a default value", () => {
     const {node} = createFormComponent({schema: {
       type: "boolean",
diff --git a/test/performance_test.js b/test/performance_test.js
index bb4f5d827..505d8ba9c 100644
--- a/test/performance_test.js
+++ b/test/performance_test.js
@@ -1,9 +1,8 @@
 import sinon from "sinon";
 import React from "react";
-
+import {scryRenderedComponentsWithType} from "react-addons-test-utils";
 import {getDefaultRegistry} from "../src/utils";
-import ArrayField from "../src/components/fields/ArrayField";
-import ObjectField from "../src/components/fields/ObjectField";
+import SchemaField from "../src/components/fields/SchemaField";
 import {
   createComponent,
   createFormComponent,
@@ -47,51 +46,61 @@ describe("Rendering performance optimizations", () => {
 
       sinon.assert.notCalled(comp.render);
     });
-  });
 
-  describe("ArrayField", () => {
-    const onChange = () => {};
-    const onBlur = () => {};
-    const schema = {type: "array", items: {type: "string"}};
-    const uiSchema = {};
-    const registry = getDefaultRegistry();
+    it("should only render changed object properties", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          const: {type: "string"},
+          var: {type: "string"}
+        }
+      };
 
-    it("should not render if next props are equivalent", () => {
-      const props = {
-        registry,
+      const {comp} = createFormComponent({
         schema,
-        uiSchema,
-        onChange,
-        onBlur
-      };
+        formData: {const: "0", var: "0"}
+      });
 
-      const {comp} = createComponent(ArrayField, props);
-      sandbox.stub(comp, "render").returns(<div/>);
+      const fields = scryRenderedComponentsWithType(comp, SchemaField)
+        .reduce( (fields, fieldComp) => {
+          sandbox.stub(fieldComp, "render").returns(<div/>);
+          fields[fieldComp.props.idSchema.$id] = fieldComp;
+          return fields;
+        });
 
-      setProps(comp, props);
+      setProps(comp, {schema, formData: {const: "0", var: "1"}});
 
-      sinon.assert.notCalled(comp.render);
+      sinon.assert.notCalled(fields.root_const.render);
+      sinon.assert.calledOnce(fields.root_var.render);
     });
 
-    it("should not render if next formData are equivalent", () => {
-      const props = {
-        registry,
-        schema,
-        formData: ["a", "b"],
-        onChange,
-        onBlur
+    it("should only render changed array items", () => {
+      const schema = {
+        type: "array",
+        items: {type: "string"}
       };
 
-      const {comp} = createComponent(ArrayField, props);
-      sandbox.stub(comp, "render").returns(<div/>);
+      const {comp} = createFormComponent({
+        schema,
+        formData: ["const", "var0"]
+      });
 
-      setProps(comp, {...props, formData: ["a", "b"]});
+      const fields = scryRenderedComponentsWithType(comp, SchemaField)
+        .reduce( (fields, fieldComp) => {
+          sandbox.stub(fieldComp, "render").returns(<div/>);
+          fields[fieldComp.props.idSchema.$id] = fieldComp;
+          return fields;
+        });
+
+      setProps(comp, {schema, formData: ["const", "var1"]});
+
+      sinon.assert.notCalled(fields.root_0.render);
+      sinon.assert.calledOnce(fields.root_1.render);
 
-      sinon.assert.notCalled(comp.render);
     });
   });
 
-  describe("ObjectField", () => {
+  describe("SchemaField", () => {
     const onChange = () => {};
     const onBlur = () => {};
     const registry = getDefaultRegistry();
@@ -114,7 +123,7 @@ describe("Rendering performance optimizations", () => {
         onBlur
       };
 
-      const {comp} = createComponent(ObjectField, props);
+      const {comp} = createComponent(SchemaField, props);
       sandbox.stub(comp, "render").returns(<div/>);
 
       setProps(comp, props);
@@ -132,7 +141,7 @@ describe("Rendering performance optimizations", () => {
         onBlur
       };
 
-      const {comp} = createComponent(ObjectField, props);
+      const {comp} = createComponent(SchemaField, props);
       sandbox.stub(comp, "render").returns(<div/>);
 
       setProps(comp, {...props, formData: {foo: "blah"}});