Repository: mozilla-services/react-jsonschema-form
Pull request #371 (2016-11-02 13:25:12)

Allow overriding the default fields/widgets
### Reasons for making this change

This pull request is conflicting with #363 and #366. There is some heated discussion about the technical details in the comments of those pull requests. I'm offering this pull request because I had the code changes lying around anyway (as I have mentioned in #310). My intention is not to step on anyones toes - I'm merely offering a solution or at least some insight to the issue.

The reason why I have sitted on this for so long time is because I was waiting for input on the issues I have raised (#354, #310).

This approach has the benefit that we aren't introducing any new techniques or new API designs to solve the issue. All fields and widgets are simply overridable through the `fields` and `widgets` props.

I didn't add any tests or update the documentation since this pull request is conflicting.

### 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
* [ ] **I'm adding a new feature**
  - [ ] I've updated the playground with an example use of the feature



Patch:
diff --git a/.eslintrc b/.eslintrc
index da6f88f79..f24ae972b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,7 +15,8 @@
         "comma-dangle": [0],
         "no-unused-vars": [2, {"vars": "all", "args": "none"}],
         "no-console": [0],
-        "object-curly-spacing": [1, "never"]
+        "object-curly-spacing": [1, "never"],
+        "keyword-spacing": ["error"]
     },
     "env": {
         "es6": true,
diff --git a/README.md b/README.md
index 21c2ba5e6..ecc15ab47 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,12 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
         - [Hidden widgets](#hidden-widgets)
         - [File widgets](#file-widgets)
            - [Multiple files](#multiple-files)
+           - [File widget input ref](#file-widget-input-ref)
      - [Object fields ordering](#object-fields-ordering)
-     - [Array items ordering](#array-items-ordering)
+     - [Array item options](#array-item-options)
+        - [Orderable option](#orderable-option)
+        - [Addable option](#addable-option)
+        - [Removable option](#removable-option)
      - [Custom CSS class names](#custom-css-class-names)
      - [Custom labels for enum fields](#custom-labels-for-enum-fields)
      - [Multiple choices list](#multiple-choices-list)
@@ -53,7 +57,9 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
         - [Field props](#field-props)
         - [The registry object](#the-registry-object)
         - [The formContext object](#the-formcontext-object)
+     - [Custom array field buttons](#custom-array-field-buttons)
      - [Custom SchemaField](#custom-schemafield)
+     - [Customizing the fields and widgets](#customizing-the-default-fields-and-widgets)
      - [Custom titles](#custom-titles)
      - [Custom descriptions](#custom-descriptions)
   - [Form data validation](#form-data-validation)
@@ -150,6 +156,8 @@ render((
 ), document.getElementById("app"));
 ```
 
+NOTE: If your form have a single field, pass a single value to `formData`. ex: `formData='Charlie'`
+
 WARNING: If you have situations where your parent component can re-render, make sure you listen to the `onChange` event and update the data you pass to the `formData` attribute.
 
 ### Form event handlers
@@ -388,7 +396,7 @@ This allows you to programmatically trigger the browser's file selector which ca
 
 ### Object fields ordering
 
-The `uiSchema` object spec also allows you to define in which order a given object field properties should be rendered using the `ui:order` property:
+Since the order of object properties in Javascript and JSON is not guaranteed, the `uiSchema` object spec allows you to define the order in which properties are rendered using the `ui:order` property:
 
 ```jsx
 const schema = {
@@ -408,7 +416,18 @@ render((
         uiSchema={uiSchema} />
 ), document.getElementById("app"));
 ```
-### Array items ordering
+
+If a guarenteed fixed order is only important for some fields, you can insert a wildcard `"*"` item in your `ui:order` definition. All fields that are not referenced explicitly anywhere in the list will be rendered at that point:
+
+```js
+const uiSchema = {
+  "ui:order": ["bar", "*"]
+};
+```
+
+### Array item options
+
+#### `orderable` option
 
 Array items are orderable by default, and react-jsonschema-form renders move up/down buttons alongside them. The `uiSchema` object spec allows you to disable ordering:
 
@@ -427,6 +446,30 @@ const uiSchema = {
 };
 ```
 
+#### `addable` option
+
+If either `items` or `additionalItems` contains a schema object, an add button for new items is shown by default. You can turn this off with the `addable` option in `uiSchema`:
+
+```jsx
+const uiSchema = {
+  "ui:options":  {
+    addable: false
+  }
+};
+```
+
+#### `removable` option
+
+A remove button is shown by default for an item if `items` contains a schema object, or the item is an `additionalItems` instance. You can turn this off with the `removable` option in `uiSchema`:
+
+```jsx
+const uiSchema = {
+  "ui:options":  {
+    removable: false
+  }
+};
+```
+
 ### Custom CSS class names
 
 The uiSchema object accepts a `classNames` property for each field of the schema:
@@ -861,6 +904,19 @@ The registry is passed down the component tree, so you can access it from your c
 
 You can provide a `formContext` object to the Form, which is passed down to all fields and widgets (including [TitleField](#custom-titles) and [DescriptionField](#custom-descriptions)). Useful for implementing context aware fields and widgets.
 
+### 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:
+
+```css
+.btn-plus > i {
+  display: none;
+}
+.btn-plus::after {
+  content: "Add";
+}
+```
+
 ### Custom SchemaField
 
 **Warning:** This is a powerful feature as you can override the whole form behavior and easily mess it up. Handle with care.
@@ -897,6 +953,49 @@ If you're curious how this could ever be useful, have a look at the [Kinto formb
 
 Props passed to a custom SchemaField are the same as [the ones passed to a custom field](#field-props).
 
+### Customizing the default fields and widgets
+
+You can override any default field and widget, including the internal widgets like the `CheckboxWidget` that `ObjectField` renders for boolean values. You can override any field and widget just by providing the customized fields/widgets in the `fields` and `widgets` props:
+
+```jsx
+
+const CustomCheckbox = function(props) {
+  return (
+    <button id="custom" className={props.value ? "checked" : "unchecked"} onClick={props.onChange(!props.value)}>
+    	{props.value}
+    </button>
+  );
+};
+
+const widgets = {
+  CheckboxWidget: CustomCheckbox
+};
+
+render((
+  <Form schema={schema}
+        uiSchema={uiSchema}
+        formData={formData}
+        widgets={widgets} />
+), document.getElementById("app"));
+```
+
+This allows you to create a reusable customized form class with your custom fields and widgets:
+
+```jsx
+const customFields = {StringField: CustomString};
+const customWidgets = {CheckboxWidget: CustomCheckbox};
+
+function MyForm(props) {
+  return <Form fields={customFields} widgets={customWidgets} {...props} />;
+}
+
+render((
+  <MyForm schema={schema}
+    uiSchema={uiSchema}
+    formData={formData} />
+), document.getElementById("app"));
+```
+
 ### Custom titles
 
 You can provide your own implementation of the `TitleField` base React component for rendering any title. This is useful when you want to augment how titles are handled.
diff --git a/playground/samples/arrays.js b/playground/samples/arrays.js
index 30cf64f57..7e4baf79a 100644
--- a/playground/samples/arrays.js
+++ b/playground/samples/arrays.js
@@ -49,6 +49,51 @@ module.exports = {
             default: "lorem ipsum"
           }
         }
+      },
+      unorderable: {
+        title: "Unorderable items",
+        type: "array",
+        items: {
+          type: "string",
+          default: "lorem ipsum"
+        }
+      },
+      unremovable: {
+        title: "Unremovable items",
+        type: "array",
+        items: {
+          type: "string",
+          default: "lorem ipsum"
+        }
+      },
+      noToolbar: {
+        title: "No add, remove and order buttons",
+        type: "array",
+        items: {
+          type: "string",
+          default: "lorem ipsum"
+        }
+      },
+      fixedNoToolbar: {
+        title: "Fixed array without buttons",
+        type: "array",
+        items: [
+          {
+            title: "A number",
+            type: "number",
+            default: 42
+          },
+          {
+            title: "A boolean",
+            type: "boolean",
+            default: false
+          }
+        ],
+        additionalItems: {
+          title: "A string",
+          type: "string",
+          default: "lorem ipsum"
+        }
       }
     }
   },
@@ -64,12 +109,40 @@ module.exports = {
       additionalItems: {
         "ui:widget": "updown"
       }
+    },
+    unorderable: {
+      "ui:options": {
+        orderable: false
+      }
+    },
+    unremovable: {
+      "ui:options": {
+        removable: false
+      }
+    },
+    noToolbar: {
+      "ui:options": {
+        addable: false,
+        orderable: false,
+        removable: false
+      }
+    },
+    fixedNoToolbar: {
+      "ui:options": {
+        addable: false,
+        orderable: false,
+        removable: false
+      }
     }
   },
   formData: {
     listOfStrings: ["foo", "bar"],
     multipleChoicesList: ["foo", "bar"],
     fixedItemsList: ["Some text", true, 123],
-    nestedList: [["lorem", "ipsum"], ["dolor"]]
+    nestedList: [["lorem", "ipsum"], ["dolor"]],
+    unorderable: ["one", "two"],
+    unremovable: ["one", "two"],
+    noToolbar: ["one", "two"],
+    fixedNoToolbar: [42, true, "additional item one", "additional item two"]
   }
 };
diff --git a/playground/samples/ordering.js b/playground/samples/ordering.js
index 179db688b..75ef995a2 100644
--- a/playground/samples/ordering.js
+++ b/playground/samples/ordering.js
@@ -27,7 +27,7 @@ module.exports = {
     }
   },
   uiSchema: {
-    "ui:order": ["firstName", "lastName", "age", "bio", "password"],
+    "ui:order": ["firstName", "lastName", "*", "password"],
     age: {
       "ui:widget": "updown"
     },
diff --git a/src/components/Form.js b/src/components/Form.js
index 50254b3ec..32819cc75 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -1,15 +1,12 @@
 import React, {Component, PropTypes} from "react";
 
-import SchemaField from "./fields/SchemaField";
-import TitleField from "./fields/TitleField";
-import DescriptionField from "./fields/DescriptionField";
-
 import ErrorList from "./ErrorList";
 import {
   getDefaultFormState,
   shouldRender,
   toIdSchema,
   setState,
+  getDefaultRegistry,
 } from "../utils";
 import validateFormData from "../validate";
 
@@ -118,19 +115,11 @@ export default class Form extends Component {
   getRegistry() {
     // For BC, accept passed SchemaField and TitleField props and pass them to
     // the "fields" registry one.
-    const _SchemaField = this.props.SchemaField || SchemaField;
-    const _TitleField = this.props.TitleField || TitleField;
-    const _DescriptionField = this.props.DescriptionField || DescriptionField;
-
-    const fields = Object.assign({
-      SchemaField: _SchemaField,
-      TitleField: _TitleField,
-      DescriptionField: _DescriptionField,
-    }, this.props.fields);
+    const {fields, widgets} = getDefaultRegistry();
     return {
-      fields,
+      fields: {...fields, ...this.props.fields},
+      widgets: {...widgets, ...this.props.widgets},
       FieldTemplate: this.props.FieldTemplate,
-      widgets: this.props.widgets || {},
       definitions: this.props.schema.definitions || {},
       formContext: this.props.formContext || {},
     };
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 76df64376..2052218d0 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -14,8 +14,6 @@ import {
   getDefaultRegistry,
   setState
 } from "../../utils";
-import FileWidget from "./../widgets/FileWidget";
-
 
 function ArrayFieldTitle({TitleField, idSchema, title, required}) {
   if (!title) {
@@ -35,6 +33,15 @@ function ArrayFieldDescription({DescriptionField, idSchema, description}) {
   return <DescriptionField id={id} description={description}/>;
 }
 
+function IconBtn(props) {
+  const {type="default", icon, className, ...otherProps} = props;
+  return (
+    <button type="button" className={`btn btn-${type} ${className}`} {...otherProps}>
+      <i className={`glyphicon glyphicon-${icon}`}/>
+    </button>
+  );
+}
+
 class ArrayField extends Component {
   static defaultProps = {
     uiSchema: {},
@@ -171,6 +178,10 @@ class ArrayField extends Component {
     const {definitions, fields} = this.props.registry;
     const {TitleField, DescriptionField} = fields;
     const itemsSchema = retrieveSchema(schema.items, definitions);
+    const {addable} = {
+      addable: true,
+      ...uiSchema["ui:options"]
+    };
 
     return (
       <fieldset
@@ -203,8 +214,9 @@ class ArrayField extends Component {
             });
           })
         }</div>
-        <AddButton
-          onClick={this.onAddClick} disabled={disabled || readonly}/>
+        {addable ? <AddButton
+                     onClick={this.onAddClick}
+                     disabled={disabled || readonly}/> : null}
       </fieldset>
     );
   }
@@ -237,6 +249,7 @@ class ArrayField extends Component {
     const {schema, idSchema, name, disabled, readonly, autofocus} = this.props;
     const title = schema.title || name;
     const {items} = this.state;
+    const {FileWidget} = this.props.registry.widgets;
     return (
       <FileWidget
         id={idSchema && idSchema.$id}
@@ -271,6 +284,11 @@ class ArrayField extends Component {
       retrieveSchema(item, definitions));
     const additionalSchema = allowAdditionalItems(schema) ?
       retrieveSchema(schema.additionalItems, definitions) : null;
+    const {addable} = {
+      addable: true,
+      ...uiSchema["ui:options"]
+    };
+    const canAdd = addable && additionalSchema;
 
     if (!items || items.length < itemSchemas.length) {
       // to make sure at least all fixed items are generated
@@ -302,7 +320,7 @@ class ArrayField extends Component {
 
             return this.renderArrayFieldItem({
               index,
-              removable: additional,
+              canRemove: additional,
               canMoveUp: index >= itemSchemas.length + 1,
               canMoveDown: additional && index < items.length - 1,
               itemSchema,
@@ -315,7 +333,7 @@ class ArrayField extends Component {
           })
         }</div>
         {
-          additionalSchema ? <AddButton
+          canAdd ? <AddButton
                                onClick={this.onAddClick}
                                disabled={disabled || readonly}/> : null
         }
@@ -325,7 +343,7 @@ class ArrayField extends Component {
 
   renderArrayFieldItem({
     index,
-    removable=true,
+    canRemove=true,
     canMoveUp=true,
     canMoveDown=true,
     itemSchema,
@@ -337,19 +355,22 @@ class ArrayField extends Component {
   }) {
     const {SchemaField} = this.props.registry.fields;
     const {disabled, readonly, uiSchema} = this.props;
-
-    const {orderable} = {orderable: true, ...uiSchema["ui:options"]};
-
-    const _canMoveUp = orderable && canMoveUp;
-    const _canMoveDown = orderable && canMoveDown;
-
-    const hasToolbar = removable || _canMoveUp || _canMoveDown;
-
+    const {orderable, removable} = {
+      orderable: true,
+      removable: true,
+      ...uiSchema["ui:options"]
+    };
+    const has = {
+      moveUp: orderable && canMoveUp,
+      moveDown: orderable && canMoveDown,
+      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={hasToolbar ? "col-xs-10" : "col-xs-12"}>
+        <div className={has.toolbar ? "col-xs-9" : "col-xs-12"}>
           <SchemaField
             schema={itemSchema}
             uiSchema={itemUiSchema}
@@ -364,29 +385,29 @@ class ArrayField extends Component {
             autofocus={autofocus}/>
         </div>
         {
-          hasToolbar ?
-            <div className="col-xs-2 array-item-toolbox text-right">
-              <div className="btn-group" style={{display: "flex"}}>
-                {_canMoveUp || _canMoveDown ?
-                  <button type="button" className="btn btn-default array-item-move-up"
-                          style={btnStyle}
+          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"
-                          disabled={disabled || readonly || !_canMoveUp}
-                          onClick={this.onReorderClick(index, index - 1)}>⬆</button>
-                  : null}
-                {_canMoveUp || _canMoveDown ?
-                  <button type="button" className="btn btn-default array-item-move-down"
                           style={btnStyle}
-                          tabIndex="-1"
-                          disabled={disabled || readonly || !_canMoveDown}
-                          onClick={this.onReorderClick(index, index + 1)}>⬇</button>
+                          disabled={disabled || readonly || !has.moveUp}
+                          onClick={this.onReorderClick(index, index - 1)}/>
                   : null}
-                {removable ?
-                  <button type="button" className="btn btn-danger array-item-remove"
+                {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)}>✖</button>
+                          onClick={this.onDropIndexClick(index)}/>
                   : null}
               </div>
             </div>
@@ -400,10 +421,10 @@ class ArrayField extends Component {
 function AddButton({onClick, disabled}) {
   return (
     <div className="row">
-      <p className="col-xs-2 col-xs-offset-10 array-item-add text-right">
-        <button type="button" className="btn btn-info col-xs-12"
-                tabIndex="-1" onClick={onClick}
-                disabled={disabled} style={{fontWeight: "bold"}}>➕</button>
+      <p className="col-xs-3 col-xs-offset-9 array-item-add text-right">
+        <IconBtn type="info" icon="plus" className="btn-add col-xs-12"
+                 tabIndex="-1" onClick={onClick}
+                 disabled={disabled}/>
       </p>
     </div>
   );
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index d5a0d59ec..1f6afb1a3 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -7,7 +7,6 @@ import {
   getDefaultRegistry,
   isObject,
 } from "../../utils";
-import CheckboxWidget from "./../widgets/CheckboxWidget";
 
 
 function buildOptions(schema, uiWidget) {
@@ -58,6 +57,7 @@ function BooleanField(props) {
     const Widget = getAlternativeWidget(schema, widget, widgets);
     return <Widget options={buildOptions(schema, uiSchema["ui:widget"])} {...commonProps}/>;
   }
+  const {CheckboxWidget} = registry.widgets;
   return <CheckboxWidget {...commonProps}/>;
 }
 
diff --git a/src/components/fields/NumberField.js b/src/components/fields/NumberField.js
index 819bba2c2..783391682 100644
--- a/src/components/fields/NumberField.js
+++ b/src/components/fields/NumberField.js
@@ -1,9 +1,9 @@
 import React, {PropTypes} from "react";
 
 import {asNumber} from "../../utils";
-import StringField from "./StringField";
 
 function NumberField(props) {
+  const {StringField} = props.registry.fields;
   return (
     <StringField {...props}
       onChange={(value) => props.onChange(asNumber(value))}/>
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 9aa4d1e65..17e3d506b 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -101,7 +101,7 @@ class ObjectField extends Component {
     try {
       const properties = Object.keys(schema.properties);
       orderedProperties = orderProperties(properties, uiSchema["ui:order"]);
-    } catch(err) {
+    } catch (err) {
       return (
         <div>
           <p className="config-error" style={{color: "red"}}>
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 878e87418..165e519dc 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -6,21 +6,16 @@ import {
   getDefaultRegistry,
   isFilesArray
 } from "../../utils";
-import ArrayField from "./ArrayField";
-import BooleanField from "./BooleanField";
-import NumberField from "./NumberField";
-import ObjectField from "./ObjectField";
-import StringField from "./StringField";
 import UnsupportedField from "./UnsupportedField";
 
 const REQUIRED_FIELD_SYMBOL = "*";
 const COMPONENT_TYPES = {
-  array:   ArrayField,
-  boolean: BooleanField,
-  integer: NumberField,
-  number:  NumberField,
-  object:  ObjectField,
-  string:  StringField,
+  array:   "ArrayField",
+  boolean: "BooleanField",
+  integer: "NumberField",
+  number:  "NumberField",
+  object:  "ObjectField",
+  string:  "StringField",
 };
 
 function getFieldComponent(schema, uiSchema, fields) {
@@ -31,7 +26,8 @@ function getFieldComponent(schema, uiSchema, fields) {
   if (typeof field === "string" && field in fields) {
     return fields[field];
   }
-  return COMPONENT_TYPES[schema.type] || UnsupportedField;
+  const componentName = COMPONENT_TYPES[schema.type];
+  return componentName in fields ? fields[componentName] : UnsupportedField;
 }
 
 function Label(props) {
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index 9bfb4c836..c777228dc 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -6,8 +6,6 @@ import {
   optionsList,
   getDefaultRegistry
 } from "../../utils";
-import TextWidget from "../widgets/TextWidget";
-import SelectWidget from "../widgets/SelectWidget";
 
 
 function StringField(props) {
@@ -39,7 +37,11 @@ function StringField(props) {
     readonly,
     formContext,
     autofocus,
+    registry,
   };
+
+  const {TextWidget, SelectWidget} = widgets;
+
   if (Array.isArray(schema.enum)) {
     const enumOptions = optionsList(schema);
     if (widget) {
diff --git a/src/components/widgets/AltDateTimeWidget.js b/src/components/widgets/AltDateTimeWidget.js
index 4137e54c7..d05e2da57 100644
--- a/src/components/widgets/AltDateTimeWidget.js
+++ b/src/components/widgets/AltDateTimeWidget.js
@@ -1,9 +1,8 @@
 import React, {PropTypes} from "react";
 
-import AltDateWidget from "./AltDateWidget";
-
 
 function AltDateTimeWidget(props) {
+  const {AltDateWidget} = props.registry.widgets;
   return <AltDateWidget time {...props}/>;
 }
 
diff --git a/src/components/widgets/AltDateWidget.js b/src/components/widgets/AltDateWidget.js
index 8f6efc710..e228b7132 100644
--- a/src/components/widgets/AltDateWidget.js
+++ b/src/components/widgets/AltDateWidget.js
@@ -1,7 +1,6 @@
 import React, {Component, PropTypes} from "react";
 
 import {shouldRender, parseDateString, toDateString, pad} from "../../utils";
-import SelectWidget from "../widgets/SelectWidget";
 
 
 function rangeOptions(type, start, stop) {
@@ -17,8 +16,9 @@ function readyForChange(state) {
 }
 
 function DateElement(props) {
-  const {type, range, value, select, rootId, disabled, readonly, autofocus} = props;
+  const {type, range, value, select, rootId, disabled, readonly, autofocus, registry} = props;
   const id = rootId + "_" + type;
+  const {SelectWidget} = registry.widgets;
   return (
     <SelectWidget
       schema={{type: "integer"}}
@@ -101,7 +101,7 @@ class AltDateWidget extends Component {
   }
 
   render() {
-    const {id, disabled, readonly, autofocus} = this.props;
+    const {id, disabled, readonly, autofocus, registry} = this.props;
     return (
       <ul className="list-inline">{
         this.dateElementProps.map((elemProps, i) => (
@@ -112,6 +112,7 @@ class AltDateWidget extends Component {
               {...elemProps}
               disabled= {disabled}
               readonly={readonly}
+              registry={registry}
               autofocus={autofocus && i === 0}/>
           </li>
         ))
diff --git a/src/components/widgets/BaseInput.js b/src/components/widgets/BaseInput.js
index 9700c3523..4710a0c6e 100644
--- a/src/components/widgets/BaseInput.js
+++ b/src/components/widgets/BaseInput.js
@@ -12,6 +12,7 @@ function BaseInput(props) {
     options,  // eslint-disable-line
     schema,   // eslint-disable-line
     formContext,  // eslint-disable-line
+    registry, // eslint-disable-line
     ...inputProps
   } = props;
   return (
diff --git a/src/utils.js b/src/utils.js
index 30c4b3e90..9973f68d6 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,89 +1,95 @@
 import "setimmediate";
 
-import TitleField from "./components/fields/TitleField";
-import DescriptionField from "./components/fields/DescriptionField";
-import PasswordWidget from "./components/widgets/PasswordWidget";
-import RadioWidget from "./components/widgets/RadioWidget";
-import UpDownWidget from "./components/widgets/UpDownWidget";
-import RangeWidget from "./components/widgets/RangeWidget";
-import SelectWidget from "./components/widgets/SelectWidget";
-import TextWidget from "./components/widgets/TextWidget";
-import DateWidget from "./components/widgets/DateWidget";
-import DateTimeWidget from "./components/widgets/DateTimeWidget";
-import AltDateWidget from "./components/widgets/AltDateWidget";
-import AltDateTimeWidget from "./components/widgets/AltDateTimeWidget";
-import EmailWidget from "./components/widgets/EmailWidget";
-import URLWidget from "./components/widgets/URLWidget";
-import TextareaWidget from "./components/widgets/TextareaWidget";
-import HiddenWidget from "./components/widgets/HiddenWidget";
-import ColorWidget from "./components/widgets/ColorWidget";
-import FileWidget from "./components/widgets/FileWidget";
-import CheckboxesWidget from "./components/widgets/CheckboxesWidget";
-
-
 
 const altWidgetMap = {
   boolean: {
-    radio: RadioWidget,
-    select: SelectWidget,
-    hidden: HiddenWidget,
+    radio: "RadioWidget",
+    select: "SelectWidget",
+    hidden: "HiddenWidget",
   },
   string: {
-    password: PasswordWidget,
-    radio: RadioWidget,
-    select: SelectWidget,
-    textarea: TextareaWidget,
-    hidden: HiddenWidget,
-    date: DateWidget,
-    datetime: DateTimeWidget,
-    "alt-date": AltDateWidget,
-    "alt-datetime": AltDateTimeWidget,
-    color: ColorWidget,
-    file: FileWidget,
+    password: "PasswordWidget",
+    radio: "RadioWidget",
+    select: "SelectWidget",
+    textarea: "TextareaWidget",
+    hidden: "HiddenWidget",
+    date: "DateWidget",
+    datetime: "DateTimeWidget",
+    "alt-date": "AltDateWidget",
+    "alt-datetime": "AltDateTimeWidget",
+    color: "ColorWidget",
+    file: "FileWidget",
   },
   number: {
-    updown: UpDownWidget,
-    range: RangeWidget,
-    radio: RadioWidget,
-    hidden: HiddenWidget,
+    updown: "UpDownWidget",
+    range: "RangeWidget",
+    radio: "RadioWidget",
+    hidden: "HiddenWidget",
   },
   integer: {
-    updown: UpDownWidget,
-    range: RangeWidget,
-    radio: RadioWidget,
-    hidden: HiddenWidget,
+    updown: "UpDownWidget",
+    range: "RangeWidget",
+    radio: "RadioWidget",
+    hidden: "HiddenWidget",
   },
   array: {
-    select: SelectWidget,
-    checkboxes: CheckboxesWidget,
+    select: "SelectWidget",
+    checkboxes: "CheckboxesWidget",
   }
 };
 
 const stringFormatWidgets = {
-  "date-time": DateTimeWidget,
-  "date": DateWidget,
-  "email": EmailWidget,
-  "hostname": TextWidget,
-  "ipv4": TextWidget,
-  "ipv6": TextWidget,
-  "uri": URLWidget,
-  "data-url": FileWidget,
+  "date-time": "DateTimeWidget",
+  "date": "DateWidget",
+  "email": "EmailWidget",
+  "hostname": "TextWidget",
+  "ipv4": "TextWidget",
+  "ipv6": "TextWidget",
+  "uri": "URLWidget",
+  "data-url": "FileWidget",
 };
 
 export function getDefaultRegistry() {
+  const load = (prefix, arr) => arr.reduce((obj, comp) => {
+    obj[comp] = require(`./components/${prefix}/${comp}`).default;
+    return obj;
+  }, {});
+
+  const fields = load("fields", [
+    "SchemaField",
+    "ArrayField",
+    "BooleanField",
+    "ObjectField",
+    "StringField",
+    "NumberField",
+    "TitleField",
+    "DescriptionField",
+  ]);
+
+  const widgets = load("widgets", [
+    "PasswordWidget",
+    "RadioWidget",
+    "UpDownWidget",
+    "RangeWidget",
+    "SelectWidget",
+    "TextWidget",
+    "DateWidget",
+    "DateTimeWidget",
+    "AltDateWidget",
+    "AltDateTimeWidget",
+    "EmailWidget",
+    "URLWidget",
+    "TextareaWidget",
+    "HiddenWidget",
+    "ColorWidget",
+    "FileWidget",
+    "CheckboxWidget",
+    "CheckboxesWidget",
+  ]);
+
   return {
-    fields: {
-      // Prevent a bug where SchemaField is undefined when imported via Babel.
-      // This seems to have been introduced when upgrading React from 0.14 to to
-      // 15.0, which now seems to prevent cyclic references of exported
-      // components.
-      // Investigation hint: getDefaultRegistry is called from within
-      // SchemaField itself.
-      SchemaField: require("./components/fields/SchemaField").default,
-      TitleField,
-      DescriptionField,
-    },
-    widgets: {},
+    fields,
+    widgets,
     definitions: {},
     formContext: {}
   };
@@ -137,12 +143,12 @@ export function getAlternativeWidget(
   }
 
   if (altWidgetMap[type].hasOwnProperty(widget)) {
-    const altWidget = altWidgetMap[type][widget];
+    const altWidget = registeredWidgets[altWidgetMap[type][widget]];
     return getAlternativeWidget(schema, altWidget, registeredWidgets, widgetOptions);
   }
 
   if (type === "string" && stringFormatWidgets.hasOwnProperty(format)) {
-    const stringFormatWidget = stringFormatWidgets[format];
+    const stringFormatWidget = registeredWidgets[stringFormatWidgets[format]];
     return getAlternativeWidget(schema, stringFormatWidget, registeredWidgets, widgetOptions);
   }
 
@@ -223,6 +229,9 @@ export function mergeObjects(obj1, obj2, concatArrays = false) {
 }
 
 export function asNumber(value) {
+  if (value === "") {
+    return undefined;
+  }
   if (/\.$/.test(value)) {
     // "3." can't really be considered a number even if it parses in js. The
     // user is most likely entering a float.
@@ -249,16 +258,35 @@ export function orderProperties(properties, order) {
   if (!Array.isArray(order)) {
     return properties;
   }
-  if (order.length !== properties.length) {
-    throw new Error(
-      "uiSchema order list length should match object properties length");
+
+  const arrayToHash = arr => arr.reduce((prev, curr) => {
+    prev[curr] = true;
+    return prev;
+  }, {});
+  const errorPropList = arr => arr.length > 1 ?
+    `properties '${arr.join("', '")}'` :
+    `property '${arr[0]}'`;
+  const propertyHash = arrayToHash(properties);
+  const orderHash = arrayToHash(order);
+  const extraneous = order.filter(prop => prop !== "*" && !propertyHash[prop]);
+  if (extraneous.length) {
+    throw new Error(`uiSchema order list contains extraneous ${errorPropList(extraneous)}`);
+  }
+  const rest = properties.filter(prop => !orderHash[prop]);
+  const restIndex = order.indexOf("*");
+  if (restIndex === -1) {
+    if (rest.length) {
+      throw new Error(`uiSchema order list does not contain ${errorPropList(rest)}`);
+    }
+    return order;
   }
-  const fingerprint = (arr) => [].slice.call(arr).sort().toString();
-  if (fingerprint(order) !== fingerprint(properties)) {
-    throw new Error(
-      "uiSchema order list does not match object properties list");
+  if (restIndex !== order.lastIndexOf("*")) {
+    throw new Error("uiSchema order list contains more than one wildcard item");
   }
-  return order;
+
+  const complete = [...order];
+  complete.splice(restIndex, 1, ...rest);
+  return complete;
 }
 
 export function isMultiSelect(schema) {
@@ -500,7 +528,7 @@ export function dataURItoBlob(dataURI) {
   // Built the Uint8Array Blob parameter from the base64 string.
   const binary = atob(splitted[1]);
   const array = [];
-  for(let i = 0; i < binary.length; i++) {
+  for (let i = 0; i < binary.length; i++) {
     array.push(binary.charCodeAt(i));
   }
   // Create the blob object


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index 636d5b11b..4f0aaac5e 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -8,6 +8,7 @@ import {createFormComponent, createSandbox} from "./test_utils";
 
 describe("ArrayField", () => {
   let sandbox;
+  const CustomComponent = () => <div id="custom"/>;
 
   beforeEach(() => {
     sandbox = createSandbox();
@@ -70,6 +71,17 @@ describe("ArrayField", () => {
         .to.eql("my description");
     });
 
+    it("should render a customized file widget", () => {
+      const {node} = createFormComponent({schema,
+        uiSchema: {
+          "ui:widget": "files"
+        },
+        widgets: {FileWidget: CustomComponent}
+      });
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
+
     it("should contain no field in the list by default", () => {
       const {node} = createFormComponent({schema});
 
@@ -84,6 +96,12 @@ describe("ArrayField", () => {
         .not.eql(null);
     });
 
+    it("should not have an add button if addable is false", () => {
+      const {node} = createFormComponent({schema, uiSchema: {"ui:options": {addable: false}}});
+
+      expect(node.querySelector(".array-item-add button")).to.be.null;
+    });
+
     it("should add a new field when clicking the add button", () => {
       const {node} = createFormComponent({schema});
 
@@ -157,7 +175,7 @@ describe("ArrayField", () => {
       expect(moveDownBtns[1].disabled).eql(true);
     });
 
-    it("should not show move up/down buttons is orderable is false", () => {
+    it("should not show move up/down buttons if orderable is false", () => {
       const {node} = createFormComponent({schema, formData: ["foo", "bar"], uiSchema: {"ui:options": {orderable: false}}});
       const moveUpBtns = node.querySelector(".array-item-move-up");
       const moveDownBtns = node.querySelector(".array-item-move-down");
@@ -177,6 +195,13 @@ describe("ArrayField", () => {
       expect(inputs[0].value).eql("bar");
     });
 
+    it("should not show remove button if removable is false", () => {
+      const {node} = createFormComponent({schema, formData: ["foo", "bar"], uiSchema: {"ui:options": {removable: false}}});
+      const dropBtn = node.querySelector(".array-item-remove");
+
+      expect(dropBtn).to.be.null;
+    });
+
     it("should force revalidation when a field is removed", () => {
       // refs #195
       const {node} = createFormComponent({
@@ -189,7 +214,7 @@ describe("ArrayField", () => {
 
       try {
         Simulate.submit(node);
-      } catch(e) {
+      } catch (e) {
         // Silencing error thrown as failure is expected here
       }
 
@@ -585,6 +610,21 @@ describe("ArrayField", () => {
       expect(addInput.value).eql("bar");
     });
 
+    it("should have an add button if additionalItems is an object", () => {
+      const {node} = createFormComponent({schema: schemaAdditional});
+      expect(node.querySelector(".array-item-add button")).not.to.be.null;
+    });
+
+    it("should not have an add button if additionalItems is not set", () => {
+      const {node} = createFormComponent({schema});
+      expect(node.querySelector(".array-item-add button")).to.be.null;
+    });
+
+    it("should not have an add button if addable is false", () => {
+      const {node} = createFormComponent({schema, uiSchema: {"ui:options": {addable: false}}});
+      expect(node.querySelector(".array-item-add button")).to.be.null;
+    });
+
     describe("operations for additional items", () => {
       const {comp, node} = createFormComponent({
         schema: schemaAdditional,
diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js
index 0ddd49d92..a7b75e583 100644
--- a/test/BooleanField_test.js
+++ b/test/BooleanField_test.js
@@ -8,6 +8,8 @@ import {createFormComponent, createSandbox} from "./test_utils";
 describe("BooleanField", () => {
   let sandbox;
 
+  const CustomWidget = () => <div id="custom"/>;
+
   beforeEach(() => {
     sandbox = createSandbox();
   });
@@ -132,6 +134,20 @@ describe("BooleanField", () => {
       .eql("root");
   });
 
+  it("should render customized checkbox", () => {
+    const {node} = createFormComponent({
+      schema: {
+        type: "boolean",
+      },
+      widgets: {
+        CheckboxWidget: CustomWidget
+      }
+    });
+
+    expect(node.querySelector("#custom"))
+      .to.exist;
+  });
+
   describe("Label", () => {
     const Widget = props => <div id={`label-${props.label}`}/>;
 
diff --git a/test/FormContext_test.js b/test/FormContext_test.js
index 5de449c92..10608ada3 100644
--- a/test/FormContext_test.js
+++ b/test/FormContext_test.js
@@ -60,7 +60,7 @@ describe("FormContext", () => {
 
   it("should be passed to TemplateField", () => {
     function CustomTemplateField({formContext}) {
-      return <div id={formContext.foo} />;
+      return <div id={formContext.foo}/>;
     }
 
     const {node} = createFormComponent({
diff --git a/test/Form_test.js b/test/Form_test.js
index 414b1c79a..be0c0ce02 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -634,7 +634,7 @@ describe("Form", () => {
           function submit(node) {
             try {
               Simulate.submit(node);
-            } catch(err) {
+            } catch (err) {
               // Validation is expected to fail and call console.error, which is
               // stubbed to actually throw in createSandbox().
             }
diff --git a/test/NumberField_test.js b/test/NumberField_test.js
index 42d26bb02..f7081bf76 100644
--- a/test/NumberField_test.js
+++ b/test/NumberField_test.js
@@ -1,3 +1,4 @@
+import React from "react";
 import {expect} from "chai";
 import {Simulate} from "react-addons-test-utils";
 
@@ -132,6 +133,22 @@ describe("NumberField", () => {
       expect(node.querySelector(".field input").value)
         .eql("2.000");
     });
+
+    it("should render customized StringField", () => {
+      const CustomStringField = () => <div id="custom"/>;
+
+      const {node} = createFormComponent({
+        schema: {
+          type: "number",
+        },
+        fields: {
+          StringField: CustomStringField
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("SelectWidget", () => {
diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js
index 4593a2043..40f3a88f7 100644
--- a/test/ObjectField_test.js
+++ b/test/ObjectField_test.js
@@ -66,7 +66,7 @@ describe("ObjectField", () => {
     it("should render a customized description", () => {
       const CustomDescriptionField = ({description}) => <div id="custom">{description}</div>;
 
-      const {node} = createFormComponent({schema, DescriptionField: CustomDescriptionField});
+      const {node} = createFormComponent({schema, fields: {DescriptionField: CustomDescriptionField}});
       expect(node.querySelector("fieldset > #custom").textContent)
       .to.eql("my description");
     });
@@ -146,36 +146,57 @@ describe("ObjectField", () => {
       type: "object",
       properties: {
         foo: {type: "string"},
-        bar: {type: "string"}
+        bar: {type: "string"},
+        baz: {type: "string"},
+        qux: {type: "string"}
       }
     };
 
     it("should use provided order", () => {
       const {node} = createFormComponent({schema, uiSchema: {
-        "ui:order": ["bar", "foo"]
+        "ui:order": ["baz", "qux", "bar", "foo"]
       }});
       const labels = [].map.call(
         node.querySelectorAll(".field > label"), l => l.textContent);
 
-      expect(labels).eql(["bar", "foo"]);
+      expect(labels).eql(["baz", "qux", "bar", "foo"]);
     });
 
-    it("should throw when order list length mismatches", () => {
+    it("should insert unordered properties at wildcard position", () => {
       const {node} = createFormComponent({schema, uiSchema: {
-        "ui:order": ["bar", "foo", "baz?"]
+        "ui:order": ["baz", "*", "foo"]
+      }});
+      const labels = [].map.call(
+        node.querySelectorAll(".field > label"), l => l.textContent);
+
+      expect(labels).eql(["baz", "bar", "qux", "foo"]);
+    });
+
+    it("should throw when order list contains an extraneous property", () => {
+      const {node} = createFormComponent({schema, uiSchema: {
+        "ui:order": ["baz", "qux", "bar", "wut?", "foo", "huh?"]
       }});
 
       expect(node.querySelector(".config-error").textContent)
-        .to.match(/should match object properties length/);
+        .to.match(/contains extraneous properties 'wut\?', 'huh\?'/);
     });
 
-    it("should throw when order and properties lists differs", () => {
+    it("should throw when order list misses an existing property", () => {
       const {node} = createFormComponent({schema, uiSchema: {
-        "ui:order": ["bar", "wut?"]
+        "ui:order": ["baz", "bar"]
       }});
 
       expect(node.querySelector(".config-error").textContent)
-        .to.match(/does not match object properties list/);
+        .to.match(/does not contain properties 'foo', 'qux'/);
+    });
+
+    it("should throw when more than one wildcard is present", () => {
+      const {node} = createFormComponent({schema, uiSchema: {
+        "ui:order": ["baz", "*", "bar", "*"]
+      }});
+
+      expect(node.querySelector(".config-error").textContent)
+        .to.match(/contains more than one wildcard/);
     });
 
     it("should order referenced schema definitions", () => {
@@ -228,6 +249,14 @@ describe("ObjectField", () => {
     });
 
     it("should render the widget with the expected id", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: {type: "string"},
+          bar: {type: "string"}
+        }
+      };
+
       const {node} = createFormComponent({schema, uiSchema: {
         "ui:order": ["bar", "foo"]
       }});
diff --git a/test/SchemaField_test.js b/test/SchemaField_test.js
index f1aa73864..701809cd9 100644
--- a/test/SchemaField_test.js
+++ b/test/SchemaField_test.js
@@ -6,6 +6,7 @@ import TitleField from "../src/components/fields/TitleField";
 import DescriptionField from "../src/components/fields/DescriptionField";
 
 import {createFormComponent, createSandbox} from "./test_utils";
+import {getDefaultRegistry} from "../src/utils";
 
 
 describe("SchemaField", () => {
@@ -97,7 +98,7 @@ describe("SchemaField", () => {
       }});
 
       const {registry} = receivedProps;
-      expect(registry.widgets).eql({});
+      expect(registry.widgets).eql(getDefaultRegistry().widgets);
       expect(registry.definitions).eql({});
       expect(registry.fields).to.be.an("object");
       expect(registry.fields.SchemaField).eql(SchemaField);
diff --git a/test/StringField_test.js b/test/StringField_test.js
index 573740871..e5451dd1f 100644
--- a/test/StringField_test.js
+++ b/test/StringField_test.js
@@ -9,6 +9,8 @@ import {createFormComponent, createSandbox} from "./test_utils";
 describe("StringField", () => {
   let sandbox;
 
+  const CustomWidget = () => <div id="custom"/>;
+
   beforeEach(() => {
     sandbox = createSandbox();
   });
@@ -92,6 +94,20 @@ describe("StringField", () => {
       expect(node.querySelector("input[type=text]").id)
         .eql("root");
     });
+
+    it("should render customized TextWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+        },
+        widgets: {
+          TextWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("SelectWidget", () => {
@@ -170,6 +186,21 @@ describe("StringField", () => {
       expect(node.querySelector("select").id)
         .eql("root");
     });
+
+    it("should render customized SelectWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          enum: []
+        },
+        widgets: {
+          SelectWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("DateTimeWidget", () => {
@@ -243,6 +274,21 @@ describe("StringField", () => {
 
       expect(comp.state.errors).to.have.length.of(1);
     });
+
+    it("should render customized DateTimeWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "date-time",
+        },
+        widgets: {
+          DateTimeWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("DateWidget", () => {
@@ -346,6 +392,21 @@ describe("StringField", () => {
       expect(comp.state.formData).to.be.a("undefined");
       expect(comp.state.errors).to.have.length.of(0);
     });
+
+    it("should render customized DateWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "date",
+        },
+        widgets: {
+          DateWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("AltDateTimeWidget", () => {
@@ -502,6 +563,43 @@ describe("StringField", () => {
         expect(comp.state.formData).eql(undefined);
       });
     });
+
+
+    it("should render customized AltDateWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "date-time",
+        },
+        uiSchema: {
+          "ui:widget": "alt-datetime"
+        },
+        widgets: {
+          AltDateTimeWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
+
+    it("should render customized AltDateTimeWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "date",
+        },
+        uiSchema: {
+          "ui:widget": "alt-datetime"
+        },
+        widgets: {
+          AltDateTimeWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("AltDateWidget", () => {
@@ -657,6 +755,24 @@ describe("StringField", () => {
         expect(comp.state.formData).eql(undefined);
       });
     });
+
+    it("should render customized AltDateWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "date",
+        },
+        uiSchema: {
+          "ui:widget": "alt-date"
+        },
+        widgets: {
+          AltDateWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("EmailWidget", () => {
@@ -751,6 +867,21 @@ describe("StringField", () => {
 
       expect(comp.state.errors).to.have.length.of(1);
     });
+
+    it("should render customized EmailWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "email",
+        },
+        widgets: {
+          EmailWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("URLWidget", () => {
@@ -843,6 +974,21 @@ describe("StringField", () => {
 
       expect(comp.state.errors).to.have.length.of(1);
     });
+
+    it("should render customized URLWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "uri",
+        },
+        widgets: {
+          URLWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("ColorWidget", () => {
@@ -917,6 +1063,21 @@ describe("StringField", () => {
 
       expect(comp.state.errors).to.have.length.of(1);
     });
+
+    it("should render customized ColorWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "color",
+        },
+        widgets: {
+          ColorWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("FileWidget", () => {
@@ -973,6 +1134,21 @@ describe("StringField", () => {
       expect(node.querySelector("[type=file]").id)
         .eql("root");
     });
+
+    it("should render customized FileWidget", () => {
+      const {node} = createFormComponent({
+        schema: {
+          type: "string",
+          format: "data-url",
+        },
+        widgets: {
+          FileWidget: CustomWidget
+        }
+      });
+
+      expect(node.querySelector("#custom"))
+        .to.exist;
+    });
   });
 
   describe("Label", () => {
diff --git a/test/utils_test.js b/test/utils_test.js
index 3ab24bdca..912b964c3 100644
--- a/test/utils_test.js
+++ b/test/utils_test.js
@@ -255,6 +255,10 @@ describe("utils", () => {
     it("should allow numbers with a 0 in the first decimal place", () => {
       expect(asNumber("3.07")).eql(3.07);
     });
+
+    it("should return undefined if the input is empty", () => {
+      expect(asNumber("")).eql(undefined);
+    });
   });
 
   describe("isMultiSelect()", () => {