Repository: mozilla-services/react-jsonschema-form
Pull request #345 (2016-10-18 14:00:54)

Array field optional sortability
In a complex form the sortability makes the form a bit cluttered and is often not needed.

I added a whole new object `ui:options` to the ArrayField's uiSchema for flexibility in case we add other options to ArrayField in the future.

olzraiti: I renamed the option to 'orderable'.

Patch:
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
index 48fccb5a9..8252c8ee5 100644
--- a/ISSUE_TEMPLATE.md
+++ b/ISSUE_TEMPLATE.md
@@ -1,6 +1,7 @@
 ### Prerequisites
 
-- [ ] I have read the [documentation](https://github.com/mozilla-services/react-jsonschema-form/blob/master/README.md#readme).
+- [ ] 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
 
@@ -12,6 +13,8 @@
 2. [Second Step]
 3. [and so on...]
 
+Ideally, I'm providing a [sample JSFiddle](https://jsfiddle.net/n1k0/f2y3fq7L/6/) demonstrating the issue.
+
 #### Expected behavior
 
 [What you expected to happen]
diff --git a/README.md b/README.md
index 1f5c0aed2..50c00f731 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
         - [File widgets](#file-widgets)
            - [Multiple files](#multiple-files)
      - [Object fields ordering](#object-fields-ordering)
+     - [Array items ordering](#array-items-ordering)
      - [Custom CSS class names](#custom-css-class-names)
      - [Custom labels for enum fields](#custom-labels-for-enum-fields)
      - [Multiple choices list](#multiple-choices-list)
@@ -399,6 +400,24 @@ render((
         uiSchema={uiSchema} />
 ), document.getElementById("app"));
 ```
+### Array items ordering
+
+Array items are orderable by default, and react-jsonschema-form renders move up/down buttons alongside them. The `uiSchema` object spec allows you to disable ordering:
+
+```jsx
+const schema = {
+  type: "array",
+  properties: {
+    type: "string"
+  }
+};
+
+const uiSchema = {
+  "ui:options":  {
+    orderable: false
+  }
+};
+```
 
 ### Custom CSS class names
 
@@ -476,6 +495,19 @@ const uiSchema = {
 };
 ```
 
+By default, checkboxes are stacked but if you prefer them inline:
+
+```js
+const uiSchema = {
+  "ui:widget": {
+    component: "checkboxes",
+    options: {
+      inline: true
+    }
+  }
+};
+```
+
 See the "Arrays" section of the playground for cool demos.
 
 ### Autogenerated widget ids
diff --git a/playground/samples/numbers.js b/playground/samples/numbers.js
index f7fefaf9d..02a3d21b1 100644
--- a/playground/samples/numbers.js
+++ b/playground/samples/numbers.js
@@ -41,7 +41,12 @@ module.exports = {
       "ui:widget": "updown"
     },
     numberEnumRadio: {
-      "ui:widget": "radio"
+      "ui:widget": {
+        component: "radio",
+        options: {
+          inline: true
+        }
+      }
     },
     integerRange: {
       "ui:widget": "range"
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index d007e64a8..c85768b3f 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -1,6 +1,7 @@
 import React, { Component, PropTypes } from "react";
 
 import {
+  getAlternativeWidget,
   getDefaultFormState,
   isMultiSelect,
   isFilesArray,
@@ -13,9 +14,7 @@ import {
   getDefaultRegistry,
   setState
 } from "../../utils";
-import SelectWidget from "./../widgets/SelectWidget";
 import FileWidget from "./../widgets/FileWidget";
-import CheckboxesWidget from "./../widgets/CheckboxesWidget";
 
 
 function ArrayFieldTitle({TitleField, idSchema, title, required}) {
@@ -213,17 +212,19 @@ class ArrayField extends Component {
   renderMultiSelect() {
     const {schema, idSchema, uiSchema, disabled, readonly, autofocus} = this.props;
     const {items} = this.state;
-    const {definitions} = this.props.registry;
+    const {widgets, definitions} = this.props.registry;
     const itemsSchema = retrieveSchema(schema.items, definitions);
 
-    const multipleCheckboxes = uiSchema["ui:widget"] === "checkboxes";
-    const Widget = (multipleCheckboxes) ? CheckboxesWidget : SelectWidget;
+    const Widget = getAlternativeWidget(schema, uiSchema["ui:widget"] || "select", widgets);
     return (
       <Widget
         id={idSchema && idSchema.$id}
         multiple
         onChange={this.onSelectChange}
-        options={{enumOptions: optionsList(itemsSchema)}}
+        options={{
+          ...Widget.defaultProps.options,
+          enumOptions: optionsList(itemsSchema),
+        }}
         schema={schema}
         value={items}
         disabled={disabled}
@@ -337,8 +338,15 @@ class ArrayField extends Component {
     autofocus
   }) {
     const {SchemaField} = this.props.registry.fields;
-    const {disabled, readonly} = this.props;
-    const hasToolbar = removable || canMoveUp || canMoveDown;
+    const {disabled, readonly, uiSchema} = this.props;
+
+    const {orderable} = {orderable: true, ...uiSchema["ui:options"]};
+
+    const _canMoveUp = orderable && canMoveUp;
+    const _canMoveDown = orderable && canMoveDown;
+
+    const hasToolbar = removable || _canMoveUp || _canMoveDown;
+
     const btnStyle = {flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold"};
 
     return (
@@ -361,18 +369,18 @@ class ArrayField extends Component {
           hasToolbar ?
             <div className="col-xs-2 array-item-toolbox text-right">
               <div className="btn-group" style={{display: "flex"}}>
-                {canMoveUp || canMoveDown ?
+                {_canMoveUp || _canMoveDown ?
                   <button type="button" className="btn btn-default array-item-move-up"
                           style={btnStyle}
                           tabIndex="-1"
-                          disabled={disabled || readonly || !canMoveUp}
+                          disabled={disabled || readonly || !_canMoveUp}
                           onClick={this.onReorderClick(index, index - 1)}>⬆</button>
                   : null}
-                {canMoveUp || canMoveDown ?
+                {_canMoveUp || _canMoveDown ?
                   <button type="button" className="btn btn-default array-item-move-down"
                           style={btnStyle}
                           tabIndex="-1"
-                          disabled={disabled || readonly || !canMoveDown}
+                          disabled={disabled || readonly || !_canMoveDown}
                           onClick={this.onReorderClick(index, index + 1)}>⬇</button>
                   : null}
                 {removable ?
@@ -406,7 +414,11 @@ function AddButton({onClick, disabled}) {
 if (process.env.NODE_ENV !== "production") {
   ArrayField.propTypes = {
     schema: PropTypes.object.isRequired,
-    uiSchema: PropTypes.object,
+    uiSchema: PropTypes.shape({
+      "ui:options": PropTypes.shape({
+        orderable: PropTypes.bool
+      })
+    }),
     idSchema: PropTypes.object,
     errorSchema: PropTypes.object,
     onChange: PropTypes.func.isRequired,
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index c4cf043c3..a936a1a93 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -4,13 +4,19 @@ import {
   defaultFieldValue,
   getAlternativeWidget,
   optionsList,
-  getDefaultRegistry
+  getDefaultRegistry,
+  isObject,
 } from "../../utils";
 import CheckboxWidget from "./../widgets/CheckboxWidget";
 
 
-function buildOptions(schema) {
+function buildOptions(schema, uiWidget) {
+  // Note: uiWidget can be undefined, a string or an object; we only deal with
+  // the inline option when we're provided a definition object.
   return {
+    inline: isObject(uiWidget) &&
+            isObject(uiWidget.options) &&
+            uiWidget.options.inline,
     enumOptions: optionsList(Object.assign({
       enumNames: ["true", "false"],
       enum: [true, false]
@@ -50,7 +56,7 @@ function BooleanField(props) {
   };
   if (widget) {
     const Widget = getAlternativeWidget(schema, widget, widgets);
-    return <Widget options={buildOptions(schema)} {...commonProps} />;
+    return <Widget options={buildOptions(schema, uiSchema["ui:widget"])} {...commonProps} />;
   }
   return <CheckboxWidget {...commonProps} />;
 }
diff --git a/src/components/widgets/CheckboxesWidget.js b/src/components/widgets/CheckboxesWidget.js
index 389f99177..76bbb1ce5 100644
--- a/src/components/widgets/CheckboxesWidget.js
+++ b/src/components/widgets/CheckboxesWidget.js
@@ -15,13 +15,13 @@ function deselectValue(value, selected) {
 
 function CheckboxesWidget(props) {
   const {id, disabled, options, value, autofocus, onChange} = props;
-  const {enumOptions} = options;
+  const {enumOptions, inline} = options;
   return (
     <div className="checkboxes" id={id}>{
       enumOptions.map((option, index) => {
         const checked = value.indexOf(option.value) !== -1;
         return (
-          <div key={index} className="checkbox">
+          <div key={index} className={`checkbox${inline ? "-inline" : ""}`}>
             <label>
               <input type="checkbox"
                 id={`${id}_${index}`}
@@ -47,6 +47,9 @@ function CheckboxesWidget(props) {
 
 CheckboxesWidget.defaultProps = {
   autofocus: false,
+  options: {
+    inline: false
+  },
 };
 
 if (process.env.NODE_ENV !== "production") {
@@ -55,6 +58,7 @@ if (process.env.NODE_ENV !== "production") {
     id: PropTypes.string.isRequired,
     options: PropTypes.shape({
       enumOptions: PropTypes.array,
+      inline: PropTypes.bool,
     }).isRequired,
     value: PropTypes.any,
     required: PropTypes.bool,
diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js
index 7f06d8a03..687289a73 100644
--- a/src/components/widgets/RadioWidget.js
+++ b/src/components/widgets/RadioWidget.js
@@ -12,13 +12,13 @@ function RadioWidget({
 }) {
   // Generating a unique field name to identify this set of radio buttons
   const name = Math.random().toString();
-  const {enumOptions} = options;
+  const {enumOptions, inline} = options;
   return (
     <div className="field-radio-group">{
       enumOptions.map((option, i) => {
         const checked = option.value === value;
         return (
-          <div key={i} className={`radio ${disabled ? "disabled" : ""}`}>
+          <div key={i} className={`radio${inline ? "-inline" : ""} ${disabled ? "disabled" : ""}`}>
             <label>
               <input type="radio"
                 name={name}
@@ -46,6 +46,7 @@ if (process.env.NODE_ENV !== "production") {
     id: PropTypes.string.isRequired,
     options: PropTypes.shape({
       enumOptions: PropTypes.array,
+      inline: PropTypes.bool,
     }).isRequired,
     value: PropTypes.any,
     required: PropTypes.bool,
diff --git a/src/utils.js b/src/utils.js
index 19692b7dd..30c4b3e90 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -54,6 +54,7 @@ const altWidgetMap = {
     hidden: HiddenWidget,
   },
   array: {
+    select: SelectWidget,
     checkboxes: CheckboxesWidget,
   }
 };
@@ -101,7 +102,14 @@ export function getAlternativeWidget(
   const {type, format} = schema;
 
   function setDefaultOptions(widget) {
-    widget.defaultProps = {...widget.defaultProps, options: widgetOptions};
+    const {defaultProps={}} = widget;
+    widget.defaultProps = {
+      ...defaultProps,
+      options: {
+        ...defaultProps.options,
+        ...widgetOptions
+      }
+    };
     return widget;
   }
 


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index 27806e232..c058a357f 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -157,6 +157,15 @@ describe("ArrayField", () => {
       expect(moveDownBtns[1].disabled).eql(true);
     });
 
+    it("should not show move up/down buttons is orderable is false", () => {
+      const {node} = createFormComponent({schema, formData: ["foo", "bar"], uiSchema: {"ui:options": {orderable: false}}});
+      const moveUpBtns = node.querySelector(".array-item-move-up");
+      const moveDownBtns = node.querySelector(".array-item-move-down");
+
+      expect(moveUpBtns).to.be.null;
+      expect(moveDownBtns).to.be.null;
+    });
+
     it("should remove a field from the list", () => {
       const {node} = createFormComponent({schema, formData: ["foo", "bar"]});
       const dropBtns = node.querySelectorAll(".array-item-remove");
@@ -355,6 +364,23 @@ describe("ArrayField", () => {
 
         expect(node.querySelector(".checkboxes").id).eql("root");
       });
+
+      it("should support inline checkboxes", () => {
+        const {node} = createFormComponent({
+          schema,
+          uiSchema: {
+            "ui:widget": {
+              component: "checkboxes",
+              options: {
+                inline: true
+              }
+            }
+          }
+        });
+
+        expect(node.querySelectorAll(".checkbox-inline"))
+          .to.have.length.of(3);
+      });
     });
   });
 
diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js
index 39da87bc6..91bc27b48 100644
--- a/test/BooleanField_test.js
+++ b/test/BooleanField_test.js
@@ -94,6 +94,24 @@ describe("BooleanField", () => {
     expect(labels).eql(["Yes", "No"]);
   });
 
+  it("should support inline radio widgets", () => {
+    const {node} = createFormComponent({
+      schema: {type: "boolean"},
+      formData: true,
+      uiSchema: {
+        "ui:widget": {
+          component: "radio",
+          options: {
+            inline: true
+          }
+        }
+      }
+    });
+
+    expect(node.querySelectorAll(".radio-inline"))
+      .to.have.length.of(2);
+  });
+
   it("should support enumNames for select", () => {
     const {node} = createFormComponent({schema: {
       type: "boolean",