Repository: mozilla-services/react-jsonschema-form
Pull request #439 (2017-01-12 15:43:23)

Don't pass consumed SchemaField class names to child component
### Reasons for making this change

Currently SchemaField passes it's `classNames` prop as-is to the child component. It shouldn't be passed, because SchemaField consumes the given class name. I tried to put some borders to a deeply nested field and I ended up with nested borders, because the nested fields use SchemaField and each of them rerendered the class name.

### 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
* [ ] **I'm adding a new feature**
  - [ ] I've updated the playground with an example use of the feature


n1k0: I don't properly understand the patch and its related use case, sorry. Would you mind adding a test case so we make clear what it's supposed to cover? Also, a comment above the modified line with a reference to this PR might be helpful to readers.

Patch:
diff --git a/README.md b/README.md
index c504811bd..00ee219f4 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
         - [Form submission](#form-submission)
         - [Form error event handler](#form-error-event-handler)
         - [Form data changes](#form-data-changes)
+        - [Form field blur events](#form-field-blur-events)
   - [Form customization](#form-customization)
      - [The uiSchema object](#the-uischema-object)
      - [Alternative widgets](#alternative-widgets)
@@ -49,6 +50,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
      - [Form attributes](#form-attributes)
   - [Advanced customization](#advanced-customization)
      - [Field template](#field-template)
+     - [Array Field Template](#array-field-template)
      - [Custom widgets and fields](#custom-widgets-and-fields)
      - [Custom widget components](#custom-widget-components)
         - [Custom component registration](#custom-component-registration)
@@ -64,8 +66,11 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
      - [Custom descriptions](#custom-descriptions)
   - [Form data validation](#form-data-validation)
      - [Live validation](#live-validation)
+     - [HTML5 Validation](#html5-validation)
      - [Custom validation](#custom-validation)
+     - [Custom error messages](#custom-error-messages)
      - [Error List Display](#error-list-display)
+     - [The case of empty strings](#the-case-of-empty-strings)
   - [Styling your forms](#styling-your-forms)
   - [Schema definitions and references](#schema-definitions-and-references)
   - [JSON Schema supporting status](#json-schema-supporting-status)
@@ -81,6 +86,10 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
 
 Requires React 15.0.0+.
 
+> Note: The `master` branch of the repository reflects ongoing development. Releases are published as [tags](https://github.com/mozilla-services/react-jsonschema-form/releases).
+>
+> You should never blindly install from `master`, but rather check what the available stable releases are.
+
 
 ### As a npm-based project dependency
 
@@ -192,6 +201,10 @@ render((
 
 If you plan on being notified everytime the form data are updated, you can pass an `onChange` handler, which will receive the same args as `onSubmit` any time a value is updated in the form.
 
+#### Form field blur events
+
+Sometimes you may want to trigger events or modify external state when a field has been touched, so you can pass an `onBlur` handler, which will receive the id of the input that was blurred and the field value.
+
 ## Form customization
 
 ### The `uiSchema` object
@@ -546,6 +559,23 @@ const uiSchema = {
 };
 ```
 
+Care should be taken when using the `required` property with arrays.  An empty array is sufficient to pass that validation check.  If you wish to ensure the user populates the array, you can specify the minimum number of items the user must select with the `minItems` property.
+
+Example:
+
+```js
+const schema = {
+  type: "array",
+  minItems: 2,
+  title: "A multiple choices list",
+  items: {
+    type: "string",
+    enum: ["foo", "bar", "fuzz", "qux"],
+  },
+  uniqueItems: true
+};
+```
+
 By default, checkboxes are stacked but if you prefer them inline:
 
 ```js
@@ -629,6 +659,15 @@ const uiSchema = {
 
 ![](http://i.imgur.com/MbHypKg.png)
 
+Fields using `enum` can also use `ui:placeholder`. The value will be used as the text for the empty option in the select widget.
+
+```jsx
+const schema = {type: "string", enum: ["First", "Second"]};
+const uiSchema = {
+  "ui:placeholder": "Choose an option"
+};
+```
+
 ### Form attributes
 
 Form component supports the following html attributes:
@@ -700,6 +739,59 @@ The following props are passed to a custom field template component:
 
 > 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.
 
+### Array Field Template
+
+Similarly to the `FieldTemplate` you can use an `ArrayFieldTemplate` to customize how your
+arrays are rendered. This allows you to customize your array, and each element in the array.
+
+```jsx
+function ArrayFieldTemplate(props) {
+  return (
+    <div>
+      {props.items.map(element => element.children)}
+      {props.canAdd && <button onClick={props.onAddClick}></button>}
+    </div>
+  );
+}
+
+render((
+  <Form schema={schema}
+        ArrayFieldTemplate={ArrayFieldTemplate} />,
+), document.getElementById("app"));
+```
+
+Please see [customArray.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customArray.js) for a better example.
+
+The following props are passed to each `ArrayFieldTemplate`:
+
+- `DescriptionField`: The generated `DescriptionField` (if you wanted to utilize it)
+- `TitleField`: The generated `TitleField` (if you wanted to utilize it).
+- `canAdd`: A boolean value stating whether new elements can be added to the array.
+- `className`: The className string.
+- `disabled`: A boolean value stating if the array is disabled.
+- `idSchema`: Object
+- `items`: An array of objects representing the items in the array. Each of the items represent a child with properties described below.
+- `onAddClick: (event) => (event) => void`: Returns a function that adds a new item to the array.
+- `readonly`: A boolean value stating if the array is readonly.
+- `required`: A boolean value stating if the array is required.
+- `schema`: The schema object for this array.
+- `title`: A string value containing the title for the array.
+- `formContext`: The `formContext` object that you passed to Form.
+
+The following props are part of each element in `items`:
+
+- `children`: The html for the item's content.
+- `className`: The className string.
+- `disabled`: A boolean value stating if the array item is disabled.
+- `hasMoveDown`: A boolean value stating whether the array item can be moved down.
+- `hasMoveUp`: A boolean value stating whether the array item can be moved up.
+- `hasRemove`: A boolean value stating whether the array item can be removed.
+- `hasToolbar`: A boolean value stating whether the array item has a toolbar.
+- `index`: A number stating the index the array item occurs in `items`.
+- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`.
+- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`.
+- `readonly`: A boolean value stating if the array item is readonly.
+
 ### Custom widgets and fields
 
 The API allows to specify your own custom *widget* and *field* components:
@@ -741,12 +833,14 @@ render((
 
 The following props are passed to custom widget components:
 
+- `id`: The generated id for this field;
 - `schema`: The JSONSchema subschema object for this field;
 - `value`: The current value for this field;
 - `required`: The required status of this field;
 - `disabled`: `true` if the widget is disabled;
 - `readonly`: `true` if the widget is read-only;
 - `onChange`: The value change event handler; call it with the new value everytime it changes;
+- `onBlur`: The input blur event handler; call it with the the widget id and value;
 - `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)).
 - `formContext`: The `formContext` object that you passed to Form.
 
@@ -1067,6 +1161,10 @@ Be warned that this is an expensive strategy, with possibly strong impact on per
 
 To disable validation entirely, you can set Form's `noValidate` prop to `true`.
 
+### HTML5 Validation
+
+By default, required field errors will cause the browser to display its standard HTML5 `required` attribute error messages and prevent form submission. If you would like to turn this off, you can set Form's `noHtml5Validate` prop to `true`, which will set `noValidate` on the `form` element.
+
 ### Custom validation
 
 Form data is always validated against the JSON schema.
@@ -1100,6 +1198,36 @@ render((
 >   received as second argument.
 > - The `validate()` function is called **after** the JSON schema validation.
 
+### Custom error messages
+
+Validation error messages are provided by the JSON Schema validation by default. If you need to change these messages or make any other modifications to the errors from the JSON Schema validation, you can define a transform function that receives the list of JSON Schema errors and returns a new list.
+
+```js
+function transformErrors(errors) {
+  return errors.map(error => {
+    if (error.name === "pattern") {
+      error.message = "Only digits are allowed"
+    }
+    return error;
+  });
+}
+
+const schema = {
+  type: "object",
+  properties: {
+    onlyNumbersString: {type: "string", pattern: "\d*"},
+  }
+};
+
+render((
+  <Form schema={schema}
+        transformErrors={transformErrors} />
+), document.getElementById("app"));
+```
+
+> Notes:
+> - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error.
+
 ### Error List Display
 
 To disable rendering of the error list at the top of the form, you can set the `showErrorList` prop to `false`. Doing so will still validate the form, but only the inline display will show.
@@ -1111,6 +1239,12 @@ render((
 ), document.getElementById("app"));
 ```
 
+### The case of empty strings
+
+When a text input is empty, the field in form data is set to `undefined`. String fields that use `enum` and a `select` widget work similarly and will have an empty option at the top of the options list that when selected will result in the field being `undefined`.
+
+One consequence of this is that if you have an empty string in your `enum` array, selecting that option in the `select` input will cause the field to be set to `undefined`, not an empty string.
+
 ## Styling your forms
 
 This library renders form fields and widgets leveraging the [Bootstrap](http://getbootstrap.com/) semantics. That means your forms will be beautiful by default if you're loading its stylesheet in your page.
diff --git a/package.json b/package.json
index 1a5ceab98..de778a270 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-jsonschema-form",
-  "version": "0.41.2",
+  "version": "0.42.0-dev",
   "description": "A simple React component capable of building HTML forms out of a JSON schema.",
   "scripts": {
     "build:readme": "toctoc README.md -w",
diff --git a/playground/app.js b/playground/app.js
index c79255432..e3b25acab 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -283,9 +283,11 @@ class App extends Component {
   }
 
   load = (data) => {
+    // Reset the ArrayFieldTemplate whenever you load new data
+    const { ArrayFieldTemplate } = data;
     // force resetting form component instance
     this.setState({form: false},
-      _ => this.setState({...data, form: true}));
+      _ => this.setState({...data, form: true, ArrayFieldTemplate}));
   };
 
   onSchemaEdited   = (schema) => this.setState({schema});
@@ -314,7 +316,9 @@ class App extends Component {
       liveValidate,
       validate,
       theme,
-      editor
+      editor,
+      ArrayFieldTemplate,
+      transformErrors
     } = this.state;
 
     return (
@@ -352,6 +356,7 @@ class App extends Component {
         <div className="col-sm-5">
           {!this.state.form ? null :
             <Form
+              ArrayFieldTemplate={ArrayFieldTemplate}
               liveValidate={liveValidate}
               schema={schema}
               uiSchema={uiSchema}
@@ -359,6 +364,8 @@ class App extends Component {
               onChange={this.onFormDataChange}
               fields={{geo: GeoPosition}}
               validate={validate}
+              onBlur={(id, value) => console.log(`Touched ${id} with value ${value}`)}
+              transformErrors={transformErrors}
               onError={log("errors")} />}
         </div>
       </div>
diff --git a/playground/samples/customArray.js b/playground/samples/customArray.js
new file mode 100644
index 000000000..dfb7645ee
--- /dev/null
+++ b/playground/samples/customArray.js
@@ -0,0 +1,40 @@
+import React, { Component } from "react";
+
+function ArrayFieldTemplate(props) {
+  return (
+    <div className={props.className}>
+
+      {props.items && props.items.map(element => (
+        <div key={element.index}>
+          <div>{element.children}</div>
+          {element.hasMoveDown &&
+            <button onClick={element.onReorderClick(element.index, element.index + 1)}>Down</button>}
+          {element.hasMoveUp &&
+            <button onClick={element.onReorderClick(element.index, element.index - 1)}>Up</button>}
+          <button onClick={element.onDropIndexClick(element.index)}>Delete</button>
+          <hr />
+        </div>
+      ))}
+
+      {props.canAdd &&
+        <div className="row">
+          <p className="col-xs-3 col-xs-offset-9 array-item-add text-right">
+            <button onClick={props.onAddClick} type="button">Custom +</button>
+          </p>
+        </div>}
+
+    </div>
+  );
+}
+
+module.exports = {
+  schema: {
+    title: "Custom array of strings",
+    type: "array",
+    items: {
+      type: "string"
+    }
+  },
+  formData: ["react", "jsonschema", "form"],
+  ArrayFieldTemplate
+};
diff --git a/playground/samples/index.js b/playground/samples/index.js
index 5f6d280dc..1527696cc 100644
--- a/playground/samples/index.js
+++ b/playground/samples/index.js
@@ -12,6 +12,7 @@ import date from "./date";
 import validation from "./validation";
 import files from "./files";
 import single from "./single";
+import customArray from "./customArray";
 
 export const samples = {
   Simple: simple,
@@ -27,5 +28,6 @@ export const samples = {
   "Date & time": date,
   Validation: validation,
   Files: files,
-  Single: single
+  Single: single,
+  "Custom Array": customArray,
 };
diff --git a/playground/samples/large.js b/playground/samples/large.js
index 8e5150d6c..de6240e95 100644
--- a/playground/samples/large.js
+++ b/playground/samples/large.js
@@ -30,6 +30,10 @@ module.exports = {
       choice10: {$ref: "#/definitions/largeEnum"},
     }
   },
-  uiSchema: {},
+  uiSchema: {
+    choice1: {
+      "ui:placeholder": "Choose one"
+    }
+  },
   formData: {}
 };
diff --git a/playground/samples/references.js b/playground/samples/references.js
index 5131c4b98..2cf8dcd11 100644
--- a/playground/samples/references.js
+++ b/playground/samples/references.js
@@ -9,6 +9,18 @@ module.exports = {
           state:          { type: "string" }
         },
         required: ["street_address", "city", "state"]
+      },
+      node: {
+        type: "object",
+        properties: {
+          name: {type: "string"},
+          children: {
+            type: "array",
+            items: {
+              $ref: "#/definitions/node"
+            }
+          }
+        }
       }
     },
     type: "object",
@@ -20,11 +32,15 @@ module.exports = {
       shipping_address: {
         title: "Shipping address",
         $ref: "#/definitions/address"
+      },
+      tree: {
+        title: "Recursive references",
+        $ref: "#/definitions/node"
       }
     }
   },
   uiSchema: {
-    "ui:order": ["shipping_address", "billing_address"]
+    "ui:order": ["shipping_address", "billing_address", "tree"]
   },
   formData: {
     billing_address: {
@@ -36,6 +52,12 @@ module.exports = {
       street_address: "221B, Baker Street",
       city: "London",
       state: "N/A"
+    },
+    tree: {
+      name: "root",
+      children: [
+         {name: "leaf"}
+      ]
     }
   }
 };
diff --git a/playground/samples/validation.js b/playground/samples/validation.js
index 90739ab60..22452dca9 100644
--- a/playground/samples/validation.js
+++ b/playground/samples/validation.js
@@ -5,6 +5,17 @@ function validate({pass1, pass2}, errors) {
   return errors;
 }
 
+function transformErrors(errors) {
+  return errors.map(error => {
+    if (error.name === "minimum" && error.property === "instance.age") {
+      return Object.assign({}, error, {
+        message: "You need to be 18 because of some legal thing"
+      });
+    }
+    return error;
+  });
+}
+
 module.exports = {
   schema:  {
     title: "Custom validation",
@@ -21,6 +32,11 @@ module.exports = {
         type: "string",
         minLength: 3
       },
+      age: {
+        title: "Age",
+        type: "number",
+        minimum: 18
+      }
     }
   },
   uiSchema: {
@@ -28,5 +44,6 @@ module.exports = {
     pass2: {"ui:widget": "password"},
   },
   formData: {},
-  validate
+  validate,
+  transformErrors
 };
diff --git a/src/components/Form.js b/src/components/Form.js
index 32819cc75..7f8f2f31e 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -17,6 +17,7 @@ export default class Form extends Component {
     noValidate: false,
     liveValidate: false,
     safeRenderCompletion: false,
+    noHtml5Validate: false
   }
 
   constructor(props) {
@@ -60,8 +61,8 @@ export default class Form extends Component {
   }
 
   validate(formData, schema) {
-    const {validate} = this.props;
-    return validateFormData(formData, schema || this.props.schema, validate);
+    const {validate, transformErrors} = this.props;
+    return validateFormData(formData, schema || this.props.schema, validate, transformErrors);
   }
 
   renderErrors() {
@@ -88,6 +89,12 @@ export default class Form extends Component {
     });
   };
 
+  onBlur = (...args) => {
+    if (this.props.onBlur) {
+      this.props.onBlur(...args);
+    }
+  }
+
   onSubmit = (event) => {
     event.preventDefault();
     this.setState({status: "submitted"});
@@ -119,6 +126,7 @@ export default class Form extends Component {
     return {
       fields: {...fields, ...this.props.fields},
       widgets: {...widgets, ...this.props.widgets},
+      ArrayFieldTemplate: this.props.ArrayFieldTemplate,
       FieldTemplate: this.props.FieldTemplate,
       definitions: this.props.schema.definitions || {},
       formContext: this.props.formContext || {},
@@ -137,7 +145,8 @@ export default class Form extends Component {
       action,
       autocomplete,
       enctype,
-      acceptcharset
+      acceptcharset,
+      noHtml5Validate
     } = this.props;
 
     const {schema, uiSchema, formData, errorSchema, idSchema} = this.state;
@@ -154,6 +163,7 @@ export default class Form extends Component {
         autoComplete={autocomplete}
         encType={enctype}
         acceptCharset={acceptcharset}
+        noValidate={noHtml5Validate}
         onSubmit={this.onSubmit}>
         {this.renderErrors()}
         <_SchemaField
@@ -163,6 +173,7 @@ export default class Form extends Component {
           idSchema={idSchema}
           formData={formData}
           onChange={this.onChange}
+          onBlur={this.onBlur}
           registry={registry}
           safeRenderCompletion={safeRenderCompletion}/>
         { children ? children :
@@ -184,6 +195,7 @@ if (process.env.NODE_ENV !== "production") {
       PropTypes.object,
     ])),
     fields: PropTypes.objectOf(PropTypes.func),
+    ArrayFieldTemplate: PropTypes.func,
     FieldTemplate: PropTypes.func,
     onChange: PropTypes.func,
     onError: PropTypes.func,
@@ -199,7 +211,10 @@ if (process.env.NODE_ENV !== "production") {
     enctype: PropTypes.string,
     acceptcharset: PropTypes.string,
     noValidate: PropTypes.bool,
+    noHtml5Validate: PropTypes.bool,
     liveValidate: PropTypes.bool,
+    validate: PropTypes.func,
+    transformErrors: PropTypes.func,
     safeRenderCompletion: PropTypes.bool,
     formContext: PropTypes.object,
   };
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 6239274be..b9d4e54b1 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -43,6 +43,111 @@ function IconBtn(props) {
   );
 }
 
+// Used in the two templates
+function DefaultArrayItem(props) {
+  const btnStyle = {flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold"};
+  return (
+    <div key={props.index} className={props.className}>
+
+      <div className={props.hasToolbar ? "col-xs-9" : "col-xs-12"}>
+        {props.children}
+      </div>
+
+      {props.hasToolbar ?
+        <div className="col-xs-3 array-item-toolbox">
+          <div className="btn-group" style={{display: "flex", justifyContent: "space-around"}}>
+
+            {props.hasMoveUp || props.hasMoveDown ?
+              <IconBtn icon="arrow-up" className="array-item-move-up"
+                tabIndex="-1"
+                style={btnStyle}
+                disabled={props.disabled || props.readonly || !props.hasMoveUp}
+                onClick={props.onReorderClick(props.index, props.index - 1)}/>
+            : null}
+
+            {props.hasMoveUp || props.hasMoveDown ?
+              <IconBtn icon="arrow-down" className="array-item-move-down"
+                tabIndex="-1"
+                style={btnStyle}
+                disabled={props.disabled || props.readonly || !props.hasMoveDown}
+                onClick={props.onReorderClick(props.index, props.index + 1)}/>
+            : null}
+
+            {props.hasRemove ?
+              <IconBtn type="danger" icon="remove" className="array-item-remove"
+                tabIndex="-1"
+                style={btnStyle}
+                disabled={props.disabled || props.readonly}
+                onClick={props.onDropIndexClick(props.index)}/>
+            : null}
+          </div>
+        </div>
+      : null}
+
+    </div>
+  );
+}
+
+function DefaultFixedArrayFieldTemplate(props) {
+  return (
+    <fieldset className={props.className}>
+
+      <ArrayFieldTitle
+          key={`array-field-title-${props.idSchema.$id}`}
+          TitleField={props.TitleField}
+          idSchema={props.idSchema}
+          title={props.title}
+          required={props.required}/>
+
+      {props.schema.description ? (
+        <div className="field-description" key={`field-description-${props.idSchema.$id}`}>
+          {props.schema.description}
+        </div>
+      ) : null}
+
+      <div className="row array-item-list"
+        key={`array-item-list-${props.idSchema.$id}`}>
+        {props.items && props.items.map(DefaultArrayItem)}
+      </div>
+
+      {props.canAdd ? <AddButton
+                        onClick={props.onAddClick}
+                        disabled={props.disabled || props.readonly}/> : null}
+    </fieldset>
+  );
+}
+
+function DefaultNormalArrayFieldTemplate(props) {
+  return (
+    <fieldset className={props.className}>
+
+      <ArrayFieldTitle
+        key={`array-field-title-${props.idSchema.$id}`}
+        TitleField={props.TitleField}
+        idSchema={props.idSchema}
+        title={props.title}
+        required={props.required}/>
+
+      {props.schema.description ? (
+        <ArrayFieldDescription
+          key={`array-field-description-${props.idSchema.$id}`}
+          DescriptionField={props.DescriptionField}
+          idSchema={props.idSchema}
+          description={props.schema.description}/>
+      ) : null}
+
+      <div className="row array-item-list"
+          key={`array-item-list-${props.idSchema.$id}`}>
+          {props.items && props.items.map(p => DefaultArrayItem(p))}
+      </div>
+
+      {props.canAdd ? <AddButton
+                        onClick={props.onAddClick}
+                        disabled={props.disabled || props.readonly}/> : null}
+    </fieldset>
+  );
+}
+
 class ArrayField extends Component {
   static defaultProps = {
     uiSchema: {},
@@ -108,7 +213,9 @@ class ArrayField extends Component {
 
   onDropIndexClick = (index) => {
     return (event) => {
-      event.preventDefault();
+      if (event) {
+        event.preventDefault();
+      }
       this.asyncSetState({
         items: this.state.items.filter((_, i) => i !== index)
       }, {validate: true}); // refs #195
@@ -117,8 +224,10 @@ class ArrayField extends Component {
 
   onReorderClick = (index, newIndex) => {
     return (event) => {
-      event.preventDefault();
-      event.target.blur();
+      if (event) {
+        event.preventDefault();
+        event.target.blur();
+      }
       const {items} = this.state;
       this.asyncSetState({
         items: items.map((item, i) => {
@@ -173,56 +282,58 @@ class ArrayField extends Component {
       disabled,
       readonly,
       autofocus,
+      registry,
+      formContext,
+      onBlur
     } = this.props;
     const title = (schema.title === undefined) ? name : schema.title;
-    const {items} = this.state;
-    const {definitions, fields} = this.props.registry;
+    const {items = []} = this.state;
+    const {ArrayFieldTemplate, definitions, fields} = registry;
     const {TitleField, DescriptionField} = fields;
     const itemsSchema = retrieveSchema(schema.items, definitions);
     const {addable=true} = getUiOptions(uiSchema);
 
-    return (
-      <fieldset
-        className={`field field-array field-array-of-${itemsSchema.type}`}>
-        <ArrayFieldTitle
-          TitleField={TitleField}
-          idSchema={idSchema}
-          title={title}
-          required={required}/>
-        {schema.description ?
-          <ArrayFieldDescription
-            DescriptionField={DescriptionField}
-            idSchema={idSchema}
-            description={schema.description}/> : null}
-        <div className="row array-item-list">{
-          items.map((item, index) => {
-            const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
-            const itemIdPrefix = idSchema.$id + "_" + index;
-            const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
-            return this.renderArrayFieldItem({
-              index,
-              canMoveUp: index > 0,
-              canMoveDown: index < items.length - 1,
-              itemSchema: itemsSchema,
-              itemIdSchema,
-              itemErrorSchema,
-              itemData: items[index],
-              itemUiSchema: uiSchema.items,
-              autofocus: autofocus && index === 0
-            });
-          })
-        }</div>
-        {addable ? <AddButton
-                     onClick={this.onAddClick}
-                     disabled={disabled || readonly}/> : null}
-      </fieldset>
-    );
+    const arrayProps = {
+      canAdd: addable,
+      items: items.map((item, index) => {
+        const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
+        const itemIdPrefix = idSchema.$id + "_" + index;
+        const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
+        return this.renderArrayFieldItem({
+          index,
+          canMoveUp: index > 0,
+          canMoveDown: index < items.length - 1,
+          itemSchema: itemsSchema,
+          itemIdSchema,
+          itemErrorSchema,
+          itemData: items[index],
+          itemUiSchema: uiSchema.items,
+          autofocus: autofocus && index === 0,
+          onBlur
+        });
+      }),
+      className: `field field-array field-array-of-${itemsSchema.type}`,
+      DescriptionField,
+      disabled,
+      idSchema,
+      onAddClick: this.onAddClick,
+      readonly,
+      required,
+      schema,
+      title,
+      TitleField,
+      formContext
+    };
+
+    // Check if a custom render function was passed in
+    const renderFunction = ArrayFieldTemplate || DefaultNormalArrayFieldTemplate;
+    return renderFunction(arrayProps);
   }
 
   renderMultiSelect() {
-    const {schema, idSchema, uiSchema, disabled, readonly, autofocus} = this.props;
+    const {schema, idSchema, uiSchema, disabled, readonly, autofocus, onBlur} = this.props;
     const {items} = this.state;
-    const {widgets, definitions} = this.props.registry;
+    const {widgets, definitions, formContext} = this.props.registry;
     const itemsSchema = retrieveSchema(schema.items, definitions);
     const enumOptions = optionsList(itemsSchema);
     const {widget="select", ...options} = {...getUiOptions(uiSchema), enumOptions};
@@ -232,20 +343,22 @@ class ArrayField extends Component {
         id={idSchema && idSchema.$id}
         multiple
         onChange={this.onSelectChange}
+        onBlur={onBlur}
         options={options}
         schema={schema}
         value={items}
         disabled={disabled}
         readonly={readonly}
+        formContext={formContext}
         autofocus={autofocus}/>
     );
   }
 
   renderFiles() {
-    const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus} = this.props;
+    const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus, onBlur} = this.props;
     const title = schema.title || name;
     const {items} = this.state;
-    const {widgets} = this.props.registry;
+    const {widgets, formContext} = this.props.registry;
     const {widget="files", ...options} = getUiOptions(uiSchema);
     const Widget = getWidget(schema, widget, widgets);
     return (
@@ -254,11 +367,13 @@ class ArrayField extends Component {
         id={idSchema && idSchema.$id}
         multiple
         onChange={this.onSelectChange}
+        onBlur={onBlur}
         schema={schema}
         title={title}
         value={items}
         disabled={disabled}
         readonly={readonly}
+        formContext={formContext}
         autofocus={autofocus}/>
     );
   }
@@ -274,10 +389,12 @@ class ArrayField extends Component {
       disabled,
       readonly,
       autofocus,
+      registry,
+      onBlur
     } = this.props;
     const title = schema.title || name;
     let {items} = this.state;
-    const {definitions, fields} = this.props.registry;
+    const {ArrayFieldTemplate, definitions, fields} = registry;
     const {TitleField} = fields;
     const itemSchemas = schema.items.map(item =>
       retrieveSchema(item, definitions));
@@ -292,49 +409,49 @@ class ArrayField extends Component {
       items = items.concat(new Array(itemSchemas.length - items.length));
     }
 
-    return (
-      <fieldset className="field field-array field-array-fixed-items">
-        <ArrayFieldTitle
-          TitleField={TitleField}
-          idSchema={idSchema}
-          title={title}
-          required={required}/>
-        {schema.description ?
-          <div className="field-description">{schema.description}</div> : null}
-        <div className="row array-item-list">{
-          items.map((item, index) => {
-            const additional = index >= itemSchemas.length;
-            const itemSchema = additional ?
-              additionalSchema : itemSchemas[index];
-            const itemIdPrefix = idSchema.$id + "_" + index;
-            const itemIdSchema = toIdSchema(itemSchema, itemIdPrefix, definitions);
-            const itemUiSchema = additional ?
-              uiSchema.additionalItems || {} :
-              Array.isArray(uiSchema.items) ?
-                uiSchema.items[index] : uiSchema.items || {};
-            const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
-
-            return this.renderArrayFieldItem({
-              index,
-              canRemove: additional,
-              canMoveUp: index >= itemSchemas.length + 1,
-              canMoveDown: additional && index < items.length - 1,
-              itemSchema,
-              itemData: item,
-              itemUiSchema,
-              itemIdSchema,
-              itemErrorSchema,
-              autofocus: autofocus && index === 0
-            });
-          })
-        }</div>
-        {
-          canAdd ? <AddButton
-                               onClick={this.onAddClick}
-                               disabled={disabled || readonly}/> : null
-        }
-      </fieldset>
-    );
+    // These are the props passed into the render function
+    const arrayProps = {
+      canAdd,
+      className: "field field-array field-array-fixed-items",
+      disabled,
+      idSchema,
+      items: items.map((item, index) => {
+        const additional = index >= itemSchemas.length;
+        const itemSchema = additional ?
+          additionalSchema : itemSchemas[index];
+        const itemIdPrefix = idSchema.$id + "_" + index;
+        const itemIdSchema = toIdSchema(itemSchema, itemIdPrefix, definitions);
+        const itemUiSchema = additional ?
+          uiSchema.additionalItems || {} :
+          Array.isArray(uiSchema.items) ?
+            uiSchema.items[index] : uiSchema.items || {};
+        const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
+
+        return this.renderArrayFieldItem({
+          index,
+          canRemove: additional,
+          canMoveUp: index >= itemSchemas.length + 1,
+          canMoveDown: additional && index < items.length - 1,
+          itemSchema,
+          itemData: item,
+          itemUiSchema,
+          itemIdSchema,
+          itemErrorSchema,
+          autofocus: autofocus && index === 0,
+          onBlur
+        });
+      }),
+      onAddClick: this.onAddClick,
+      readonly,
+      required,
+      schema,
+      title,
+      TitleField
+    };
+
+    // Check if a custom template template was passed in
+    const renderFunction = ArrayFieldTemplate || DefaultFixedArrayFieldTemplate;
+    return renderFunction(arrayProps);
   }
 
   renderArrayFieldItem({
@@ -347,7 +464,8 @@ class ArrayField extends Component {
     itemUiSchema,
     itemIdSchema,
     itemErrorSchema,
-    autofocus
+    autofocus,
+    onBlur
   }) {
     const {SchemaField} = this.props.registry.fields;
     const {disabled, readonly, uiSchema} = this.props;
@@ -362,55 +480,34 @@ class ArrayField extends Component {
       remove: removable && canRemove
     };
     has.toolbar = Object.keys(has).some(key => has[key]);
-    const btnStyle = {flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold"};
 
-    return (
-      <div key={index} className="array-item">
-        <div className={has.toolbar ? "col-xs-9" : "col-xs-12"}>
-          <SchemaField
-            schema={itemSchema}
-            uiSchema={itemUiSchema}
-            formData={itemData}
-            errorSchema={itemErrorSchema}
-            idSchema={itemIdSchema}
-            required={this.isItemRequired(itemSchema)}
-            onChange={this.onChangeForIndex(index)}
-            registry={this.props.registry}
-            disabled={this.props.disabled}
-            readonly={this.props.readonly}
-            autofocus={autofocus}/>
-        </div>
-        {
-          has.toolbar ?
-            <div className="col-xs-3 array-item-toolbox">
-              <div className="btn-group" style={{display: "flex", justifyContent: "space-around"}}>
-                {has.moveUp || has.moveDown ?
-                  <IconBtn icon="arrow-up" className="array-item-move-up"
-                          tabIndex="-1"
-                          style={btnStyle}
-                          disabled={disabled || readonly || !has.moveUp}
-                          onClick={this.onReorderClick(index, index - 1)}/>
-                  : null}
-                {has.moveUp || has.moveDown ?
-                  <IconBtn icon="arrow-down" className="array-item-move-down"
-                          tabIndex="-1"
-                          style={btnStyle}
-                          disabled={disabled || readonly || !has.moveDown}
-                          onClick={this.onReorderClick(index, index + 1)}/>
-                  : null}
-                {has.remove ?
-                  <IconBtn type="danger" icon="remove" className="array-item-remove"
-                          tabIndex="-1"
-                          style={btnStyle}
-                          disabled={disabled || readonly}
-                          onClick={this.onDropIndexClick(index)}/>
-                  : null}
-              </div>
-            </div>
-          : null
-        }
-      </div>
-    );
+    return {
+      children: (
+        <SchemaField
+          schema={itemSchema}
+          uiSchema={itemUiSchema}
+          formData={itemData}
+          errorSchema={itemErrorSchema}
+          idSchema={itemIdSchema}
+          required={this.isItemRequired(itemSchema)}
+          onChange={this.onChangeForIndex(index)}
+          onBlur={onBlur}
+          registry={this.props.registry}
+          disabled={this.props.disabled}
+          readonly={this.props.readonly}
+          autofocus={autofocus}/>
+      ),
+      className: "array-item",
+      disabled,
+      hasToolbar: has.toolbar,
+      hasMoveUp: has.moveUp,
+      hasMoveDown: has.moveDown,
+      hasRemove: has.remove,
+      index,
+      onDropIndexClick: this.onDropIndexClick,
+      onReorderClick: this.onReorderClick,
+      readonly
+    };
   }
 }
 
@@ -439,6 +536,7 @@ if (process.env.NODE_ENV !== "production") {
     idSchema: PropTypes.object,
     errorSchema: PropTypes.object,
     onChange: PropTypes.func.isRequired,
+    onBlur: PropTypes.func,
     formData: PropTypes.array,
     required: PropTypes.bool,
     disabled: PropTypes.bool,
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 17e3d506b..1512aa1db 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -91,7 +91,8 @@ class ObjectField extends Component {
       name,
       required,
       disabled,
-      readonly
+      readonly,
+      onBlur
     } = this.props;
     const {definitions, fields, formContext} = this.props.registry;
     const {SchemaField, TitleField, DescriptionField} = fields;
@@ -136,6 +137,7 @@ class ObjectField extends Component {
               idSchema={idSchema[name]}
               formData={this.state[name]}
               onChange={this.onPropertyChange(name)}
+              onBlur={onBlur}
               registry={this.props.registry}
               disabled={disabled}
               readonly={readonly}/>
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 266e183ad..141d7fc88 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -162,6 +162,8 @@ function SchemaField(props) {
   const field = (
     <FieldComponent {...props}
       schema={schema}
+      // See #439: Don't pass consumed class names to child components
+      uiSchema={{...uiSchema, classNames: undefined}}
       disabled={disabled}
       readonly={readonly}
       autofocus={autofocus}
@@ -233,6 +235,7 @@ if (process.env.NODE_ENV !== "production") {
       ])).isRequired,
       fields: PropTypes.objectOf(PropTypes.func).isRequired,
       definitions: PropTypes.object.isRequired,
+      ArrayFieldTemplate: PropTypes.func,
       FieldTemplate: PropTypes.func,
       formContext: PropTypes.object.isRequired,
     })
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index f1ed3eb0f..98df908c9 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -21,7 +21,8 @@ function StringField(props) {
     readonly,
     autofocus,
     registry,
-    onChange
+    onChange,
+    onBlur
   } = props;
   const {title, format} = schema;
   const {widgets, formContext} = registry;
@@ -37,6 +38,7 @@ function StringField(props) {
     label={title === undefined ? name : title}
     value={defaultFieldValue(formData, schema)}
     onChange={onChange}
+    onBlur={onBlur}
     required={required}
     disabled={disabled}
     readonly={readonly}
@@ -52,6 +54,7 @@ if (process.env.NODE_ENV !== "production") {
     uiSchema: PropTypes.object.isRequired,
     idSchema: PropTypes.object,
     onChange: PropTypes.func.isRequired,
+    onBlur: PropTypes.func.isRequired,
     formData: PropTypes.oneOfType([
       React.PropTypes.string,
       React.PropTypes.number,
diff --git a/src/components/widgets/AltDateWidget.js b/src/components/widgets/AltDateWidget.js
index e228b7132..b977f7c9b 100644
--- a/src/components/widgets/AltDateWidget.js
+++ b/src/components/widgets/AltDateWidget.js
@@ -3,8 +3,8 @@ import React, {Component, PropTypes} from "react";
 import {shouldRender, parseDateString, toDateString, pad} from "../../utils";
 
 
-function rangeOptions(type, start, stop) {
-  let options = [{value: -1, label: type}];
+function rangeOptions(start, stop) {
+  let options = [];
   for (let i=start; i<= stop; i++) {
     options.push({value: i, label: pad(i, 2)});
   }
@@ -16,7 +16,7 @@ function readyForChange(state) {
 }
 
 function DateElement(props) {
-  const {type, range, value, select, rootId, disabled, readonly, autofocus, registry} = props;
+  const {type, range, value, select, rootId, disabled, readonly, autofocus, registry, onBlur} = props;
   const id = rootId + "_" + type;
   const {SelectWidget} = registry.widgets;
   return (
@@ -24,12 +24,14 @@ function DateElement(props) {
       schema={{type: "integer"}}
       id={id}
       className="form-control"
-      options={{enumOptions: rangeOptions(type, range[0], range[1])}}
+      options={{enumOptions: rangeOptions(range[0], range[1])}}
+      placeholder={type}
       value={value}
       disabled={disabled}
       readonly={readonly}
       autofocus={autofocus}
-      onChange={(value) => select(type, value)}/>
+      onChange={(value) => select(type, value)}
+      onBlur={onBlur}/>
   );
 }
 
@@ -55,7 +57,7 @@ class AltDateWidget extends Component {
   }
 
   onChange = (property, value) => {
-    this.setState({[property]: value}, () => {
+    this.setState({[property]: typeof value === "undefined" ? -1 : value}, () => {
       // Only propagate to parent state if we have a complete date{time}
       if (readyForChange(this.state)) {
         this.props.onChange(toDateString(this.state, this.props.time));
@@ -101,7 +103,7 @@ class AltDateWidget extends Component {
   }
 
   render() {
-    const {id, disabled, readonly, autofocus, registry} = this.props;
+    const {id, disabled, readonly, autofocus, registry, onBlur} = this.props;
     return (
       <ul className="list-inline">{
         this.dateElementProps.map((elemProps, i) => (
@@ -113,6 +115,7 @@ class AltDateWidget extends Component {
               disabled= {disabled}
               readonly={readonly}
               registry={registry}
+              onBlur={onBlur}
               autofocus={autofocus && i === 0}/>
           </li>
         ))
@@ -140,6 +143,7 @@ if (process.env.NODE_ENV !== "production") {
     readonly: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
+    onBlur: PropTypes.func,
     time: PropTypes.bool,
   };
 }
diff --git a/src/components/widgets/BaseInput.js b/src/components/widgets/BaseInput.js
index 4710a0c6e..d07ba5021 100644
--- a/src/components/widgets/BaseInput.js
+++ b/src/components/widgets/BaseInput.js
@@ -8,13 +8,16 @@ function BaseInput(props) {
     value,
     readonly,
     autofocus,
-    onChange,
+    onBlur,
     options,  // eslint-disable-line
     schema,   // eslint-disable-line
     formContext,  // eslint-disable-line
     registry, // eslint-disable-line
     ...inputProps
   } = props;
+  const _onChange = ({target: {value}}) => {
+    return props.onChange(value === "" ? undefined : value);
+  };
   return (
     <input
       {...inputProps}
@@ -22,7 +25,8 @@ function BaseInput(props) {
       readOnly={readonly}
       autoFocus={autofocus}
       value={typeof value === "undefined" ? "" : value}
-      onChange={(event) => onChange(event.target.value)}/>
+      onChange={_onChange}
+      onBlur={onBlur && (event => onBlur(inputProps.id, event.target.value))}/>
   );
 }
 
@@ -44,6 +48,7 @@ if (process.env.NODE_ENV !== "production") {
     readonly: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
+    onBlur: PropTypes.func
   };
 }
 
diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js
index 32f0545d5..0595e5dff 100644
--- a/src/components/widgets/SelectWidget.js
+++ b/src/components/widgets/SelectWidget.js
@@ -2,13 +2,14 @@ import React, {PropTypes} from "react";
 
 import {asNumber} from "../../utils";
 
-
 /**
  * This is a silly limitation in the DOM where option change event values are
  * always retrieved as strings.
  */
 function processValue({type, items}, value) {
-  if (type === "array" && items && ["number", "integer"].includes(items.type)) {
+  if (value === "") {
+    return undefined;
+  } else if (type === "array" && items && ["number", "integer"].includes(items.type)) {
     return value.map(asNumber);
   } else if (type === "boolean") {
     return value === "true";
@@ -18,6 +19,16 @@ function processValue({type, items}, value) {
   return value;
 }
 
+function getValue(event, multiple) {
+  if (multiple) {
+    return [].slice.call(event.target.options)
+      .filter(o => o.selected)
+      .map(o => o.value);
+  } else {
+    return event.target.value;
+  }
+}
+
 function SelectWidget({
   schema,
   id,
@@ -28,33 +39,35 @@ function SelectWidget({
   readonly,
   multiple,
   autofocus,
-  onChange
+  onChange,
+  onBlur,
+  placeholder
 }) {
   const {enumOptions} = options;
+  const emptyValue = multiple ? [] : "";
   return (
     <select
       id={id}
       multiple={multiple}
       className="form-control"
-      value={value}
+      value={typeof value === "undefined" ? emptyValue : value}
       required={required}
       disabled={disabled}
       readOnly={readonly}
       autoFocus={autofocus}
+      onBlur={onBlur && (event => {
+        const newValue = getValue(event, multiple);
+        onBlur(id, processValue(schema, newValue));
+      })}
       onChange={(event) => {
-        let newValue;
-        if (multiple) {
-          newValue = [].filter.call(
-            event.target.options, o => o.selected).map(o => o.value);
-        } else {
-          newValue = event.target.value;
-        }
+        const newValue = getValue(event, multiple);
         onChange(processValue(schema, newValue));
-      }}>{
-      enumOptions.map(({value, label}, i) => {
+      }}>
+      {!multiple && !schema.default && <option value="">{placeholder}</option>}
+      {enumOptions.map(({value, label}, i) => {
         return <option key={i} value={value}>{label}</option>;
-      })
-    }</select>
+      })}
+    </select>
   );
 }
 
@@ -74,6 +87,7 @@ if (process.env.NODE_ENV !== "production") {
     multiple: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
+    onBlur: PropTypes.func,
   };
 }
 
diff --git a/src/components/widgets/TextareaWidget.js b/src/components/widgets/TextareaWidget.js
index d7be10236..7bcb03b26 100644
--- a/src/components/widgets/TextareaWidget.js
+++ b/src/components/widgets/TextareaWidget.js
@@ -10,8 +10,12 @@ function TextareaWidget({
   disabled,
   readonly,
   autofocus,
-  onChange
+  onChange,
+  onBlur
 }) {
+  const _onChange = ({target: {value}}) => {
+    return onChange(value === "" ? undefined : value);
+  };
   return (
     <textarea
       id={id}
@@ -22,7 +26,8 @@ function TextareaWidget({
       disabled={disabled}
       readOnly={readonly}
       autoFocus={autofocus}
-      onChange={(event) => onChange(event.target.value)}/>
+      onBlur={onBlur && (event => onBlur(id, event.target.value))}
+      onChange={_onChange}/>
   );
 }
 
@@ -39,6 +44,7 @@ if (process.env.NODE_ENV !== "production") {
     required: PropTypes.bool,
     autofocus: PropTypes.bool,
     onChange: PropTypes.func,
+    onBlur: PropTypes.func,
   };
 }
 
diff --git a/src/utils.js b/src/utils.js
index 96a94db26..4656c9901 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -116,9 +116,6 @@ function computeDefaults(schema, parentDefaults, definitions={}) {
   } else if ("default" in schema) {
     // Use schema defaults for this node.
     defaults = schema.default;
-  } else if ("enum" in schema && Array.isArray(schema.enum)) {
-    // For enum with no defined default, select the first entry.
-    defaults = schema.enum[0];
   } else if ("$ref" in schema) {
     // Use referenced schema defaults for this node.
     const refSchema = findSchemaDefinition(schema.$ref, definitions);
@@ -400,7 +397,7 @@ export function toIdSchema(schema, id, definitions) {
     const _schema = retrieveSchema(schema, definitions);
     return toIdSchema(_schema, id, definitions);
   }
-  if ("items" in schema) {
+  if ("items" in schema && !schema.items.$ref) {
     return toIdSchema(schema.items, id, definitions);
   }
   if (schema.type !== "object") {
diff --git a/src/validate.js b/src/validate.js
index 371546160..0a7fe27a9 100644
--- a/src/validate.js
+++ b/src/validate.js
@@ -96,8 +96,11 @@ function unwrapErrorHandler(errorHandler) {
  * function, which receives the form data and an `errorHandler` object that
  * will be used to add custom validation errors for each field.
  */
-export default function validateFormData(formData, schema, customValidate) {
-  const {errors} = jsonValidate(formData, schema);
+export default function validateFormData(formData, schema, customValidate, transformErrors) {
+  let {errors} = jsonValidate(formData, schema);
+  if (typeof transformErrors === "function") {
+    errors = transformErrors(errors);
+  }
   const errorSchema = toErrorSchema(errors);
 
   if (typeof customValidate !== "function") {


Test patch:
diff --git a/test/ArrayFieldTemplate_test.js b/test/ArrayFieldTemplate_test.js
new file mode 100644
index 000000000..2fa744ffd
--- /dev/null
+++ b/test/ArrayFieldTemplate_test.js
@@ -0,0 +1,142 @@
+import React from "react";
+
+import {expect} from "chai";
+import {createFormComponent, createSandbox} from "./test_utils";
+
+describe("ArrayFieldTemplate", () => {
+  let sandbox;
+
+  beforeEach(() => {
+    sandbox = createSandbox();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  describe("Custom ArrayFieldTemplate of string array", () => {
+
+    function ArrayFieldTemplate(props) {
+      return (
+        <div className="custom-array">
+          {props.canAdd && <button className="custom-array-add"></button>}
+          {props.items.map(element => {
+            return (
+              <div className="custom-array-item" key={element.index}>
+                {element.hasMoveUp &&
+                  <button className="custom-array-item-move-up"></button>}
+                {element.hasMoveDown &&
+                  <button className="custom-array-item-move-down"></button>}
+
+                {element.children}
+              </div>
+            );
+          })}
+        </div>
+      );
+    }
+
+    const formData = ["one", "two", "three"];
+
+    describe("not fixed items", () => {
+      const schema = {
+        type: "array",
+        title: "my list",
+        description: "my description",
+        items: {type: "string"}
+      };
+
+      let node;
+      beforeEach(() => {
+        node = createFormComponent({
+          ArrayFieldTemplate,
+          formData,
+          schema
+        }).node;
+      });
+
+      it("should render one root element for the array", () => {
+        expect(node.querySelectorAll(".custom-array"))
+          .to.have.length.of(1);
+      });
+
+      it("should render one add button", () => {
+        expect(node.querySelectorAll(".custom-array-add"))
+          .to.have.length.of(1);
+      });
+
+      it("should render one child for each array item", () => {
+        expect(node.querySelectorAll(".custom-array-item"))
+         .to.have.length.of(formData.length);
+      });
+
+      it("should render text input for each array item", () => {
+        expect(node.querySelectorAll(".custom-array-item .field input[type=text]"))
+          .to.have.length.of(formData.length);
+      });
+
+      it("should render move up button for all but one array items", () => {
+        expect(node.querySelectorAll(".custom-array-item-move-up"))
+          .to.have.length.of(formData.length - 1);
+      });
+
+      it("should render move down button for all but one array items", () => {
+        expect(node.querySelectorAll(".custom-array-item-move-down"))
+          .to.have.length.of(formData.length - 1);
+      });
+    });
+
+    describe("fixed items", () => {
+      const schema = {
+        type: "array",
+        title: "my list",
+        description: "my description",
+        items: [
+          {type: "string"},
+          {type: "string"},
+          {type: "string"}
+        ]
+      };
+
+      let node;
+      beforeEach(() => {
+        node = createFormComponent({
+          ArrayFieldTemplate,
+          formData,
+          schema
+        }).node;
+      });
+
+      it("should render one root element for the array", () => {
+        expect(node.querySelectorAll(".custom-array"))
+          .to.have.length.of(1);
+      });
+
+      it("should not render an add button", () => {
+        expect(node.querySelectorAll(".custom-array-add"))
+          .to.have.length.of(0);
+      });
+
+      it("should render one child for each array item", () => {
+        expect(node.querySelectorAll(".custom-array-item"))
+          .to.have.length.of(formData.length);
+      });
+
+      it("should render text input for each array item", () => {
+        expect(node.querySelectorAll(".custom-array-item .field input[type=text]"))
+          .to.have.length.of(formData.length);
+      });
+
+      it("should not render any move up buttons", () => {
+        expect(node.querySelectorAll(".custom-array-item-move-up"))
+          .to.have.length.of(0);
+      });
+
+      it("should not render any move down buttons", () => {
+        expect(node.querySelectorAll(".custom-array-item-move-down"))
+          .to.have.length.of(0);
+      });
+    });
+
+  });
+});
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index f4febee80..c71b49547 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -322,6 +322,22 @@ describe("ArrayField", () => {
         expect(comp.state.formData).eql(["foo", "bar"]);
       });
 
+      it("should handle a blur event", () => {
+        const onBlur = sandbox.spy();
+        const {node} = createFormComponent({schema, onBlur});
+
+        const select = node.querySelector(".field select");
+        Simulate.blur(select, {
+          target: {options: [
+            {selected: true, value: "foo"},
+            {selected: true, value: "bar"},
+            {selected: false, value: "fuzz"},
+          ]}
+        });
+
+        expect(onBlur.calledWith(select.id, ["foo", "bar"])).to.be.true;
+      });
+
       it("should fill field with data", () => {
         const {node} = createFormComponent({schema, formData: ["foo", "bar"]});
 
diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js
index 6d3be909c..54831dc95 100644
--- a/test/BooleanField_test.js
+++ b/test/BooleanField_test.js
@@ -120,7 +120,7 @@ describe("BooleanField", () => {
 
     const labels = [].map.call(node.querySelectorAll(".field option"),
                                label => label.textContent);
-    expect(labels).eql(["Yes", "No"]);
+    expect(labels).eql(["", "Yes", "No"]);
   });
 
   it("should render the widget with the expected id", () => {
diff --git a/test/FormContext_test.js b/test/FormContext_test.js
index 10608ada3..c285ea78d 100644
--- a/test/FormContext_test.js
+++ b/test/FormContext_test.js
@@ -66,7 +66,6 @@ describe("FormContext", () => {
     const {node} = createFormComponent({
       schema: {
         type: "object",
-        title: "A title",
         properties: {
           prop: {
             type:  "string"
@@ -81,6 +80,26 @@ describe("FormContext", () => {
       .to.exist;
   });
 
+  it("should be passed to ArrayTemplateField", () => {
+    function CustomArrayTemplateField({formContext}) {
+      return <div id={formContext.foo}/>;
+    }
+
+    const {node} = createFormComponent({
+      schema: {
+        type: "array",
+        items: {
+          type:  "string"
+        }
+      },
+      ArrayFieldTemplate: CustomArrayTemplateField,
+      formContext
+    });
+
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
+  });
+
   it("should be passed to custom TitleField", () => {
     const fields = {TitleField: CustomComponent};
 
@@ -114,4 +133,43 @@ describe("FormContext", () => {
     expect(node.querySelector("#" + formContext.foo))
       .to.exist;
   });
+
+
+  it("should be passed to multiselect", () => {
+    const widgets = {SelectWidget: CustomComponent};
+    const {node} = createFormComponent({
+      schema: {
+        type: "array",
+        items: {
+          type: "string",
+          enum: ["foo"],
+          enumNames: ["bar"]
+        },
+        uniqueItems: true
+      },
+      widgets,
+      formContext
+    });
+
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
+  });
+
+  it("should be passed to files array", () => {
+    const widgets = {FileWidget: CustomComponent};
+    const {node} = createFormComponent({
+      schema: {
+        type: "array",
+        items: {
+          type: "string",
+          format: "data-url"
+        }
+      },
+      widgets,
+      formContext
+    });
+
+    expect(node.querySelector("#" + formContext.foo))
+      .to.exist;
+  });
 });
diff --git a/test/Form_test.js b/test/Form_test.js
index be0c0ce02..c55f8a0c1 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -313,6 +313,36 @@ describe("Form", () => {
         .eql("hello");
     });
 
+    it("should recursively handle referenced definitions", () => {
+      const schema = {
+        $ref: "#/definitions/node",
+        definitions: {
+          node: {
+            type: "object",
+            properties: {
+              name: {type: "string"},
+              children: {
+                type: "array",
+                items: {
+                  $ref: "#/definitions/node"
+                }
+              }
+            }
+          }
+        }
+      };
+
+      const {node} = createFormComponent({schema});
+
+      expect(node.querySelector("#root_children_0_name"))
+        .to.not.exist;
+
+      Simulate.click(node.querySelector(".array-item-add button"));
+
+      expect(node.querySelector("#root_children_0_name"))
+        .to.exist;
+    });
+
     it("should priorize definition over schema type property", () => {
       // Refs bug #140
       const schema = {
@@ -381,7 +411,7 @@ describe("Form", () => {
       const {node} = createFormComponent({schema});
 
       expect(node.querySelectorAll("option"))
-        .to.have.length.of(2);
+        .to.have.length.of(3);
     });
   });
 
@@ -503,6 +533,30 @@ describe("Form", () => {
       });
     });
   });
+  describe("Blur handler", () => {
+    it("should call provided blur handler on form input blur event", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {
+            type: "string",
+          },
+        }
+      };
+      const formData = {
+        foo: ""
+      };
+      const onBlur = sandbox.spy();
+      const {node} = createFormComponent({schema, formData, onBlur});
+
+      const input = node.querySelector("[type=text]");
+      Simulate.blur(input, {
+        target: {value: "new"}
+      });
+
+      sinon.assert.calledWithMatch(onBlur, input.id, "new");
+    });
+  });
 
   describe("Error handler", () => {
     it("should call provided error handler on validation errors", () => {
@@ -1131,7 +1185,8 @@ describe("Form", () => {
       action: "/users/list",
       autocomplete: "off",
       enctype: "multipart/form-data",
-      acceptcharset: "ISO-8859-1"
+      acceptcharset: "ISO-8859-1",
+      noHtml5Validate: true
     };
 
     let node;
@@ -1175,5 +1230,9 @@ describe("Form", () => {
     it("should set attr acceptcharset of form", () => {
       expect(node.getAttribute("accept-charset")).eql(formProps.acceptcharset);
     });
+
+    it("should set attr novalidate of form", () => {
+      expect(node.getAttribute("novalidate")).not.to.be.null;
+    });
   });
 });
diff --git a/test/NumberField_test.js b/test/NumberField_test.js
index f7081bf76..80b468c10 100644
--- a/test/NumberField_test.js
+++ b/test/NumberField_test.js
@@ -74,6 +74,19 @@ describe("NumberField", () => {
       expect(comp.state.formData).eql(2);
     });
 
+    it("should handle a blur event", () => {
+      const onBlur = sandbox.spy();
+      const {node} = createFormComponent({schema: {
+        type: "number",
+      }, onBlur});
+
+      const input = node.querySelector("input");
+      Simulate.blur(input, {
+        target: {value: "2"}
+      });
+
+      expect(onBlur.calledWith(input.id, 2));
+    });
     it("should fill field with data", () => {
       const {node} = createFormComponent({schema: {
         type: "number",
diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js
index 40f3a88f7..18e86a5e8 100644
--- a/test/ObjectField_test.js
+++ b/test/ObjectField_test.js
@@ -133,6 +133,18 @@ describe("ObjectField", () => {
       expect(comp.state.formData.foo).eql("changed");
     });
 
+    it("should handle object fields with blur events", () => {
+      const onBlur = sandbox.spy();
+      const {node} = createFormComponent({schema, onBlur});
+
+      const input = node.querySelector("input[type=text]");
+      Simulate.blur(input, {
+        target: {value: "changed"}
+      });
+
+      expect(onBlur.calledWith(input.id, "changed")).to.be.true;
+    });
+
     it("should render the widget with the expected id", () => {
       const {node} = createFormComponent({schema});
 
diff --git a/test/SchemaField_test.js b/test/SchemaField_test.js
index 70924f81f..b002f1a95 100644
--- a/test/SchemaField_test.js
+++ b/test/SchemaField_test.js
@@ -138,6 +138,29 @@ describe("SchemaField", () => {
       expect(node.querySelectorAll("#custom"))
         .to.have.length.of(1);
     });
+
+    it("should not pass classNames to child component", () => {
+      const CustomSchemaField = function(props) {
+        return (
+          <SchemaField
+            {...props}
+            uiSchema={{...props.uiSchema, "ui:field": undefined}}/>
+        );
+      };
+
+      const schema = {
+        type: "string"
+      };
+      const uiSchema = {
+        "ui:field": "customSchemaField",
+        classNames: "foo"};
+      const fields = {"customSchemaField": CustomSchemaField};
+
+      const {node} = createFormComponent({schema, uiSchema, fields});
+
+      expect(node.querySelectorAll(".foo"))
+        .to.have.length.of(1);
+    });
   });
 
   describe("description support", () => {
diff --git a/test/StringField_test.js b/test/StringField_test.js
index e5451dd1f..92b94a9de 100644
--- a/test/StringField_test.js
+++ b/test/StringField_test.js
@@ -77,6 +77,32 @@ describe("StringField", () => {
       expect(comp.state.formData).eql("yo");
     });
 
+    it("should handle a blur event", () => {
+      const onBlur = sandbox.spy();
+      const {node} = createFormComponent({schema: {
+        type: "string",
+      }, onBlur});
+      const input = node.querySelector("input");
+      Simulate.blur(input, {
+        target: {value: "yo"}
+      });
+
+      expect(onBlur.calledWith(input.id, "yo")).to.be.true;
+    });
+    
+    it("should handle an empty string change event", () => {
+      const {comp, node} = createFormComponent({
+        schema: {type: "string"},
+        formData: "x",
+      });
+
+      Simulate.change(node.querySelector("input"), {
+        target: {value: ""}
+      });
+
+      expect(comp.state.formData).eql(undefined);
+    });
+
     it("should fill field with data", () => {
       const {node} = createFormComponent({schema: {
         type: "string",
@@ -132,6 +158,31 @@ describe("StringField", () => {
         .eql("foo");
     });
 
+    it("should render empty option", () => {
+      const {node} = createFormComponent({schema: {
+        type: "string",
+        enum: ["foo", "bar"],
+      }});
+
+      expect(node.querySelectorAll(".field option")[0].value)
+        .eql("");
+    });
+
+    it("should render empty option with placeholder text", () => {
+      const {node} = createFormComponent({schema: {
+        type: "string",
+        enum: ["foo", "bar"],
+      }, uiSchema: {
+        "ui:options": {
+          placeholder: "Test"
+        }
+      }});
+
+      console.log(node.querySelectorAll(".field option")[0].innerHTML);
+      expect(node.querySelectorAll(".field option")[0].textContent)
+        .eql("Test");
+    });
+
     it("should assign a default value", () => {
       const {comp} = createFormComponent({schema: {
         type: "string",
@@ -155,6 +206,19 @@ describe("StringField", () => {
       expect(comp.state.formData).eql("foo");
     });
 
+    it("should reflect undefined into form state if empty option selected", () => {
+      const {comp, node} = createFormComponent({schema: {
+        type: "string",
+        enum: ["foo", "bar"],
+      }});
+
+      Simulate.change(node.querySelector("select"), {
+        target: {value: ""}
+      });
+
+      expect(comp.state.formData).to.be.undefined;
+    });
+
     it("should reflect the change into the dom", () => {
       const {node} = createFormComponent({schema: {
         type: "string",
@@ -168,6 +232,19 @@ describe("StringField", () => {
       expect(node.querySelector("select").value).eql("foo");
     });
 
+    it("should reflect undefined value into the dom as empty option", () => {
+      const {node} = createFormComponent({schema: {
+        type: "string",
+        enum: ["foo", "bar"],
+      }});
+
+      Simulate.change(node.querySelector("select"), {
+        target: {value: ""}
+      });
+
+      expect(node.querySelector("select").value).eql("");
+    });
+
     it("should fill field with data", () => {
       const {comp} = createFormComponent({schema: {
         type: "string",
@@ -203,6 +280,22 @@ describe("StringField", () => {
     });
   });
 
+  describe("TextareaWidget", () => {
+    it("should handle an empty string change event", () => {
+      const {comp, node} = createFormComponent({
+        schema: {type: "string"},
+        uiSchema: {"ui:widget": "textarea"},
+        formData: "x",
+      });
+
+      Simulate.change(node.querySelector("textarea"), {
+        target: {value: ""}
+      });
+
+      expect(comp.state.formData).eql(undefined);
+    });
+  });
+
   describe("DateTimeWidget", () => {
     it("should render an datetime-local field", () => {
       const {node} = createFormComponent({schema: {
@@ -508,7 +601,7 @@ describe("StringField", () => {
       const monthOptions = node.querySelectorAll("select#root_month option");
       const monthOptionsValues = [].map.call(monthOptions, o => o.value);
       expect(monthOptionsValues).eql([
-        "-1", "1", "2", "3", "4", "5", "6",
+        "", "1", "2", "3", "4", "5", "6",
         "7", "8", "9", "10", "11", "12"]);
     });
 
@@ -692,7 +785,7 @@ describe("StringField", () => {
       const monthOptions = node.querySelectorAll("select#root_month option");
       const monthOptionsValues = [].map.call(monthOptions, o => o.value);
       expect(monthOptionsValues).eql([
-        "-1", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]);
+        "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]);
     });
 
     it("should render the widgets with the expected options' labels", () => {
diff --git a/test/performance_test.js b/test/performance_test.js
index 098230bd1..481698217 100644
--- a/test/performance_test.js
+++ b/test/performance_test.js
@@ -50,6 +50,7 @@ describe("Rendering performance optimizations", () => {
 
   describe("ArrayField", () => {
     const onChange = () => {};
+    const onBlur = () => {};
     const schema = {type: "array", items: {type: "string"}};
     const uiSchema = {};
     const registry = getDefaultRegistry();
@@ -60,6 +61,7 @@ describe("Rendering performance optimizations", () => {
         schema,
         uiSchema,
         onChange,
+        onBlur
       });
       sandbox.stub(comp, "render").returns(<div/>);
 
@@ -76,6 +78,7 @@ describe("Rendering performance optimizations", () => {
         schema,
         formData,
         onChange,
+        onBlur
       });
       sandbox.stub(comp, "render").returns(<div/>);
 
@@ -87,6 +90,7 @@ describe("Rendering performance optimizations", () => {
 
   describe("ObjectField", () => {
     const onChange = () => {};
+    const onBlur = () => {};
     const registry = getDefaultRegistry();
     const uiSchema = {};
     const schema = {
@@ -104,6 +108,7 @@ describe("Rendering performance optimizations", () => {
         uiSchema,
         onChange,
         idSchema,
+        onBlur
       });
       sandbox.stub(comp, "render").returns(<div/>);
 
@@ -121,6 +126,7 @@ describe("Rendering performance optimizations", () => {
         formData,
         onChange,
         idSchema,
+        onBlur
       });
       sandbox.stub(comp, "render").returns(<div/>);
 
diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js
index f248137f6..3d33899ef 100644
--- a/test/uiSchema_test.js
+++ b/test/uiSchema_test.js
@@ -1158,15 +1158,15 @@ describe("uiSchema", () => {
         const {node} = createFormComponent({schema, uiSchema});
 
         expect(node.querySelectorAll("select option"))
-          .to.have.length.of(2);
+          .to.have.length.of(3);
       });
 
       it("should render boolean option labels", () => {
         const {node} = createFormComponent({schema, uiSchema});
 
-        expect(node.querySelectorAll("option")[0].textContent)
-          .eql("yes");
         expect(node.querySelectorAll("option")[1].textContent)
+          .eql("yes");
+        expect(node.querySelectorAll("option")[2].textContent)
           .eql("no");
       });
 
diff --git a/test/utils_test.js b/test/utils_test.js
index 912b964c3..97fbfc07c 100644
--- a/test/utils_test.js
+++ b/test/utils_test.js
@@ -178,20 +178,6 @@ describe("utils", () => {
           .eql({level1: [1, 2, 3]});
       });
 
-      it("should use first enum value when no default is specified", () => {
-        const schema = {
-          type: "object",
-          properties: {
-            foo: {
-              type: "string",
-              enum: ["a", "b", "c"],
-            }
-          }
-        };
-        expect(getDefaultFormState(schema, {}))
-          .eql({foo: "a"});
-      });
-
       it("should map item defaults to fixed array default", () => {
         const schema = {
           type: "object",
diff --git a/test/validate_test.js b/test/validate_test.js
index fff8f796e..f3d12a028 100644
--- a/test/validate_test.js
+++ b/test/validate_test.js
@@ -94,6 +94,32 @@ describe("Validation", () => {
         ]);
       });
     });
+
+    describe("transformErrors", () => {
+      const illFormedKey = "bar.'\"[]()=+*&^%$#@!";
+      const schema = {
+        type: "object",
+        properties: {foo: {type: "string"}, [illFormedKey]: {type: "string"}}
+      };
+      const newErrorMessage = "Better error message";
+      const transformErrors = (errors) => {
+        return [
+          Object.assign({}, errors[0], {message: newErrorMessage})
+        ];
+      };
+
+      let errors;
+
+      beforeEach(() => {
+        const result = validateFormData({foo: 42, [illFormedKey]: 41}, schema, undefined, transformErrors);
+        errors = result.errors;
+      });
+
+      it("should use transformErrors function", () => {
+        expect(errors).not.to.be.empty;
+        expect(errors[0].message).to.equal(newErrorMessage);
+      });
+    });
   });
 
   describe("Form integration", () => {