Repository: mozilla-services/react-jsonschema-form
Pull request #341 (2016-10-13 08:45:03)

Pass data to FieldTemplate as strings instead of as React components
This adds flexibility.

An example use case would be when you want the help block to be a tooltip of the label. Then you don't want the help object to be a React component, but a string instead so you can render a tooltip for the label with the help string as the content.

I think this is also more consistent, since we are rendering the label already in TemplateField instead of passing it as a React component.

I exposed `Label`, `Help` and `ErrorList` components from SchemaField with `export`, because they are probably useful for writing a custom TemplateField - should we put these to a separate file?


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/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..897689e1e
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,16 @@
+### Reasons for making this change
+
+[Please describe them here]
+
+If this is related to existing tickets, include links to them as well.
+
+### 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
+* [ ] **I'm adding or updating code**
+  - [ ] I've added and/or updated tests
+  - [ ] I've updated docs if needed
+* [ ] **I'm adding a new feature**
+  - [ ] I've updated the playground with an example use of the feature
diff --git a/README.md b/README.md
index 05fd26a84..d11629d90 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)
@@ -297,6 +298,7 @@ Please note that while standardized, `datetime-local` and `date` input elements
 
   * `updown`: an `input[type=number]` updown selector;
   * `range`: an `input[type=range]` slider;
+  * `radio`: a radio button group with enum values. **can only be used when `enum` values are specified for this input**
   * by default, a regular `input[type=text]` element is used.
 
 > Note: for numbers, `min`, `max` and `step` input attributes values will be handled according to JSONSchema's `minimum`, `maximium` and `multipleOf` values when they're defined.
