Repository: mozilla-services/react-jsonschema-form
Pull request #5 (2015-12-22 10:52:55)

Added support for uiSchema.
WiP

Natim: LGTM thanks :+1: 

Patch:
diff --git a/README.md b/README.md
index 2e6866cbe..ce3ee57e3 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,7 @@ react-jsonschema-form
 
 [![Build Status](https://travis-ci.org/mozilla-services/react-jsonschema-form.svg)](https://travis-ci.org/mozilla-services/react-jsonschema-form)
 
-A simple [React](http://facebook.github.io/react/) component capable of building
-HTML forms out of a [JSON schema](http://jsonschema.net/).
+A simple [React](http://facebook.github.io/react/) component capable of building HTML forms out of a [JSON schema](http://jsonschema.net/).
 
 Requires React 0.14+.
 
@@ -60,15 +59,46 @@ render((
 ), document.getElementById("app"));
 ```
 
+### Alternative widgets
+
+JSONSchema is limited for describing how a given data type should be rendered as an input component, that's why this lib introduces the concept of *UI schema*. A UI schema is basically an object literal describing which UI widget should be used to render a certain field
+
+Example:
+
+```js
+const uiSchema =  {
+  done: {
+    widget: "radio" // could also be "select"
+  }
+};
+
+render((
+  <Form schema={schema}
+        uiSchema={uiSchema}
+        formData={formData} />
+), document.getElementById("app"));
+```
+
+Here's a list of supported alternative widgets for different JSONSchema data types:
+
+#### `boolean`
+
+  * `radio`: a radio button group with `true` and `false` as selectable values;
+  * `select`: a select box with `true` and `false` as options;
+  * by default, a checkbox is used
+
+#### `string`:
+
+  * `textarea`: a `textarea` element
+  * by default, a regular `input[type=text]` element is used.
+
 ## Development server
 
 ```
 $ npm start
 ```
 
-A [Cosmos development server](https://github.com/skidding/cosmos) showcasing
-components with hot reload enabled is available at
-[localhost:8080](http://localhost:8080).
+A [Cosmos development server](https://github.com/skidding/cosmos) showcasing components with hot reload enabled is available at [localhost:8080](http://localhost:8080).
 
 ## Tests
 
diff --git a/fixtures/Form/nested-default.js b/fixtures/Form/nested-default.js
new file mode 100644
index 000000000..cac7a9056
--- /dev/null
+++ b/fixtures/Form/nested-default.js
@@ -0,0 +1,46 @@
+module.exports = {
+  schema: {
+    title: "Todo Tasks",
+    description: "Tasks collection.",
+    type: "object",
+    additionalProperties: false,
+    required: [
+      "title", "tasks"
+    ],
+    default: {
+      title: "Default task list",
+      tasks: [
+        {title: "A default task", done: true},
+        {title: "Another default task", done: false},
+      ]
+    },
+    properties: {
+      title: {
+        type: "string",
+        title: "Tasks list title",
+      },
+      tasks: {
+        type: "array",
+        title: "Tasks list",
+        items: {
+          type: "object",
+          properties: {
+            done: {
+              type: "boolean",
+              title: "Done?",
+              description: "Is that task done already?"
+            },
+            title: {
+              type: "string",
+              title: "Title",
+              description: "The task title.",
+              minLength: 1
+            }
+          }
+        }
+      }
+    }
+  },
+  onSubmit: console.log.bind(console, "submit"),
+  onError: console.log.bind(console, "errors")
+};
diff --git a/fixtures/Form/nested-uiSchema.js b/fixtures/Form/nested-uiSchema.js
new file mode 100644
index 000000000..751d7a957
--- /dev/null
+++ b/fixtures/Form/nested-uiSchema.js
@@ -0,0 +1,62 @@
+module.exports = {
+  schema: {
+    title: "Todo Tasks",
+    description: "Tasks collection.",
+    type: "object",
+    additionalProperties: false,
+    required: [
+      "title", "tasks"
+    ],
+    properties: {
+      title: {
+        type: "string",
+        title: "Tasks list title",
+      },
+      tasks: {
+        type: "array",
+        title: "Tasks list",
+        items: {
+          type: "object",
+          properties: {
+            type: {
+              type: "string",
+              title: "Category",
+              enum: ["coding", "sleeping"]
+            },
+            done: {
+              type: "boolean",
+              title: "Done?",
+              description: "Is that task done already?"
+            },
+            title: {
+              type: "string",
+              title: "Title",
+              description: "The task title.",
+              minLength: 1
+            }
+          }
+        }
+      }
+    }
+  },
+  uiSchema: {
+    title: {
+      widget: "textarea",
+    },
+    tasks: {
+      items: {
+        type: {
+          widget: "radio"
+        },
+        done: {
+          widget: "radio",
+        },
+        title: {
+          widget: "textarea"
+        }
+      }
+    }
+  },
+  onSubmit: console.log.bind(console, "submit"),
+  onError: console.log.bind(console, "errors")
+};
diff --git a/fixtures/fields/ArrayField/index.js b/fixtures/fields/ArrayField/index.js
index 15496683c..c3cf57505 100644
--- a/fixtures/fields/ArrayField/index.js
+++ b/fixtures/fields/ArrayField/index.js
@@ -7,5 +7,5 @@ module.exports = {
       title: "item"
     }
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/ArrayField/with-default.js b/fixtures/fields/ArrayField/with-default.js
index 1c99f0122..4d9444509 100644
--- a/fixtures/fields/ArrayField/with-default.js
+++ b/fixtures/fields/ArrayField/with-default.js
@@ -8,5 +8,5 @@ module.exports = {
       title: "item"
     }
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/ArrayField/with-formData.js b/fixtures/fields/ArrayField/with-formData.js
index ea2724492..887d12882 100644
--- a/fixtures/fields/ArrayField/with-formData.js
+++ b/fixtures/fields/ArrayField/with-formData.js
@@ -8,5 +8,5 @@ module.exports = {
     }
   },
   formData: ["item1", "item2"],
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/ArrayField/with-uiSchema.js b/fixtures/fields/ArrayField/with-uiSchema.js
new file mode 100644
index 000000000..4dad01aff
--- /dev/null
+++ b/fixtures/fields/ArrayField/with-uiSchema.js
@@ -0,0 +1,17 @@
+module.exports = {
+  schema: {
+    type: "array",
+    title: "title",
+    items: {
+      type: "string",
+      title: "item"
+    }
+  },
+  uiSchema: {
+    items: {
+      widget: "textarea"
+    }
+  },
+  formData: ["item1", "item2"],
+  onChange: console.log.bind(console, "change")
+};
diff --git a/fixtures/fields/BooleanField/index.js b/fixtures/fields/BooleanField/index.js
index ee7573750..10391b91d 100644
--- a/fixtures/fields/BooleanField/index.js
+++ b/fixtures/fields/BooleanField/index.js
@@ -3,5 +3,5 @@ module.exports = {
     type: "boolean",
     title: "My boolean",
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/BooleanField/with-default.js b/fixtures/fields/BooleanField/with-default.js
index d1015f9eb..45c57842b 100644
--- a/fixtures/fields/BooleanField/with-default.js
+++ b/fixtures/fields/BooleanField/with-default.js
@@ -4,5 +4,5 @@ module.exports = {
     title: "My boolean",
     default: true,
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/BooleanField/with-formData.js b/fixtures/fields/BooleanField/with-formData.js
index 31c0b4dfa..ddcb4983d 100644
--- a/fixtures/fields/BooleanField/with-formData.js
+++ b/fixtures/fields/BooleanField/with-formData.js
@@ -4,5 +4,5 @@ module.exports = {
     title: "My boolean",
   },
   formData: true,
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/BooleanField/with-uiSchema.js b/fixtures/fields/BooleanField/with-uiSchema.js
new file mode 100644
index 000000000..e298ab95f
--- /dev/null
+++ b/fixtures/fields/BooleanField/with-uiSchema.js
@@ -0,0 +1,11 @@
+module.exports = {
+  schema: {
+    type: "boolean",
+    title: "My boolean"
+  },
+  uiSchema: {
+    widget: "radio"
+  },
+  formData: false,
+  onChange: console.log.bind(console, "change")
+};
diff --git a/fixtures/fields/ObjectField/index.js b/fixtures/fields/ObjectField/index.js
index e75f4162c..719d502e6 100644
--- a/fixtures/fields/ObjectField/index.js
+++ b/fixtures/fields/ObjectField/index.js
@@ -13,5 +13,5 @@ module.exports = {
       }
     }
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/ObjectField/with-default.js b/fixtures/fields/ObjectField/with-default.js
index 86aff0442..54d425553 100644
--- a/fixtures/fields/ObjectField/with-default.js
+++ b/fixtures/fields/ObjectField/with-default.js
@@ -17,5 +17,5 @@ module.exports = {
       }
     }
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/ObjectField/with-formData.js b/fixtures/fields/ObjectField/with-formData.js
index 4784e56d5..041ba3c07 100644
--- a/fixtures/fields/ObjectField/with-formData.js
+++ b/fixtures/fields/ObjectField/with-formData.js
@@ -17,5 +17,5 @@ module.exports = {
     string: "an existing string",
     bool: true
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/ObjectField/with-uiSchema.js b/fixtures/fields/ObjectField/with-uiSchema.js
new file mode 100644
index 000000000..15f5b4e2c
--- /dev/null
+++ b/fixtures/fields/ObjectField/with-uiSchema.js
@@ -0,0 +1,25 @@
+module.exports = {
+  schema: {
+    type: "object",
+    title: "object title",
+    properties: {
+      string: {
+        type: "string",
+        title: "string"
+      },
+      bool: {
+        type: "boolean",
+        title: "bool"
+      }
+    }
+  },
+  uiSchema: {
+    string: {
+      widget: "textarea"
+    },
+    bool: {
+      widget: "select"
+    }
+  },
+  onChange: console.log.bind(console, "change")
+};
diff --git a/fixtures/fields/SchemaField/array.js b/fixtures/fields/SchemaField/array.js
index 15496683c..c3cf57505 100644
--- a/fixtures/fields/SchemaField/array.js
+++ b/fixtures/fields/SchemaField/array.js
@@ -7,5 +7,5 @@ module.exports = {
       title: "item"
     }
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/SchemaField/boolean.js b/fixtures/fields/SchemaField/boolean.js
index ee7573750..10391b91d 100644
--- a/fixtures/fields/SchemaField/boolean.js
+++ b/fixtures/fields/SchemaField/boolean.js
@@ -3,5 +3,5 @@ module.exports = {
     type: "boolean",
     title: "My boolean",
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/SchemaField/object.js b/fixtures/fields/SchemaField/object.js
index e75f4162c..719d502e6 100644
--- a/fixtures/fields/SchemaField/object.js
+++ b/fixtures/fields/SchemaField/object.js
@@ -13,5 +13,5 @@ module.exports = {
       }
     }
   },
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/fields/StringField/with-uiSchema.js b/fixtures/fields/StringField/with-uiSchema.js
new file mode 100644
index 000000000..9a3850943
--- /dev/null
+++ b/fixtures/fields/StringField/with-uiSchema.js
@@ -0,0 +1,10 @@
+module.exports = {
+  schema: {
+    type: "string",
+    title: "string",
+  },
+  uiSchema: {
+    widget: "textarea"
+  },
+  formData: "an existing string"
+};
diff --git a/fixtures/widgets/CheckboxWidget/index.js b/fixtures/widgets/CheckboxWidget/index.js
index 23e67eb31..fd11d42c9 100644
--- a/fixtures/widgets/CheckboxWidget/index.js
+++ b/fixtures/widgets/CheckboxWidget/index.js
@@ -1,5 +1,5 @@
 module.exports = {
   type: "boolean",
   label: "foo",
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/widgets/RadioWidget/index.js b/fixtures/widgets/RadioWidget/index.js
index 8f7c30eb3..100750f40 100644
--- a/fixtures/widgets/RadioWidget/index.js
+++ b/fixtures/widgets/RadioWidget/index.js
@@ -3,5 +3,5 @@ module.exports = {
   label: "foo",
   defaultValue: "b",
   options: ["a", "b"],
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/widgets/SelectWidget/index.js b/fixtures/widgets/SelectWidget/index.js
index d3c2524be..8febb4be2 100644
--- a/fixtures/widgets/SelectWidget/index.js
+++ b/fixtures/widgets/SelectWidget/index.js
@@ -2,5 +2,5 @@ module.exports = {
   type: "string",
   label: "foo",
   options: ["foo", "bar", "baz"],
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/fixtures/widgets/TextWidget/index.js b/fixtures/widgets/TextWidget/index.js
index 59cefd5ee..a6f36e5ae 100644
--- a/fixtures/widgets/TextWidget/index.js
+++ b/fixtures/widgets/TextWidget/index.js
@@ -2,5 +2,5 @@ module.exports = {
   type: "string",
   label: "foo",
   value: "plop",
-  onChange: console.log.bind(console)
+  onChange: console.log.bind(console, "change")
 };
diff --git a/src/components/Form.js b/src/components/Form.js
index ff899bd4b..44ed3dbf1 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -1,11 +1,24 @@
-import React from "react";
+import React, { Component, PropTypes } from "react";
 import { Validator } from "jsonschema";
 
 import SchemaField from "./fields/SchemaField";
 import ErrorList from "./ErrorList";
 
 
-export default class Form extends React.Component {
+export default class Form extends Component {
+  static propTypes = {
+    schema: PropTypes.object.isRequired,
+    uiSchema: PropTypes.object,
+    formData: PropTypes.any,
+    onChange: PropTypes.func,
+    onError: PropTypes.func,
+    onSubmit: PropTypes.func,
+  }
+
+  static defaultProps = {
+    uiSchema: {}
+  }
+
   constructor(props) {
     super(props);
     const edit = !!props.formData;
@@ -63,13 +76,14 @@ export default class Form extends React.Component {
   }
 
   render() {
-    const {schema} = this.props;
+    const {schema, uiSchema} = this.props;
     const {formData} = this.state;
     return (
       <form className="generic-form" onSubmit={this.onSubmit.bind(this)}>
         {this.renderErrors()}
         <SchemaField
           schema={schema}
+          uiSchema={uiSchema}
           formData={formData}
           onChange={this.onChange.bind(this)} />
         <p><button type="submit">Submit</button></p>
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index da7d54cd0..a3541bd5a 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -7,10 +7,15 @@ import SchemaField from "./SchemaField";
 export default class ArrayField extends Component {
   static propTypes = {
     schema: PropTypes.object.isRequired,
+    uiSchema: PropTypes.object,
     onChange: PropTypes.func.isRequired,
     formData: PropTypes.array,
   }
 
+  static defaultProps = {
+    uiSchema: {}
+  };
+
   constructor(props) {
     super(props);
     const formData = Array.isArray(props.formData) ? props.formData : null;
@@ -61,7 +66,7 @@ export default class ArrayField extends Component {
   }
 
   render() {
-    const {schema} = this.props;
+    const {schema, uiSchema} = this.props;
     const {items} = this.state;
     return (
       <fieldset
@@ -71,7 +76,9 @@ export default class ArrayField extends Component {
         <div className="array-item-list">{
           items.map((item, index) => {
             return <div key={index}>
-              <SchemaField schema={schema.items}
+              <SchemaField
+                schema={schema.items}
+                uiSchema={uiSchema.items}
                 formData={items[index]}
                 required={this.isItemRequired(schema.items)}
                 onChange={this.onChange.bind(this, index)} />
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index 9f1d71d50..e239b7304 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -1,10 +1,11 @@
 import React, { PropTypes } from "react";
 
-import { defaultFieldValue } from "../../utils";
+import { defaultFieldValue, getAlternativeWidget } from "../../utils";
 import CheckboxField from "./../widgets/CheckboxWidget";
 
-function BooleanField({schema, formData, required, onChange}) {
+function BooleanField({schema, uiSchema, formData, required, onChange}) {
   const {title, description} = schema;
+  const {widget} = uiSchema;
   const commonProps = {
     type: schema.type,
     onChange,
@@ -14,15 +15,23 @@ function BooleanField({schema, formData, required, onChange}) {
     value: defaultFieldValue(formData, schema),
     required,
   };
-  // XXX handle uiSchema.widget here
+  if (widget) {
+    const Widget = getAlternativeWidget(widget);
+    return <Widget options={[true, false]} {... commonProps} />;
+  }
   return <CheckboxField {...commonProps} />;
 }
 
 BooleanField.propTypes = {
   schema: PropTypes.object.isRequired,
+  uiSchema: PropTypes.object,
   onChange: PropTypes.func.isRequired,
   formData: PropTypes.bool,
   required: PropTypes.bool,
 };
 
+BooleanField.defaultProps = {
+  uiSchema: {}
+};
+
 export default BooleanField;
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index f0812fc33..4d79b4e2a 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -6,11 +6,16 @@ import SchemaField from "./SchemaField";
 export default class ObjectField extends Component {
   static propTypes = {
     schema: PropTypes.object.isRequired,
+    uiSchema: PropTypes.object,
     onChange: PropTypes.func.isRequired,
     formData: PropTypes.object,
     required: PropTypes.bool,
   }
 
+  static defaultProps = {
+    uiSchema: {}
+  }
+
   constructor(props) {
     super(props);
     this.state = props.formData || props.schema.default || {};
@@ -32,18 +37,21 @@ export default class ObjectField extends Component {
   }
 
   render() {
-    const {schema} = this.props;
-    return <fieldset>
-      <legend>{schema.title || "Object"}</legend>
-      {
-      Object.keys(schema.properties).map((name, index) => {
-        return <SchemaField key={index}
-          name={name}
-          required={this.isRequired(name)}
-          schema={schema.properties[name]}
-          formData={this.state[name]}
-          onChange={this.onChange.bind(this, name)} />;
-      })
-    }</fieldset>;
+    const {schema, uiSchema} = this.props;
+    return (
+      <fieldset>
+        <legend>{schema.title || "Object"}</legend>
+        {
+        Object.keys(schema.properties).map((name, index) => {
+          return <SchemaField key={index}
+            name={name}
+            required={this.isRequired(name)}
+            schema={schema.properties[name]}
+            uiSchema={uiSchema[name]}
+            formData={this.state[name]}
+            onChange={this.onChange.bind(this, name)} />;
+        })
+      }</fieldset>
+    );
   }
 }
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 1f32a22f5..7320d484b 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -22,7 +22,8 @@ function SchemaField(props) {
 }
 
 SchemaField.propTypes = {
-  schema: PropTypes.object.isRequired
+  schema: PropTypes.object.isRequired,
+  uiSchema: PropTypes.object,
 };
 
 export default SchemaField;
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index 201beec96..a139194ae 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -1,12 +1,13 @@
 import React, { PropTypes } from "react";
 
-import { defaultFieldValue } from "../../utils";
+import { defaultFieldValue, getAlternativeWidget } from "../../utils";
 import TextWidget from "./../widgets/TextWidget";
 import SelectWidget from "./../widgets/SelectWidget";
 
 
-function StringField({schema, formData, required, onChange}) {
+function StringField({schema, uiSchema, formData, required, onChange}) {
   const {type, title, description} = schema;
+  const {widget} = uiSchema;
   const commonProps = {
     type: type,
     label: title,
@@ -17,10 +18,16 @@ function StringField({schema, formData, required, onChange}) {
     defaultValue: schema.default,
   };
   if (Array.isArray(schema.enum)) {
-    // XXX uiSchema: Could also be a list of radio buttons
+    if (widget) {
+      const Widget = getAlternativeWidget(widget);
+      return <Widget options={schema.enum} {...commonProps} />;
+    }
     return <SelectWidget options={schema.enum} {...commonProps} />;
   }
-  // XXX uiSchema: Could also be a textarea for longer texts
+  if (widget) {
+    const Widget = getAlternativeWidget(widget);
+    return <Widget {...commonProps} />;
+  }
   return <TextWidget {...commonProps} />;
 }
 
@@ -31,4 +38,8 @@ StringField.propTypes = {
   required: PropTypes.bool,
 };
 
+StringField.defaultProps = {
+  uiSchema: {}
+};
+
 export default StringField;
diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js
index 1f865ef7d..9fe0d2114 100644
--- a/src/components/widgets/RadioWidget.js
+++ b/src/components/widgets/RadioWidget.js
@@ -19,10 +19,10 @@ function RadioWidget({
     <Wrapper type={type} label={label}>
       {
         options.map((option, i) => {
-          const checked = value ? option === value :
-                                  option === defaultValue;
+          const checked = value !== undefined ? option === value :
+                                                option === defaultValue;
           return (
-            <div key={i}>
+            <div className="field-radio-group" key={i}>
               <label>
                 <input type="radio"
                   name={name}
@@ -30,7 +30,7 @@ function RadioWidget({
                   checked={checked}
                   placeholder={placeholder}
                   onChange={_ => onChange(option)} />
-                {option}
+                {String(option)}
               </label>
             </div>
           );
diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js
index dcbaa7c40..0810530c7 100644
--- a/src/components/widgets/SelectWidget.js
+++ b/src/components/widgets/SelectWidget.js
@@ -3,6 +3,19 @@ import React, { PropTypes } from "react";
 import Wrapper from "./../widgets/Wrapper";
 
 
+/**
+ * This is a silly limitation in the DOM where option change event values are
+ * always retrieved as strings.
+ */
+function processValue(type, value) {
+  if (type === "boolean") {
+    return value === "true";
+  } else if (type === "number") {
+    return value === Number(value);
+  }
+  return value;
+}
+
 function SelectWidget({
   type,
   options,
@@ -19,9 +32,9 @@ function SelectWidget({
         title={placeholder}
         value={value}
         defaultValue={defaultValue}
-        onChange={(event) => onChange(event.target.value)}>{
+        onChange={(event) => onChange(processValue(type, event.target.value))}>{
         options.map((option, i) => {
-          return <option key={i}>{option}</option>;
+          return <option key={i} value={option}>{String(option)}</option>;
         })
       }</select>
     </Wrapper>
diff --git a/src/components/widgets/TextareaWidget.js b/src/components/widgets/TextareaWidget.js
new file mode 100644
index 000000000..3b119c1be
--- /dev/null
+++ b/src/components/widgets/TextareaWidget.js
@@ -0,0 +1,37 @@
+import React, { PropTypes } from "react";
+
+import Wrapper from "./../widgets/Wrapper";
+
+
+function TextWidget({
+  type,
+  label,
+  placeholder,
+  value,
+  defaultValue,
+  required,
+  onChange
+}) {
+  return (
+    <Wrapper label={label} required={required} type={type}>
+      <textarea
+        value={value}
+        defaultValue={defaultValue}
+        placeholder={placeholder}
+        required={required}
+        onChange={(event) => onChange(event.target.value)} />
+    </Wrapper>
+  );
+}
+
+TextWidget.propTypes = {
+  type: PropTypes.string.isRequired,
+  label: PropTypes.string,
+  placeholder: PropTypes.string,
+  value: PropTypes.string,
+  defaultValue: PropTypes.string,
+  required: PropTypes.bool,
+  onChange: PropTypes.func,
+};
+
+export default TextWidget;
diff --git a/src/utils.js b/src/utils.js
index db9d925b9..32fbf7f59 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,3 +1,7 @@
+import RadioWidget from "./components/widgets/RadioWidget";
+import SelectWidget from "./components/widgets/SelectWidget";
+import TextareaWidget from "./components/widgets/TextareaWidget";
+
 export function defaultTypeValue(type) {
   switch (type) {
   case "array":     return [];
@@ -13,3 +17,12 @@ export function defaultTypeValue(type) {
 export function defaultFieldValue(formData, schema) {
   return formData === null ? defaultTypeValue(schema.type) : formData;
 }
+
+export function getAlternativeWidget(name) {
+  switch(name) {
+  case "radio": return RadioWidget;
+  case "select": return SelectWidget;
+  case "textarea": return TextareaWidget;
+  default: throw new Error(`No alternative widget for "${name}"`);
+  }
+}


Test patch:
diff --git a/test/index_test.js b/test/index_test.js
index 69de868af..772f5fdd7 100644
--- a/test/index_test.js
+++ b/test/index_test.js
@@ -1,3 +1,5 @@
+/*eslint no-unused-vars: [2, { "varsIgnorePattern": "^d$" }]*/
+
 import { expect } from "chai";
 import sinon from "sinon";
 import React from "react";
@@ -12,9 +14,9 @@ function createComponent(props) {
   return {comp, node};
 }
 
-// function d(node) {
-//   console.log(node.outerHTML);
-// }
+function d(node) {
+  console.log(node.outerHTML);
+}
 
 describe("Form", () => {
   var sandbox;
@@ -380,6 +382,224 @@ describe("Form", () => {
     });
   });
 
+  describe("uiSchema", () => {
+    describe("string", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {
+            type: "string",
+          }
+        }
+      };
+
+      describe("textarea", () => {
+        const uiSchema = {
+          foo: {
+            widget: "textarea"
+          }
+        };
+
+        it("should accept a uiSchema object", () => {
+          const {node} = createComponent({schema, uiSchema});
+
+          expect(node.querySelectorAll("textarea"))
+            .to.have.length.of(1);
+        });
+
+        it("should support formData", () => {
+          const {node} = createComponent({schema, uiSchema, formData: {
+            foo: "a"
+          }});
+
+          expect(node.querySelector("textarea").value)
+            .eql("a");
+        });
+
+        it("should update state when text is updated is checked", () => {
+          const {comp, node} = createComponent({schema, uiSchema, formData: {
+            foo: "a"
+          }});
+
+          Simulate.change(node.querySelector("textarea"), {
+            target: {value: "b"}
+          });
+
+          expect(comp.state.formData).eql({foo: "b"});
+        });
+      });
+    });
+
+    describe("string (enum)", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {
+            type: "string",
+            enum: ["a", "b"],
+          }
+        }
+      };
+
+      describe("radio", () => {
+        const uiSchema = {
+          foo: {
+            widget: "radio"
+          }
+        };
+
+        it("should accept a uiSchema object", () => {
+          const {node} = createComponent({schema, uiSchema});
+
+          expect(node.querySelectorAll("[type=radio]"))
+            .to.have.length.of(2);
+        });
+
+        it("should support formData", () => {
+          const {node} = createComponent({schema, uiSchema, formData: {
+            foo: "b"
+          }});
+
+          expect(node.querySelector("[type=radio][value=b]").checked)
+            .eql(true);
+        });
+
+        it("should update state when value is updated", () => {
+          const {comp, node} = createComponent({schema, uiSchema, formData: {
+            foo: "a"
+          }});
+
+          Simulate.change(node.querySelector("[type=radio][value=b]"), {
+            target: {checked: true}
+          });
+
+          expect(comp.state.formData).eql({foo: "b"});
+        });
+      });
+    });
+
+    describe("boolean", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {
+            type: "boolean",
+          }
+        }
+      };
+
+      describe("radio", () => {
+        const uiSchema = {
+          foo: {
+            widget: "radio"
+          }
+        };
+
+        it("should accept a uiSchema object", () => {
+          const {node} = createComponent({schema, uiSchema});
+
+          expect(node.querySelectorAll("[type=radio]"))
+            .to.have.length.of(2);
+          expect(node.querySelector("[type=radio][value=true]"))
+            .to.be.truthy;
+          expect(node.querySelector("[type=radio][value=false]"))
+            .to.be.truthy;
+        });
+
+        it("should render boolean option labels", () => {
+          const {node} = createComponent({schema, uiSchema});
+          const labels = [].map.call(node.querySelectorAll("label span"),
+                                     node => node.textContent);
+          expect(labels)
+            .eql(["true", "false"]);
+        });
+
+        it("should support formData", () => {
+          const {node} = createComponent({schema, uiSchema, formData: {
+            foo: false
+          }});
+
+          expect(node.querySelector("[type=radio][value=false]").checked)
+            .eql(true);
+        });
+
+        it("should update state when false is checked", () => {
+          const {comp, node} = createComponent({schema, uiSchema, formData: {
+            foo: true
+          }});
+
+          Simulate.change(node.querySelector("[type=radio][value=false]"), {
+            target: {checked: true}
+          });
+
+          expect(comp.state.formData).eql({foo: false});
+        });
+
+        it("should update state when true is checked", () => {
+          const {comp, node} = createComponent({schema, uiSchema, formData: {
+            foo: false
+          }});
+
+          Simulate.change(node.querySelector("[type=radio][value=true]"), {
+            target: {checked: true}
+          });
+
+          expect(comp.state.formData).eql({foo: true});
+        });
+      });
+
+      describe("select", () => {
+        const uiSchema = {
+          foo: {
+            widget: "select"
+          }
+        };
+
+        it("should accept a uiSchema object", () => {
+          const {node} = createComponent({schema, uiSchema});
+
+          expect(node.querySelectorAll("select option"))
+            .to.have.length.of(2);
+        });
+
+        it("should render boolean option labels", () => {
+          const {node} = createComponent({schema, uiSchema});
+
+          expect(node.querySelectorAll("option")[0].textContent)
+            .eql("true");
+          expect(node.querySelectorAll("option")[1].textContent)
+            .eql("false");
+        });
+
+        it("should update state when true is selected", () => {
+          const {comp, node} = createComponent({schema, uiSchema, formData: {
+            foo: false
+          }});
+
+          Simulate.change(node.querySelector("select"), {
+            // DOM option change events always return strings
+            target: {value: "true"}
+          });
+
+          expect(comp.state.formData).eql({foo: true});
+        });
+
+        it("should update state when false is selected", () => {
+          const {comp, node} = createComponent({schema, uiSchema, formData: {
+            foo: false
+          }});
+
+          Simulate.change(node.querySelector("select"), {
+            // DOM option change events always return strings
+            target: {value: "false"}
+          });
+
+          expect(comp.state.formData).eql({foo: false});
+        });
+      });
+    });
+  });
+
   describe("Validation", () => {
     describe("Required fields", () => {
       const schema = {