Repository: mozilla-services/react-jsonschema-form (js)

Issue:
Inline checkboxes and radios
### Prerequisites

- [x] I have read the [documentation](https://github.com/mozilla-services/react-jsonschema-form/blob/master/README.md#readme).

### Description

I would like to display inputs with `type="radio"` or `type="checkbox"` with [inline markup from Bootstrap](http://getbootstrap.com/css/#inline-checkboxes-and-radios). 

Is there a way to add option like `{ "ui:inline": true }` to enable this flag or I have to override the whole widget?    

### Version

0.40.0

Pull request:
Fix #346: Add support for inline radios and checkboxes.
This patch enables having inline checkboxes and radio buttons. 

![demo](https://cloud.githubusercontent.com/assets/41547/19487448/251a8b7c-9563-11e6-94e7-105d8b5d19d1.gif)

- [x] List of checkboxes
- [x] List of radio buttons
- [x] Boolean field radio buttons case
- [x] Docs
- [x] Tests


Patch:
diff --git a/README.md b/README.md
index 1f5c0aed2..464211b56 100644
--- a/README.md
+++ b/README.md
@@ -476,6 +476,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..548fe22a1 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}
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..af4503770 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -355,6 +355,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",