@@ -376,6 +378,12 @@ const schema = {
 
 > Note that storing large dataURIs into form state might slow rendering.
 
+##### File widget input ref
+
+The included `FileWidget` exposes a reference to the `<input type="file" />` element node as an `inputRef` component property.
+
+This allows you to programmatically trigger the browser's file selector which can be used in a custom file widget.
+
 ### Object fields ordering
 
 The `uiSchema` object spec also allows you to define in which order a given object field properties should be rendered using the `ui:order` property:
@@ -398,6 +406,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
 
@@ -475,6 +501,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
@@ -593,22 +632,28 @@ render((
 ), document.getElementById("app"));
 ```
 
+If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`.
+
 The following props are passed to a custom field template component:
 
-- `id`: The id of the field in the hierarchy. You can use it to render a label targetting the wrapped widget;
-- `classNames`: A string containing the base bootstrap CSS classes merged with any [custom ones](#custom-css-class-names) defined in your uiSchema;
-- `label`: The computed label for this field, as a string;
-- `description`: A component instance rendering the field description, if any defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined);
-- `children`: The field or widget component instance for this field row;
-- `errors`: A component instance listing any encountered errors for this field;
-- `help`: A component instance rendering any `ui:help` uiSchema directive defined;
-- `hidden`: A boolean value stating if the field should be hidden;
-- `required`: A boolean value stating if the field is required;
-- `readonly`: A boolean value stating if the field is read-only;
+- `id`: The id of the field in the hierarchy. You can use it to render a label targetting the wrapped widget.
+- `classNames`: A string containing the base bootstrap CSS classes merged with any [custom ones](#custom-css-class-names) defined in your uiSchema.
+- `label`: The computed label for this field, as a string.
+- `description`: A component instance rendering the field description, if any defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined).
+- `rawDescription`: A string containing any `ui:description` uiSchema directive defined.
+- `children`: The field or widget component instance for this field row.
+- `errors`: A component instance listing any encountered errors for this field.
+- `rawErrors`: An array of strings listing all generated error messages from encountered errors for this field.
+- `help`: A component instance rendering any `ui:help` uiSchema directive defined.
+- `rawHelp`: A string containing any `ui:help` uiSchema directive defined. **NOTE:** `rawHelp` will be `undefined` if passed `ui:help` is a React component instead of a string.
+- `hidden`: A boolean value stating if the field should be hidden.
+- `required`: A boolean value stating if the field is required.
+- `readonly`: A boolean value stating if the field is read-only.
 - `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI.
+- `fields`: An array containing all Form's fields including your [custom fields](#custom-field-components) and the built-in fields.
 - `formContext`: The `formContext` object that you passed to Form.
 
-> Note: you can only define a single field template for a form. If you need many, it's probably time to look for [custom fields](#custom-field-components) instead.
+> Note: you can only define a single field template for a form. If you need many, it's probably time to look at [custom fields](#custom-field-components) instead.
 
 ### Custom widgets and fields
 
diff --git a/playground/samples/numbers.js b/playground/samples/numbers.js
index 35997ba38..02a3d21b1 100644
--- a/playground/samples/numbers.js
+++ b/playground/samples/numbers.js
@@ -16,6 +16,11 @@ module.exports = {
         title: "Number enum",
         enum: [1, 2, 3]
       },
+      numberEnumRadio: {
+        type: "number",
+        title: "Number enum",
+        enum: [1, 2, 3]
+      },
       integerRange: {
         title: "Integer range",
         type: "integer",
@@ -35,6 +40,14 @@ module.exports = {
     integer: {
       "ui:widget": "updown"
     },
+    numberEnumRadio: {
+      "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 d2baf391b..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}) {
@@ -167,7 +166,7 @@ class ArrayField extends Component {
       readonly,
       autofocus,
     } = this.props;
-    const title = schema.title || name;
+    const title = (schema.title === undefined) ? name : schema.title;
     const {items} = this.state;
     const {definitions, fields} = this.props.registry;
     const {TitleField, DescriptionField} = fields;
@@ -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 a8616219a..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]
@@ -39,7 +45,7 @@ function BooleanField(props) {
     schema,
     id: idSchema && idSchema.$id,
     onChange,
-    label: title || name,
+    label: (title === undefined) ? name : title,
     value: defaultFieldValue(formData, schema),
     required,
     disabled,
@@ -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/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 27485da5d..394ab16cc 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -96,7 +96,7 @@ class ObjectField extends Component {
     const {definitions, fields, formContext} = this.props.registry;
     const {SchemaField, TitleField, DescriptionField} = fields;
     const schema = retrieveSchema(this.props.schema, definitions);
-    const title = schema.title || name;
+    const title = (schema.title === undefined) ? name : schema.title;
     let orderedProperties;
     try {
       const properties = Object.keys(schema.properties);
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index f03c4e851..59c48f6c5 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -13,7 +13,6 @@ import ObjectField from "./ObjectField";
 import StringField from "./StringField";
 import UnsupportedField from "./UnsupportedField";
 
-
 const REQUIRED_FIELD_SYMBOL = "*";
 const COMPONENT_TYPES = {
   array:   ArrayField,
@@ -93,6 +92,7 @@ function DefaultTemplate(props) {
   if (hidden) {
     return children;
   }
+
   return (
     <div className={classNames}>
       {displayLabel ? <Label label={label} required={required} id={id} /> : null}
@@ -111,12 +111,16 @@ if (process.env.NODE_ENV !== "production") {
     label: PropTypes.string,
     children: PropTypes.node.isRequired,
     errors: PropTypes.element,
+    rawErrors: PropTypes.arrayOf(PropTypes.string),
     help: PropTypes.element,
+    rawHelp: PropTypes.string,
     description: PropTypes.element,
+    rawDescription: PropTypes.string,
     hidden: PropTypes.bool,
     required: PropTypes.bool,
     readonly: PropTypes.bool,
     displayLabel: PropTypes.bool,
+    fields: PropTypes.object,
     formContext: PropTypes.object,
   };
 }
@@ -185,8 +189,11 @@ function SchemaField(props) {
     description: <DescriptionField id={id + "__description"}
                                    description={description}
                                    formContext={formContext} />,
+    rawDescription: description,
     help: <Help help={help} />,
+    rawHelp: typeof help === "string" ? help : undefined,
     errors: <ErrorList errors={errors} />,
+    rawErrors: errors,
     id,
     label,
     hidden,
@@ -195,6 +202,7 @@ function SchemaField(props) {
     displayLabel,
     classNames,
     formContext,
+    fields,
   };
 
   return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index ca930f281..018581df6 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -31,7 +31,7 @@ function StringField(props) {
   const commonProps = {
     schema,
     id: idSchema && idSchema.$id,
-    label: title || name,
+    label: (title === undefined) ? name : title,
     value: defaultFieldValue(formData, schema),
     onChange,
     required,
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/FileWidget.js b/src/components/widgets/FileWidget.js
index 4ea58b2ca..62422543b 100644
--- a/src/components/widgets/FileWidget.js
+++ b/src/components/widgets/FileWidget.js
@@ -100,6 +100,7 @@ class FileWidget extends Component {
       <div>
         <p>
           <input
+            ref={ref => this.inputRef = ref}
             id={id}
             type="file"
             disabled={readonly || disabled}
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/components/widgets/RangeWidget.js b/src/components/widgets/RangeWidget.js
index c8bc400dc..99bb9ec45 100644
--- a/src/components/widgets/RangeWidget.js
+++ b/src/components/widgets/RangeWidget.js
@@ -1,22 +1,8 @@
 import React, { PropTypes } from "react";
 
+import { rangeSpec } from "../../utils";
 import BaseInput from "./BaseInput";
 
-
-function rangeSpec(schema) {
-  const spec = {};
-  if (schema.multipleOf) {
-    spec.step = schema.multipleOf;
-  }
-  if (schema.minimum) {
-    spec.min = schema.minimum;
-  }
-  if (schema.maximum) {
-    spec.max = schema.maximum;
-  }
-  return spec;
-}
-
 function RangeWidget(props) {
   const {schema, value} = props;
   return (
diff --git a/src/components/widgets/UpDownWidget.js b/src/components/widgets/UpDownWidget.js
index 2ed97990f..d57b0bed9 100644
--- a/src/components/widgets/UpDownWidget.js
+++ b/src/components/widgets/UpDownWidget.js
@@ -1,22 +1,8 @@
 import React, { PropTypes } from "react";
 
+import { rangeSpec } from "../../utils";
 import BaseInput from "./BaseInput";
 
-
-function rangeSpec(schema) {
-  const spec = {};
-  if (schema.multipleOf) {
-    spec.step = schema.multipleOf;
-  }
-  if (schema.minimum) {
-    spec.min = schema.minimum;
-  }
-  if (schema.maximum) {
-    spec.max = schema.maximum;
-  }
-  return spec;
-}
-
 function UpDownWidget(props) {
   return <BaseInput type="number" {...props} {...rangeSpec(props.schema)} />;
 }
diff --git a/src/utils.js b/src/utils.js
index 36372dc64..30c4b3e90 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -44,14 +44,17 @@ const altWidgetMap = {
   number: {
     updown: UpDownWidget,
     range: RangeWidget,
+    radio: RadioWidget,
     hidden: HiddenWidget,
   },
   integer: {
     updown: UpDownWidget,
     range: RangeWidget,
+    radio: RadioWidget,
     hidden: HiddenWidget,
   },
   array: {
+    select: SelectWidget,
     checkboxes: CheckboxesWidget,
   }
 };
@@ -99,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;
   }
 
@@ -224,6 +234,14 @@ export function asNumber(value) {
   }
   const n = Number(value);
   const valid = typeof n === "number" && !Number.isNaN(n);
+
+  if (/\.\d*0$/.test(value)) {
+    // It's a number, that's cool - but we need it as a string so it doesn't screw
+    // with the user when entering dollar amounts or other values (such as those with
+    // specific precision or number of significant digits)
+    return value;
+  }
+
   return valid ? n : value;
 }
 
@@ -490,3 +508,17 @@ export function dataURItoBlob(dataURI) {
 
   return {blob, name};
 }
+
+export function rangeSpec(schema) {
+  const spec = {};
+  if (schema.multipleOf) {
+    spec.step = schema.multipleOf;
+  }
+  if (schema.minimum || schema.minimum === 0) {
+    spec.min = schema.minimum;
+  }
+  if (schema.maximum || schema.maximum === 0) {
+    spec.max = schema.maximum;
+  }
+  return spec;
+}


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index 8b2f258ad..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);
+      });
     });
   });
 
@@ -625,4 +651,49 @@ describe("ArrayField", () => {
       expect(comp.state.formData).eql([1, 2]);
     });
   });
+
+  describe("Title", () => {
+    const TitleField = props => <div id={`title-${props.title}`} />;
+
+    const fields = {TitleField};
+
+    it("should pass field name to TitleField if there is no title", () => {
+      const schema = {
+        "type": "object",
+        "properties": {
+          "array": {
+            "type": "array",
+            "items": {
+            }
+          }
+        }
+      };
+
+      const {node} = createFormComponent({schema, fields});
+      expect(node.querySelector("#title-array")).to.not.be.null;
+    });
+
+    it("should pass schema title to TitleField", () => {
+      const schema = {
+        "type": "array",
+        "title": "test",
+        "items": {
+        }
+      };
+
+      const {node} = createFormComponent({schema, fields});
+      expect(node.querySelector("#title-test")).to.not.be.null;
+    });
+
+    it("should pass empty schema title to TitleField", () => {
+      const schema = {
+        "type": "array",
+        "title": "",
+        "items": {
+        }
+      };
+      const {node} = createFormComponent({schema, fields});
+      expect(node.querySelector("#title-")).to.be.null;
+    });
+  });
 });
diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js
index 677521f1b..91bc27b48 100644
--- a/test/BooleanField_test.js
+++ b/test/BooleanField_test.js
@@ -1,3 +1,4 @@
+import React from "react";
 import { expect } from "chai";
 import { Simulate } from "react-addons-test-utils";
 
@@ -93,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",
@@ -112,4 +131,54 @@ describe("BooleanField", () => {
     expect(node.querySelector("input[type=checkbox]").id)
       .eql("root");
   });
+
+  describe("Label", () => {
+    const Widget = props => <div id={`label-${props.label}`}/>;
+
+    const widgets = {Widget};
+
+    it("should pass field name to widget if there is no title", () => {
+      const schema = {
+        "type": "object",
+        "properties": {
+          "boolean": {
+            "type": "boolean"
+          }
+        }
+      };
+      const uiSchema = {
+        "boolean": {
+          "ui:widget": "Widget"
+        }
+      };
+
+      const {node} = createFormComponent({schema, widgets, uiSchema});
+      expect(node.querySelector("#label-boolean")).to.not.be.null;
+    });
+
+    it("should pass schema title to widget", () => {
+      const schema = {
+        "type": "boolean",
+        "title": "test"
+      };
+      const uiSchema = {
+        "ui:widget": "Widget"
+      };
+
+      const {node} = createFormComponent({schema, widgets, uiSchema});
+      expect(node.querySelector("#label-test")).to.not.be.null;
+    });
+
+    it("should pass empty schema title to widget", () => {
+      const schema = {
+        "type": "boolean",
+        "title": ""
+      };
+      const uiSchema = {
+        "ui:widget": "Widget"
+      };
+      const {node} = createFormComponent({schema, widgets, uiSchema});
+      expect(node.querySelector("#label-")).to.not.be.null;
+    });
+  });
 });
diff --git a/test/FormContext_test.js b/test/FormContext_test.js
index 705a6095d..b94a3178b 100644
--- a/test/FormContext_test.js
+++ b/test/FormContext_test.js
@@ -40,8 +40,8 @@ describe("FormContext", () => {
       formContext
     });
 
-    expect(node.querySelectorAll("#" + formContext.foo))
-      .to.have.length.of(1);
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
   });
 
   it("should be passed to custom widget", () => {
@@ -54,8 +54,31 @@ describe("FormContext", () => {
       formContext
     });
 
-    expect(node.querySelectorAll("#" + formContext.foo))
-      .to.have.length.of(1);
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
+  });
+
+  it("should be passed to TemplateField", () => {
+    function CustomTemplateField({formContext}) {
+      return <div id={formContext.foo} />;
+    }
+
+    const {node} = createFormComponent({
+      schema: {
+        type: "object",
+        title: "A title",
+        properties: {
+          prop: {
+            type:  "string"
+          }
+        }
+      },
+      FieldTemplate: CustomTemplateField,
+      formContext
+    });
+
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
   });
 
   it("should be passed to custom TitleField", () => {
@@ -75,23 +98,20 @@ describe("FormContext", () => {
       formContext
     });
 
-    expect(node.querySelectorAll("#" + formContext.foo))
-      .to.have.length.of(1);
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
   });
 
   it("should be passed to custom DescriptionField", () => {
     const fields = {DescriptionField: CustomComponent};
 
     const {node} = createFormComponent({
-      schema: {type: "string"},
-      uiSchema: {
-        "ui:description": "A description"
-      },
+      schema: {type: "string", description: "A description"},
       fields,
       formContext
     });
 
-    expect(node.querySelectorAll("#" + formContext.foo))
-      .to.have.length.of(1);
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
   });
 });
diff --git a/test/Form_test.js b/test/Form_test.js
index 98002b058..c9acb744e 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -7,7 +7,6 @@ import { findDOMNode } from "react-dom";
 import Form from "../src";
 import { createFormComponent, createSandbox } from "./test_utils";
 
-
 describe("Form", () => {
   let sandbox;
 
@@ -70,7 +69,19 @@ describe("Form", () => {
     const formData = {foo: "invalid"};
 
     function FieldTemplate(props) {
-      const {id, classNames, label, help, required, description, errors, children} = props;
+      const {
+        id,
+        classNames,
+        label,
+        help,
+        rawHelp,
+        required,
+        description,
+        rawDescription,
+        errors,
+        rawErrors,
+        children,
+      } = props;
       return (
         <div className={"my-template " + classNames}>
           <label htmlFor={id}>{label}{required ? "*" : null}</label>
@@ -78,6 +89,16 @@ describe("Form", () => {
           {children}
           {errors}
           {help}
+          <span className="raw-help">{`${rawHelp} rendered from the raw format`}</span>
+          <span className="raw-description">{`${rawDescription} rendered from the raw format`}</span>
+          {rawErrors ? (
+            <ul>
+              {rawErrors.map(
+                (error, i) => <li key={i} className="raw-error">{error}</li>
+              )}
+            </ul>
+          ) : null
+          }
         </div>
       );
     }
@@ -105,20 +126,35 @@ describe("Form", () => {
         .eql("foo*");
     });
 
-    it("should use the provided template for descriptions", () => {
+    it("should pass description as the provided React element", () => {
       expect(node.querySelector("#root_foo__description").textContent)
         .eql("this is description");
     });
 
-    it("should use the provided template for errors", () => {
+    it("should pass rawDescription as a string", () => {
+      expect(node.querySelector(".raw-description").textContent)
+        .eql("this is description rendered from the raw format");
+    });
+
+    it("should pass errors as the provided React component", () => {
       expect(node.querySelectorAll(".error-detail li"))
         .to.have.length.of(1);
     });
 
-    it("should use the provided template for help", () => {
+    it("should pass rawErrors as an array of strings", () => {
+      expect(node.querySelectorAll(".raw-error"))
+        .to.have.length.of(1);
+    });
+
+    it("should pass help as a the provided React element", () => {
       expect(node.querySelector(".help-block").textContent)
         .eql("this is help");
     });
+
+    it("should pass rawHelp as a string", () => {
+      expect(node.querySelector(".raw-help").textContent)
+        .eql("this is help rendered from the raw format");
+    });
   });
 
   describe("Custom submit buttons", () => {
diff --git a/test/NumberField_test.js b/test/NumberField_test.js
index 97b191e7e..d3dce39b8 100644
--- a/test/NumberField_test.js
+++ b/test/NumberField_test.js
@@ -102,6 +102,36 @@ describe("NumberField", () => {
       expect(node.querySelector("input[type=text]").id)
         .eql("root");
     });
+
+    it("should render with trailing zeroes", () => {
+      const {node} = createFormComponent({schema: {
+        type: "number"
+      }});
+
+      Simulate.change(node.querySelector("input"), {
+        target: {value: "2."}
+      });
+      expect(node.querySelector(".field input").value)
+        .eql("2.");
+
+      Simulate.change(node.querySelector("input"), {
+        target: {value: "2.0"}
+      });
+      expect(node.querySelector(".field input").value)
+        .eql("2.0");
+
+      Simulate.change(node.querySelector("input"), {
+        target: {value: "2.00"}
+      });
+      expect(node.querySelector(".field input").value)
+        .eql("2.00");
+
+      Simulate.change(node.querySelector("input"), {
+        target: {value: "2.000"}
+      });
+      expect(node.querySelector(".field input").value)
+        .eql("2.000");
+    });
   });
 
   describe("SelectWidget", () => {
diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js
index e724fda2b..ca746487a 100644
--- a/test/ObjectField_test.js
+++ b/test/ObjectField_test.js
@@ -237,4 +237,49 @@ describe("ObjectField", () => {
       expect(ids).eql(["root_bar", "root_foo"]);
     });
   });
+
+  describe("Title", () => {
+    const TitleField = props => <div id={`title-${props.title}`} />;
+
+    const fields = {TitleField};
+
+    it("should pass field name to TitleField if there is no title", () => {
+      const schema = {
+        "type": "object",
+        "properties": {
+          "object": {
+            "type": "object",
+            "properties": {
+            }
+          }
+        }
+      };
+
+      const {node} = createFormComponent({schema, fields});
+      expect(node.querySelector("#title-object")).to.not.be.null;
+    });
+
+    it("should pass schema title to TitleField", () => {
+      const schema = {
+        "type": "object",
+        "properties": {
+        },
+        "title": "test"
+      };
+
+      const {node} = createFormComponent({schema, fields});
+      expect(node.querySelector("#title-test")).to.not.be.null;
+    });
+
+    it("should pass empty schema title to TitleField", () => {
+      const schema = {
+        "type": "object",
+        "properties": {
+        },
+        "title": ""
+      };
+      const {node} = createFormComponent({schema, fields});
+      expect(node.querySelector("#title-")).to.be.null;
+    });
+  });
 });
diff --git a/test/StringField_test.js b/test/StringField_test.js
index bc058f43e..e0e32e9e5 100644
--- a/test/StringField_test.js
+++ b/test/StringField_test.js
@@ -1,3 +1,4 @@
+import React from "react";
 import { expect } from "chai";
 import { Simulate } from "react-addons-test-utils";
 
@@ -973,4 +974,55 @@ describe("StringField", () => {
         .eql("root");
     });
   });
+
+  describe("Label", () => {
+    const Widget = props => <div id={`label-${props.label}`} />;
+
+    const widgets = {Widget};
+
+
+    it("should pass field name to widget if there is no title", () => {
+      const schema = {
+        "type": "object",
+        "properties": {
+          "string": {
+            "type": "string"
+          }
+        }
+      };
+      const uiSchema = {
+        "string": {
+          "ui:widget": "Widget"
+        }
+      };
+
+      const {node} = createFormComponent({schema, widgets, uiSchema});
+      expect(node.querySelector("#label-string")).to.not.be.null;
+    });
+
+    it("should pass schema title to widget", () => {
+      const schema = {
+        "type": "string",
+        "title": "test"
+      };
+      const uiSchema = {
+        "ui:widget": "Widget"
+      };
+
+      const {node} = createFormComponent({schema, widgets, uiSchema});
+      expect(node.querySelector("#label-test")).to.not.be.null;
+    });
+
+    it("should pass empty schema title to widget", () => {
+      const schema = {
+        "type": "string",
+        "title": ""
+      };
+      const uiSchema = {
+        "ui:widget": "Widget"
+      };
+      const {node} = createFormComponent({schema, widgets, uiSchema});
+      expect(node.querySelector("#label-")).to.not.be.null;
+    });
+  });
 });
diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js
index e0370503f..d3c8e0be6 100644
--- a/test/uiSchema_test.js
+++ b/test/uiSchema_test.js
@@ -236,15 +236,15 @@ describe("uiSchema", () => {
 
       expect(node.querySelector("p.help-block").textContent).eql("plop");
     });
+  });
 
-    it("should accept a react element as help", () => {
-      const schema = {type: "string"};
-      const uiSchema = {"ui:help": (<b>plop</b>)};
+  it("should accept a react element as help", () => {
+    const schema = {type: "string"};
+    const uiSchema = {"ui:help": (<b>plop</b>)};
 
-      const {node} = createFormComponent({schema, uiSchema});
+    const {node} = createFormComponent({schema, uiSchema});
 
-      expect(node.querySelector("div.help-block").textContent).eql("plop");
-    });
+    expect(node.querySelector("div.help-block").textContent).eql("plop");
   });
 
   describe("ui:focus", () => {
@@ -654,6 +654,20 @@ describe("uiSchema", () => {
           expect(input.getAttribute("max")).eql("100");
         });
 
+        it("should support '0' as minimum and maximum constraints", () => {
+          const schema = {
+            type: "number",
+            minimum: 0,
+            maximum: 0,
+          };
+          const uiSchema = {"ui:widget": "updown"};
+          const {node} = createFormComponent({schema, uiSchema});
+          input = node.querySelector("[type=number]");
+
+          expect(input.getAttribute("min")).eql("0");
+          expect(input.getAttribute("max")).eql("0");
+        });
+
         it("should support the multipleOf constraint", () => {
           expect(input.getAttribute("step")).eql("1");
         });
@@ -711,12 +725,72 @@ describe("uiSchema", () => {
           expect(input.getAttribute("max")).eql("100");
         });
 
+        it("should support '0' as minimum and maximum constraints", () => {
+          const schema = {
+            type: "number",
+            minimum: 0,
+            maximum: 0,
+          };
+          const uiSchema = {"ui:widget": "range"};
+          const {node} = createFormComponent({schema, uiSchema});
+          input = node.querySelector("[type=range]");
+
+          expect(input.getAttribute("min")).eql("0");
+          expect(input.getAttribute("max")).eql("0");
+        });
+
         it("should support the multipleOf constraint", () => {
           expect(input.getAttribute("step")).eql("1");
         });
       });
     });
 
+    describe("radio", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {
+            type: "number",
+            enum: [3.14159, 2.718, 1.4142],
+          }
+        }
+      };
+
+      const uiSchema = {
+        foo: {
+          "ui:widget": "radio"
+        }
+      };
+
+      it("should accept a uiSchema object", () => {
+        const {node} = createFormComponent({schema, uiSchema});
+
+        expect(node.querySelectorAll("[type=radio]"))
+          .to.have.length.of(3);
+      });
+
+      it("should support formData", () => {
+        const {node} = createFormComponent({schema, uiSchema, formData: {
+          foo: 2.718
+        }});
+
+        expect(node.querySelectorAll("[type=radio]")[1].checked)
+          .eql(true);
+      });
+
+      it("should update state when value is updated", () => {
+        const {comp, node} = createFormComponent({schema, uiSchema, formData: {
+          foo: 1.4142
+        }});
+
+        Simulate.change(node.querySelectorAll("[type=radio]")[2], {
+          target: {checked: true}
+        });
+
+        expect(comp.state.formData).eql({foo: 1.4142});
+      });
+    });
+
     describe("hidden", () => {
       const uiSchema = {
         foo: {
@@ -832,6 +906,52 @@ describe("uiSchema", () => {
       });
     });
 
+    describe("radio", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {
+            type: "integer",
+            enum: [1, 2],
+          }
+        }
+      };
+
+      const uiSchema = {
+        foo: {
+          "ui:widget": "radio"
+        }
+      };
+
+      it("should accept a uiSchema object", () => {
+        const {node} = createFormComponent({schema, uiSchema});
+
+        expect(node.querySelectorAll("[type=radio]"))
+          .to.have.length.of(2);
+      });
+
+      it("should support formData", () => {
+        const {node} = createFormComponent({schema, uiSchema, formData: {
+          foo: 2
+        }});
+
+        expect(node.querySelectorAll("[type=radio]")[1].checked)
+          .eql(true);
+      });
+
+      it("should update state when value is updated", () => {
+        const {comp, node} = createFormComponent({schema, uiSchema, formData: {
+          foo: 1
+        }});
+
+        Simulate.change(node.querySelectorAll("[type=radio]")[1], {
+          target: {checked: true}
+        });
+
+        expect(comp.state.formData).eql({foo: 2});
+      });
+    });
+
     describe("hidden", () => {
       const uiSchema = {
         foo: {