Repository: mozilla-services/react-jsonschema-form
Pull request #450 (2017-01-24 17:23:32)

Add support for rows attribute of textarea widget.
### Reasons for making this change

Add support for rows attribute of textarea widget.
Now users can specify initial height of it.

### Checklist

* [x] **I'm updating documentation**
  - [x] I've checked the rendering of the Markdown text I've added
  - [x] If I'm adding a new section, I've updated the Table of Content
* [x] **I'm adding or updating code**
  - [x] I've added and/or updated tests
  - [x] I've updated docs if needed
* [x] **I'm adding a new feature**
  - [x] I've updated the playground with an example use of the feature



Patch:
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..2c01c1402
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+indent_style = space
+indent_size = 2
+charset = utf-8
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
index 7e671b889..03c1b53ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@ node_modules
 build
 dist
 lib
-
+yarn.lock
diff --git a/README.md b/README.md
index f4cb9bfc2..35a5db40c 100644
--- a/README.md
+++ b/README.md
@@ -46,11 +46,12 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
      - [Form action buttons](#form-action-buttons)
      - [Help texts](#help-texts)
      - [Auto focus](#auto-focus)
+     - [Textarea rows option](#textarea-rows-option)
      - [Placeholders](#placeholders)
      - [Form attributes](#form-attributes)
   - [Advanced customization](#advanced-customization)
      - [Field template](#field-template)
-     - [Array field template](#array-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)
@@ -66,10 +67,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)
+     - [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)
@@ -85,6 +87,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
 
@@ -239,12 +245,12 @@ const uiSchema = {
     bar: {
       "ui:widget": "textarea"
     },
-    baz: {
-      // note the "items" for an array
-      items: {
-        description: {
-          "ui:widget": "textarea"
-        }
+  },
+  baz: {
+    // note the "items" for an array
+    items: {
+      description: {
+        "ui:widget": "textarea"
       }
     }
   }
@@ -641,6 +647,20 @@ const uiSchema = {
 }
 ```
 
+### Textarea `rows` option
+
+You can set initial height of a textarea widget by specifying `rows` option.
+
+```js
+const schema = {type: "string"};
+const uiSchema = {
+  "ui:widget": "textarea",
+  "ui:options": {
+    rows: 15
+  }
+}
+```
+
 ### Placeholders
 
 Text fields can benefit from placeholders by using the `ui:placeholder` uiSchema directive:
@@ -654,6 +674,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:
@@ -762,6 +791,7 @@ The following props are passed to each `ArrayFieldTemplate`:
 - `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`:
 
@@ -995,14 +1025,14 @@ You can provide a `formContext` object to the Form, which is passed down to all
 
 ### Custom array field buttons
 
-The `ArrayField` component provides a UI to add, remove and reorder array items, and these buttons use [Bootstrap glyphicons](http://getbootstrap.com/components/#glyphicons). If you don't use Bootstrap yet still want to provide your own icons or texts for these buttons, you can easily do so using CSS:
+The `ArrayField` component provides a UI to add, remove and reorder array items, and these buttons use [Bootstrap glyphicons](http://getbootstrap.com/components/#glyphicons). If you don't use glyphicons but still want to provide your own icons or texts for these buttons, you can easily do so using CSS:
 
 ```css
-.btn-plus > i {
-  display: none;
-}
-.btn-plus::after {
-  content: "Add";
+i.glyphicon { display: none; }
+.btn-add::after { content: 'Add'; }
+.array-item-move-up::after { content: 'Move Up'; }
+.array-item-move-down::after { content: 'Move Down'; }
+.array-item-remove::after { content: 'Remove'; }
 }
 ```
 
@@ -1185,7 +1215,7 @@ render((
 
 ### 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. 
+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) {
@@ -1224,6 +1254,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..8535d32d8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-jsonschema-form",
-  "version": "0.41.2",
+  "version": "0.43.0",
   "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 e3b25acab..770599e92 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -362,6 +362,7 @@ class App extends Component {
               uiSchema={uiSchema}
               formData={formData}
               onChange={this.onFormDataChange}
+              onSubmit={({formData}) => console.log("submitted formData", formData)}
               fields={{geo: GeoPosition}}
               validate={validate}
               onBlur={(id, value) => console.log(`Touched ${id} with value ${value}`)}
diff --git a/playground/samples/index.js b/playground/samples/index.js
index 8eba6cc82..1527696cc 100644
--- a/playground/samples/index.js
+++ b/playground/samples/index.js
@@ -29,5 +29,5 @@ export const samples = {
   Validation: validation,
   Files: files,
   Single: single,
-  "Custom Array": customArray
+  "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/single.js b/playground/samples/single.js
index 40aa07084..af638d1ac 100644
--- a/playground/samples/single.js
+++ b/playground/samples/single.js
@@ -3,5 +3,6 @@ module.exports = {
     title: "A single-field form",
     type: "string",
   },
-  formData: "initial value"
+  formData: "initial value",
+  uiSchema: {}
 };
diff --git a/playground/samples/widgets.js b/playground/samples/widgets.js
index a91820598..cca38b1f0 100644
--- a/playground/samples/widgets.js
+++ b/playground/samples/widgets.js
@@ -95,7 +95,10 @@ module.exports = {
     },
     string: {
       textarea: {
-        "ui:widget": "textarea"
+        "ui:widget": "textarea",
+        "ui:options": {
+          rows: 5
+        }
       },
       color: {
         "ui:widget": "color"
diff --git a/src/components/Form.js b/src/components/Form.js
index 7f8f2f31e..078c6bbd0 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -18,7 +18,7 @@ export default class Form extends Component {
     liveValidate: false,
     safeRenderCompletion: false,
     noHtml5Validate: false
-  }
+  };
 
   constructor(props) {
     super(props);
@@ -93,7 +93,7 @@ export default class Form extends Component {
     if (this.props.onBlur) {
       this.props.onBlur(...args);
     }
-  }
+  };
 
   onSubmit = (event) => {
     event.preventDefault();
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index d610ef081..5344b2f80 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -12,8 +12,7 @@ import {
   retrieveSchema,
   toIdSchema,
   shouldRender,
-  getDefaultRegistry,
-  setState
+  getDefaultRegistry
 } from "../../utils";
 
 function ArrayFieldTitle({TitleField, idSchema, title, required}) {
@@ -151,6 +150,7 @@ function DefaultNormalArrayFieldTemplate(props) {
 class ArrayField extends Component {
   static defaultProps = {
     uiSchema: {},
+    formData: [],
     idSchema: {},
     registry: getDefaultRegistry(),
     required: false,
@@ -159,23 +159,6 @@ class ArrayField extends Component {
     autofocus: false,
   };
 
-  constructor(props) {
-    super(props);
-    this.state = this.getStateFromProps(props);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.setState(this.getStateFromProps(nextProps));
-  }
-
-  getStateFromProps(props) {
-    const formData = Array.isArray(props.formData) ? props.formData : null;
-    const {definitions} = this.props.registry;
-    return {
-      items: getDefaultFormState(props.schema, formData, definitions) || []
-    };
-  }
-
   shouldComponentUpdate(nextProps, nextState) {
     return shouldRender(this, nextProps, nextState);
   }
@@ -185,30 +168,28 @@ class ArrayField extends Component {
     return schema.items.title || schema.items.description || "Item";
   }
 
-  isItemRequired(itemsSchema) {
-    return itemsSchema.type === "string" && itemsSchema.minLength > 0;
-  }
-
-  asyncSetState(state, options={validate: false}) {
-    setState(this, state, () => {
-      this.props.onChange(this.state.items, options);
-    });
+  isItemRequired(itemSchema) {
+    if (Array.isArray(itemSchema.type)) {
+      // While we don't yet support composite/nullable jsonschema types, it's
+      // future-proof to check for requirement against these.
+      return !itemSchema.type.includes("null");
+    }
+    // All non-null array item types are inherently required by design
+    return itemSchema.type !== "null";
   }
 
   onAddClick = (event) => {
     event.preventDefault();
-    const {items} = this.state;
-    const {schema, registry} = this.props;
+    const {schema, registry, formData} = this.props;
     const {definitions} = registry;
     let itemSchema = schema.items;
     if (isFixedItems(schema) && allowAdditionalItems(schema)) {
       itemSchema = schema.additionalItems;
     }
-    this.asyncSetState({
-      items: items.concat([
-        getDefaultFormState(itemSchema, undefined, definitions)
-      ])
-    });
+    this.props.onChange([
+      ...formData,
+      getDefaultFormState(itemSchema, undefined, definitions)
+    ], {validate: false});
   };
 
   onDropIndexClick = (index) => {
@@ -216,9 +197,9 @@ class ArrayField extends Component {
       if (event) {
         event.preventDefault();
       }
-      this.asyncSetState({
-        items: this.state.items.filter((_, i) => i !== index)
-      }, {validate: true}); // refs #195
+      const {formData, onChange} = this.props;
+      // refs #195: revalidate to ensure properly reindexing errors
+      onChange(formData.filter((_, i) => i !== index), {validate: true});
     };
   };
 
@@ -228,33 +209,33 @@ class ArrayField extends Component {
         event.preventDefault();
         event.target.blur();
       }
-      const {items} = this.state;
-      this.asyncSetState({
-        items: items.map((item, i) => {
-          if (i === newIndex) {
-            return items[index];
-          } else if (i === index) {
-            return items[newIndex];
-          } else {
-            return item;
-          }
-        })
-      }, {validate: true});
+      const {formData, onChange} = this.props;
+      onChange(formData.map((item, i) => {
+        if (i === newIndex) {
+          return formData[index];
+        } else if (i === index) {
+          return formData[newIndex];
+        } else {
+          return item;
+        }
+      }), {validate: true});
     };
   };
 
   onChangeForIndex = (index) => {
     return (value) => {
-      this.asyncSetState({
-        items: this.state.items.map((item, i) => {
-          return index === i ? value : item;
-        })
-      });
+      const {formData, onChange} = this.props;
+      onChange(formData.map((item, i) => {
+        // We need to treat undefined items as nulls to have validation.
+        // See https://github.com/tdegrunt/jsonschema/issues/206
+        const jsonValue = typeof value === "undefined" ? null : value;
+        return index === i ? jsonValue : item;
+      }), {validate: false});
     };
   };
 
   onSelectChange = (value) => {
-    this.asyncSetState({items: value});
+    this.props.onChange(value, {validate: false});
   };
 
   render() {
@@ -275,6 +256,7 @@ class ArrayField extends Component {
     const {
       schema,
       uiSchema,
+      formData,
       errorSchema,
       idSchema,
       name,
@@ -283,10 +265,10 @@ class ArrayField extends Component {
       readonly,
       autofocus,
       registry,
+      formContext,
       onBlur
     } = this.props;
     const title = (schema.title === undefined) ? name : schema.title;
-    const {items = []} = this.state;
     const {ArrayFieldTemplate, definitions, fields} = registry;
     const {TitleField, DescriptionField} = fields;
     const itemsSchema = retrieveSchema(schema.items, definitions);
@@ -294,18 +276,18 @@ class ArrayField extends Component {
 
     const arrayProps = {
       canAdd: addable,
-      items: items.map((item, index) => {
+      items: formData.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,
+          canMoveDown: index < formData.length - 1,
           itemSchema: itemsSchema,
           itemIdSchema,
           itemErrorSchema,
-          itemData: items[index],
+          itemData: formData[index],
           itemUiSchema: uiSchema.items,
           autofocus: autofocus && index === 0,
           onBlur
@@ -320,7 +302,8 @@ class ArrayField extends Component {
       required,
       schema,
       title,
-      TitleField
+      TitleField,
+      formContext
     };
 
     // Check if a custom render function was passed in
@@ -330,8 +313,8 @@ class ArrayField extends Component {
 
   renderMultiSelect() {
     const {schema, idSchema, uiSchema, disabled, readonly, autofocus, onBlur} = this.props;
-    const {items} = this.state;
-    const {widgets, definitions} = this.props.registry;
+    const items = this.props.formData;
+    const {widgets, definitions, formContext} = this.props.registry;
     const itemsSchema = retrieveSchema(schema.items, definitions);
     const enumOptions = optionsList(itemsSchema);
     const {widget="select", ...options} = {...getUiOptions(uiSchema), enumOptions};
@@ -347,6 +330,7 @@ class ArrayField extends Component {
         value={items}
         disabled={disabled}
         readonly={readonly}
+        formContext={formContext}
         autofocus={autofocus}/>
     );
   }
@@ -354,8 +338,8 @@ class ArrayField extends Component {
   renderFiles() {
     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 items = this.props.formData;
+    const {widgets, formContext} = this.props.registry;
     const {widget="files", ...options} = getUiOptions(uiSchema);
     const Widget = getWidget(schema, widget, widgets);
     return (
@@ -370,6 +354,7 @@ class ArrayField extends Component {
         value={items}
         disabled={disabled}
         readonly={readonly}
+        formContext={formContext}
         autofocus={autofocus}/>
     );
   }
@@ -389,7 +374,7 @@ class ArrayField extends Component {
       onBlur
     } = this.props;
     const title = schema.title || name;
-    let {items} = this.state;
+    let items = this.props.formData;
     const {ArrayFieldTemplate, definitions, fields} = registry;
     const {TitleField} = fields;
     const itemSchemas = schema.items.map(item =>
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index 57ee2e82f..8a6c24328 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -1,7 +1,6 @@
 import React, {PropTypes} from "react";
 
 import {
-  defaultFieldValue,
   getWidget,
   getUiOptions,
   optionsList,
@@ -36,7 +35,7 @@ function BooleanField(props) {
     id={idSchema && idSchema.$id}
     onChange={onChange}
     label={title === undefined ? name : title}
-    value={defaultFieldValue(formData, schema)}
+    value={formData}
     required={required}
     disabled={disabled}
     readonly={readonly}
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 1512aa1db..97bb12966 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -1,35 +1,16 @@
 import React, {Component, PropTypes} from "react";
 
-import {deepEquals} from "../../utils";
-
-
 import {
-  getDefaultFormState,
   orderProperties,
   retrieveSchema,
   shouldRender,
-  getDefaultRegistry,
-  setState
+  getDefaultRegistry
 } from "../../utils";
 
-
-function objectKeysHaveChanged(formData, state) {
-  // for performance, first check for lengths
-  const newKeys = Object.keys(formData);
-  const oldKeys = Object.keys(state);
-  if (newKeys.length < oldKeys.length) {
-    return true;
-  }
-  // deep check on sorted keys
-  if (!deepEquals(newKeys.sort(), oldKeys.sort())) {
-    return true;
-  }
-  return false;
-}
-
 class ObjectField extends Component {
   static defaultProps = {
     uiSchema: {},
+    formData: {},
     errorSchema: {},
     idSchema: {},
     registry: getDefaultRegistry(),
@@ -38,29 +19,6 @@ class ObjectField extends Component {
     readonly: false,
   }
 
-  constructor(props) {
-    super(props);
-    this.state = this.getStateFromProps(props);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    const state = this.getStateFromProps(nextProps);
-    const {formData} = nextProps;
-    if (formData && objectKeysHaveChanged(formData, this.state)) {
-      // We *need* to replace state entirely here has we have received formData
-      // holding different keys (so with some removed).
-      this.state = state;
-      this.forceUpdate();
-    } else {
-      this.setState(state);
-    }
-  }
-
-  getStateFromProps(props) {
-    const {schema, formData, registry} = props;
-    return getDefaultFormState(schema, formData, registry.definitions) || {};
-  }
-
   shouldComponentUpdate(nextProps, nextState) {
     return shouldRender(this, nextProps, nextState);
   }
@@ -71,21 +29,17 @@ class ObjectField extends Component {
       schema.required.indexOf(name) !== -1;
   }
 
-  asyncSetState(state, options={validate: false}) {
-    setState(this, state, () => {
-      this.props.onChange(this.state, options);
-    });
-  }
-
   onPropertyChange = (name) => {
     return (value, options) => {
-      this.asyncSetState({[name]: value}, options);
+      const newFormData = {...this.props.formData, [name]: value};
+      this.props.onChange(newFormData, options);
     };
   };
 
   render() {
     const {
       uiSchema,
+      formData,
       errorSchema,
       idSchema,
       name,
@@ -135,7 +89,7 @@ class ObjectField extends Component {
               uiSchema={uiSchema[name]}
               errorSchema={errorSchema[name]}
               idSchema={idSchema[name]}
-              formData={this.state[name]}
+              formData={formData[name]}
               onChange={this.onPropertyChange(name)}
               onBlur={onBlur}
               registry={this.props.registry}
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 14aab87ad..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}
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index 98df908c9..ab6248261 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -1,7 +1,6 @@
 import React, {PropTypes} from "react";
 
 import {
-  defaultFieldValue,
   getWidget,
   getUiOptions,
   optionsList,
@@ -36,7 +35,7 @@ function StringField(props) {
     schema={schema}
     id={idSchema && idSchema.$id}
     label={title === undefined ? name : title}
-    value={defaultFieldValue(formData, schema)}
+    value={formData}
     onChange={onChange}
     onBlur={onBlur}
     required={required}
diff --git a/src/components/widgets/AltDateWidget.js b/src/components/widgets/AltDateWidget.js
index ddbcfa90c..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)});
   }
@@ -24,7 +24,8 @@ 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}
@@ -56,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));
diff --git a/src/components/widgets/BaseInput.js b/src/components/widgets/BaseInput.js
index d07ba5021..5a09ea907 100644
--- a/src/components/widgets/BaseInput.js
+++ b/src/components/widgets/BaseInput.js
@@ -24,7 +24,7 @@ function BaseInput(props) {
       className="form-control"
       readOnly={readonly}
       autoFocus={autofocus}
-      value={typeof value === "undefined" ? "" : value}
+      value={value == null ? "" : value}
       onChange={_onChange}
       onBlur={onBlur && (event => onBlur(inputProps.id, event.target.value))}/>
   );
diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js
index 717bcd4e8..86a88b27b 100644
--- a/src/components/widgets/RadioWidget.js
+++ b/src/components/widgets/RadioWidget.js
@@ -25,6 +25,7 @@ function RadioWidget({
             <input type="radio"
               checked={checked}
               name={name}
+              required={required}
               value={option.value}
               disabled={disabled}
               autoFocus={autofocus && i === 0}
diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js
index 41cc8ca66..0595e5dff 100644
--- a/src/components/widgets/SelectWidget.js
+++ b/src/components/widgets/SelectWidget.js
@@ -7,7 +7,9 @@ import {asNumber} from "../../utils";
  * 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";
@@ -38,15 +40,17 @@ function SelectWidget({
   multiple,
   autofocus,
   onChange,
-  onBlur
+  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}
@@ -58,11 +62,12 @@ function SelectWidget({
       onChange={(event) => {
         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>
   );
 }
 
diff --git a/src/components/widgets/TextareaWidget.js b/src/components/widgets/TextareaWidget.js
index 70aa3d9f4..7c9aa200e 100644
--- a/src/components/widgets/TextareaWidget.js
+++ b/src/components/widgets/TextareaWidget.js
@@ -4,6 +4,7 @@ import React, {PropTypes} from "react";
 function TextareaWidget({
   schema,
   id,
+  options,
   placeholder,
   value,
   required,
@@ -15,7 +16,7 @@ function TextareaWidget({
 }) {
   const _onChange = ({target: {value}}) => {
     return onChange(value === "" ? undefined : value);
-  };  
+  };
   return (
     <textarea
       id={id}
@@ -26,13 +27,15 @@ function TextareaWidget({
       disabled={disabled}
       readOnly={readonly}
       autoFocus={autofocus}
+      rows={options.rows}
       onBlur={onBlur && (event => onBlur(id, event.target.value))}
-      onChange={_onChange} />
+      onChange={_onChange}/>
   );
 }
 
 TextareaWidget.defaultProps = {
-  autofocus: false
+  autofocus: false,
+  options: {}
 };
 
 if (process.env.NODE_ENV !== "production") {
@@ -40,6 +43,9 @@ if (process.env.NODE_ENV !== "production") {
     schema: PropTypes.object.isRequired,
     id: PropTypes.string.isRequired,
     placeholder: PropTypes.string,
+    options: PropTypes.shape({
+      rows: PropTypes.number
+    }),
     value: PropTypes.string,
     required: PropTypes.bool,
     autofocus: PropTypes.bool,
diff --git a/src/utils.js b/src/utils.js
index 96a94db26..74797b7c8 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -64,10 +64,6 @@ export function getDefaultRegistry() {
   return defaultRegistry;
 }
 
-export function defaultFieldValue(formData, schema) {
-  return typeof formData === "undefined" ? schema.default : formData;
-}
-
 export function getWidget(schema, widget, registeredWidgets={}) {
   const {type} = schema;
 
@@ -116,9 +112,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 +393,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") {


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index c71b49547..5d67e6029 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -111,6 +111,15 @@ describe("ArrayField", () => {
         .to.have.length.of(1);
     });
 
+    it("should mark a non-null array item widget as required", () => {
+      const {node} = createFormComponent({schema});
+
+      Simulate.click(node.querySelector(".array-item-add button"));
+
+      expect(node.querySelector(".field-string input[type=text]").required)
+        .eql(true);
+    });
+
     it("should fill an array field with data", () => {
       const {node} = createFormComponent({schema, formData: ["foo", "bar"]});
       const inputs = node.querySelectorAll(".field-string input[type=text]");
@@ -229,6 +238,26 @@ describe("ArrayField", () => {
         .to.have.length.of(0);
     });
 
+    it("should handle cleared field values in the array", () => {
+      const schema = {
+        type: "array",
+        items: {type: "integer"},
+      };
+      const formData = [1, 2, 3];
+      const {comp, node} = createFormComponent({
+        liveValidate: true,
+        schema,
+        formData,
+      });
+
+      Simulate.change(node.querySelector("#root_1"), {
+        target: {value: ""}
+      });
+
+      expect(comp.state.formData).eql([1, null, 3]);
+      expect(comp.state.errors).to.have.length.of(1);
+    });
+
     it("should render the input widgets with the expected ids", () => {
       const {node} = createFormComponent({schema, formData: ["foo", "bar"]});
 
@@ -590,6 +619,16 @@ describe("ArrayField", () => {
       expect(numInput.id).eql("root_1");
     });
 
+    it("should mark non-null item widgets as required", () => {
+      const {node} = createFormComponent({schema});
+      const strInput =
+          node.querySelector("fieldset .field-string input[type=text]");
+      const numInput =
+          node.querySelector("fieldset .field-number input[type=text]");
+      expect(strInput.required).eql(true);
+      expect(numInput.required).eql(true);
+    });
+
     it("should fill fields with data", () => {
       const {node} = createFormComponent({schema, formData: ["foo", 42]});
       const strInput =
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 10bb712c7..d3f8d35e8 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,24 @@ describe("Form", () => {
       const {node} = createFormComponent({schema});
 
       expect(node.querySelectorAll("option"))
-        .to.have.length.of(2);
+        .to.have.length.of(3);
+    });
+  });
+
+  describe("Default value handling on clear", () => {
+    const schema = {
+      type: "string",
+      default: "foo",
+    };
+
+    it("should not set default when a text field is cleared", () => {
+      const {node} = createFormComponent({schema, formData: "bar"});
+
+      Simulate.change(node.querySelector("input"), {
+        target: {value: ""}
+      });
+
+      expect(node.querySelector("input").value).eql("");
     });
   });
 
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 1dac26040..60e1569cd 100644
--- a/test/StringField_test.js
+++ b/test/StringField_test.js
@@ -89,7 +89,7 @@ describe("StringField", () => {
 
       expect(onBlur.calledWith(input.id, "yo")).to.be.true;
     });
-    
+
     it("should handle an empty string change event", () => {
       const {comp, node} = createFormComponent({
         schema: {type: "string"},
@@ -158,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",
@@ -181,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",
@@ -194,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",
@@ -243,6 +294,19 @@ describe("StringField", () => {
 
       expect(comp.state.formData).eql(undefined);
     });
+
+    it("should render a textarea field with rows", () => {
+      const {node} = createFormComponent({
+        schema: {type: "string"},
+        uiSchema: {
+          "ui:widget": "textarea",
+          "ui:options": {rows: 20}
+        },
+        formData: "x",
+      });
+
+      expect(node.querySelector("textarea").getAttribute("rows")).eql("20");
+    });
   });
 
   describe("DateTimeWidget", () => {
@@ -550,7 +614,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"]);
     });
 
@@ -734,7 +798,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 481698217..bb4f5d827 100644
--- a/test/performance_test.js
+++ b/test/performance_test.js
@@ -7,7 +7,8 @@ import ObjectField from "../src/components/fields/ObjectField";
 import {
   createComponent,
   createFormComponent,
-  createSandbox
+  createSandbox,
+  setProps
 } from "./test_utils";
 
 
@@ -56,33 +57,35 @@ describe("Rendering performance optimizations", () => {
     const registry = getDefaultRegistry();
 
     it("should not render if next props are equivalent", () => {
-      const {comp} = createComponent(ArrayField, {
+      const props = {
         registry,
         schema,
         uiSchema,
         onChange,
         onBlur
-      });
+      };
+
+      const {comp} = createComponent(ArrayField, props);
       sandbox.stub(comp, "render").returns(<div/>);
 
-      comp.componentWillReceiveProps({schema});
+      setProps(comp, props);
 
       sinon.assert.notCalled(comp.render);
     });
 
     it("should not render if next formData are equivalent", () => {
-      const formData = ["a", "b"];
-
-      const {comp} = createComponent(ArrayField, {
+      const props = {
         registry,
         schema,
-        formData,
+        formData: ["a", "b"],
         onChange,
         onBlur
-      });
+      };
+
+      const {comp} = createComponent(ArrayField, props);
       sandbox.stub(comp, "render").returns(<div/>);
 
-      comp.componentWillReceiveProps({schema, formData});
+      setProps(comp, {...props, formData: ["a", "b"]});
 
       sinon.assert.notCalled(comp.render);
     });
@@ -102,35 +105,37 @@ describe("Rendering performance optimizations", () => {
     const idSchema = {$id: "root", foo: {$id: "root_plop"}};
 
     it("should not render if next props are equivalent", () => {
-      const {comp} = createComponent(ObjectField, {
+      const props = {
         registry,
         schema,
         uiSchema,
         onChange,
         idSchema,
         onBlur
-      });
+      };
+
+      const {comp} = createComponent(ObjectField, props);
       sandbox.stub(comp, "render").returns(<div/>);
 
-      comp.componentWillReceiveProps({schema, registry});
+      setProps(comp, props);
 
       sinon.assert.notCalled(comp.render);
     });
 
     it("should not render if next formData are equivalent", () => {
-      const formData = {foo: "blah"};
-
-      const {comp} = createComponent(ObjectField, {
+      const props = {
         registry,
         schema,
-        formData,
+        formData: {foo: "blah"},
         onChange,
         idSchema,
         onBlur
-      });
+      };
+
+      const {comp} = createComponent(ObjectField, props);
       sandbox.stub(comp, "render").returns(<div/>);
 
-      comp.componentWillReceiveProps({schema, formData, registry});
+      setProps(comp, {...props, formData: {foo: "blah"}});
 
       sinon.assert.notCalled(comp.render);
     });
diff --git a/test/test_utils.js b/test/test_utils.js
index c2ab0efa4..2a03e5d65 100644
--- a/test/test_utils.js
+++ b/test/test_utils.js
@@ -3,7 +3,7 @@
 import React from "react";
 import sinon from "sinon";
 import {renderIntoDocument} from "react-addons-test-utils";
-import {findDOMNode} from "react-dom";
+import {findDOMNode, render} from "react-dom";
 
 import Form from "../src";
 
@@ -26,3 +26,13 @@ export function createSandbox() {
   });
   return sandbox;
 }
+
+export function setProps(comp, newProps) {
+  const node = findDOMNode(comp);
+  render(
+    React.createElement(
+      comp.constructor,
+      newProps
+    ),
+    node.parentNode);
+}
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",