Repository: mozilla-services/react-jsonschema-form (js)

Issue:
Add support for custom widget components
```js
function MyCustomWidget(props) {
  const {placeholder, value, defaultValue, required, onChange} = props;
  return (
    <div className="my-custom-widget">
      <em>I am a custom widget with value {value}.</em>
      <input type="text"
        value={value}
        defaultValue={defaultValue}
        placeholder={placeholder}
        required={required}
        onChange={(event) => onChange(event.target.value)} />
    </div>
  );
}

const uiSchema = {
  foo: {widget: MyCustomWidget}
}
```

Pull request description:
Fixes #13: Added support for custom widget components.
refs #13

For now it only supports single input widgets, next step will be to provide object field ones; but let's iterate on this.

r=? @leplatrem 

Patch:
diff --git a/README.md b/README.md
index ab6a57ab7..2e4c65284 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,47 @@ Will result in:
 </div>
 ```
 
+## Custom widgets
+
+You can provide your own custom widgets to a uiSchema for the following json data types:
+
+- `string`
+- `number`
+- `integer`
+- `boolean`
+- `date-time`
+
+```js
+const schema = {
+  type: "string"
+};
+
+const uiSchema = {
+  widget: (props) => {
+    return (
+      <input type="text"
+        className="custom"
+        value={props.value}
+        defaultValue={props.defaultValue}
+        required={props.required}
+        onChange={(event) => props.onChange(event.target.value)} />
+    );
+  }
+};
+
+render(<Form schema={schema} uiSchema={uiSchema} />);
+```
+
+The following props are passed to the widget component:
+
+- `schema`: The JSONSchema subschema object for this field;
+- `value`: The current value for this field;
+- `defaultValue`: The default value for this field;
+- `required`: The required status of this field;
+- `onChange`: The value change event handler; call it with the new value everytime it changes;
+- `placeholder`: The placeholder value, if any;
+- `options`: The list of options for `enum` fields;
+
 ## Development server
 
 ```
diff --git a/playground/app.js b/playground/app.js
index 14ea95bdb..88ae27239 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -119,24 +119,24 @@ class App extends Component {
           <h1>react-jsonschema-form</h1>
           <Selector onSelected={this.load.bind(this)} />
         </div>
-        <div className="col-md-6">
+        <div className="col-sm-6">
           <Editor title="JSONSchema"
             code={toJson(this.state.schema)}
             onChange={schema => this.setState({schema})} />
           <div className="row">
-            <div className="col-md-6">
+            <div className="col-sm-6">
               <Editor title="UISchema"
                 code={toJson(this.state.uiSchema)}
                 onChange={uiSchema => this.setState({uiSchema})} />
             </div>
-            <div className="col-md-6">
+            <div className="col-sm-6">
               <Editor title="formData"
                 code={toJson(this.state.formData)}
                 onChange={formData => this.setState({formData})} />
             </div>
           </div>
         </div>
-        <div className="col-md-6">
+        <div className="col-sm-6">
           {!this.state.form ? null :
             <Form
               schema={this.state.schema}
diff --git a/src/utils.js b/src/utils.js
index 99279113f..94d275920 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -6,6 +6,27 @@ import SelectWidget from "./components/widgets/SelectWidget";
 import TextareaWidget from "./components/widgets/TextareaWidget";
 
 
+const altWidgetMap = {
+  boolean: {
+    radio: RadioWidget,
+    select: SelectWidget,
+  },
+  string: {
+    password: PasswordWidget,
+    radio: RadioWidget,
+    select: SelectWidget,
+    textarea: TextareaWidget,
+  },
+  number: {
+    updown: UpDownWidget,
+    range: RangeWidget,
+  },
+  integer: {
+    updown: UpDownWidget,
+    range: RangeWidget,
+  }
+};
+
 export function defaultTypeValue(type) {
   switch (type) {
   case "array":     return [];
@@ -22,38 +43,20 @@ export function defaultFieldValue(formData, schema) {
   return formData === null ? defaultTypeValue(schema.type) : formData;
 }
 
-export function getAlternativeWidget(type, name) {
-  switch(type) {
-  case "boolean":
-    switch(name) {
-    case "radio": return RadioWidget;
-    case "select": return SelectWidget;
-    default:
-      throw new Error(`No alternative widget "${name}" for type ${type}`);
-    }
-    break;
-  case "string":
-    switch(name) {
-    case "password": return PasswordWidget;
-    case "radio": return RadioWidget;
-    case "select": return SelectWidget;
-    case "textarea": return TextareaWidget;
-    default:
-      throw new Error(`No alternative widget "${name}" for type ${type}`);
-    }
-    break;
-  case "number":
-  case "integer":
-    switch(name) {
-    case "updown": return UpDownWidget;
-    case "range": return RangeWidget;
-    default:
-      throw new Error(`No alternative widget "${name}" for type ${type}`);
-    }
-    break;
-  default:
+export function getAlternativeWidget(type, widget) {
+  if (typeof widget === "function") {
+    return widget;
+  }
+  if (typeof widget !== "string") {
+    throw new Error(`Unsupported widget definition: ${typeof widget}`);
+  }
+  if (!altWidgetMap.hasOwnProperty(type)) {
     throw new Error(`No alternative widget for type ${type}`);
   }
+  if (!altWidgetMap[type].hasOwnProperty(widget)) {
+    throw new Error(`No alternative widget "${widget}" for type ${type}`);
+  }
+  return altWidgetMap[type][widget];
 }
 
 export function getDefaultFormState(schema) {


Test patch:
diff --git a/test/index_test.js b/test/index_test.js
index 4e4e2d719..10e48c18d 100644
--- a/test/index_test.js
+++ b/test/index_test.js
@@ -544,6 +544,31 @@ describe("Form", () => {
       });
     });
 
+    describe("custom widget", () => {
+      const schema = {
+        type: "string"
+      };
+
+      const uiSchema = {
+        widget: (props) => {
+          return (
+            <input type="text"
+              className="custom"
+              value={props.value}
+              defaultValue={props.defaultValue}
+              required={props.required}
+              onChange={(event) => props.onChange(event.target.value)} />
+          );
+        }
+      };
+
+      it("should render a custom widget", () => {
+        const {node} = createComponent({schema, uiSchema});
+
+        expect(node.querySelectorAll(".custom")).to.have.length.of(1);
+      });
+    });
+
     describe("string", () => {
       const schema = {
         type: "object",