Repository: mozilla-services/react-jsonschema-form
Pull request #67 (2016-03-15 22:46:00)

Refs #63: Custom widgets registration.
Refs #63. WiP.


Patch:
diff --git a/README.md b/README.md
index d13dd26d9..5dc2daccf 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,29 @@ A [live demo](https://mozilla-services.github.io/react-jsonschema-form/) is host
 
 ![](http://i.imgur.com/oxBlg96.png)
 
+## Table of Contents
+
+  - [Installation](#installation)
+  - [Usage](#usage)
+     - [Custom labels for <code>enum</code> fields](#custom-labels-for-enum-fields)
+     - [Alternative widgets](#alternative-widgets)
+        - [For <code>boolean</code> fields](#for-boolean-fields)
+        - [For <code>string</code> fields](#for-string-fields)
+        - [For <code>number</code> and <code>integer</code> fields](#for-number-and-integer-fields)
+  - [Object fields ordering](#object-fields-ordering)
+  - [Multiple choices list](#multiple-choices-list)
+  - [Custom styles](#custom-styles)
+  - [Custom widgets](#custom-widgets)
+  - [Custom SchemaField](#custom-schemafield)
+  - [Custom titles](#custom-titles)
+  - [Custom buttons](#custom-buttons)
+  - [Development server](#development-server)
+  - [Tests](#tests)
+     - [TDD](#tdd)
+  - [License](#license)
+
+---
+
 ## Installation
 
 Requires React 0.14+.
@@ -119,19 +142,19 @@ render((
 
 Here's a list of supported alternative widgets for different JSONSchema data types:
 
-#### `boolean`:
+#### For `boolean` fields
 
   * `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`:
+#### For `string` fields
 
   * `textarea`: a `textarea` element;
   * `password`: an `input[type=password]` element;
   * by default, a regular `input[type=text]` element is used.
 
-#### `number` and `integer`:
+#### For `number` and `integer` fields
 
   * `updown`: an `input[type=number]` updown selector;
   * `range`: an `input[type=range]` slider;
@@ -221,6 +244,36 @@ const uiSchema = {
 render(<Form schema={schema} uiSchema={uiSchema} />);
 ```
 
+Alternatively, you can register them all at once by passing the `widgets` prop to the `Form` component, and reference their identifier from the `uiSchema`:
+
+```jsx
+const MyCustomWidget = (props) => {
+  return (
+    <input type="text"
+      className="custom"
+      value={props.value}
+      defaultValue={props.defaultValue}
+      required={props.required}
+      onChange={(event) => props.onChange(event.target.value)} />
+  );
+};
+
+const widgets = {
+  myCustomWidget: MyCustomWidget
+};
+
+const uiSchema = {
+  "ui:widget": "myCustomWidget"
+}
+
+render(<Form
+  schema={schema}
+  uiSchema={uiSchema}
+  widgets={widgets}/>);
+```
+
+This is useful if you expose the `uiSchema` as pure JSON, which can't carry functions.
+
 The following props are passed to the widget component:
 
 - `schema`: The JSONSchema subschema object for this field;
diff --git a/package.json b/package.json
index a9852d50d..754da1387 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
   "version": "0.14.0",
   "description": "A simple React component capable of building HTML forms out of a JSON schema.",
   "scripts": {
+    "build:readme": "toctoc README.md -w",
     "build:css": "cp css/react-jsonschema-form.css dist/",
     "build:lib": "rimraf lib && babel -d lib/ src/",
     "build:dist": "rimraf dist && webpack --config webpack.config.dist.js --optimize-minimize",
@@ -10,7 +11,7 @@
     "dist": "npm run build:lib && npm run build:dist && npm run build:css",
     "lint": "eslint src test",
     "publish-to-gh-pages": "npm run build:playground && gh-pages --dist build/",
-    "publish-to-npm": "npm run dist && npm publish",
+    "publish-to-npm": "npm run build:readme && npm run dist && npm publish",
     "start": "node devServer.js",
     "tdd": "npm run test -- -w",
     "test": "NODE_ENV=test mocha --compilers js:babel/register --recursive --require ./test/setup-jsdom.js $(find test -name '*_test.js')"
@@ -22,8 +23,8 @@
   ],
   "engineStrict": false,
   "engines": {
-      "npm": "^2.14.7",
-      "node": ">=4"
+    "npm": "^2.14.7",
+    "node": ">=4"
   },
   "peerDependencies": {
     "react": "^0.14.3",
@@ -56,6 +57,7 @@
     "rimraf": "^2.4.4",
     "sinon": "^1.17.2",
     "style-loader": "^0.13.0",
+    "toctoc": "^0.0.6",
     "webpack": "^1.10.5",
     "webpack-dev-middleware": "^1.4.0",
     "webpack-hot-middleware": "^2.6.0"
diff --git a/src/components/Form.js b/src/components/Form.js
index 98fed2f7e..fd1505233 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -79,11 +79,12 @@ export default class Form extends Component {
     return {
       SchemaField: this.props.SchemaField || SchemaField,
       TitleField: this.props.TitleField || TitleField,
+      widgets: this.props.widgets || {},
     };
   }
 
   render() {
-    const {children, schema, uiSchema} = this.props;
+    const {children, schema, uiSchema, widgets} = this.props;
     const {formData} = this.state;
     const registry = this.getRegistry();
     const _SchemaField = registry.SchemaField;
@@ -94,6 +95,7 @@ export default class Form extends Component {
           schema={schema}
           uiSchema={uiSchema}
           formData={formData}
+          widgets={widgets}
           onChange={this.onChange.bind(this)}
           registry={registry}/>
         { children ? children : <p><button type="submit">Submit</button></p> }
@@ -107,6 +109,7 @@ if (process.env.NODE_ENV !== "production") {
     schema: PropTypes.object.isRequired,
     uiSchema: PropTypes.object,
     formData: PropTypes.any,
+    widgets: PropTypes.object,
     onChange: PropTypes.func,
     onError: PropTypes.func,
     onSubmit: PropTypes.func,
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 837aeea7a..b231c7e2e 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -69,7 +69,7 @@ class ArrayField extends Component {
     const {schema, uiSchema, name} = this.props;
     const title = schema.title || name;
     const {items} = this.state;
-    const SchemaField = this.props.registry.SchemaField;
+    const {SchemaField} = this.props.registry;
     if (isMultiSelect(schema)) {
       return (
         <SelectWidget
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index 10c8281fc..4e0f43bdf 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -3,7 +3,8 @@ import React, { PropTypes } from "react";
 import { defaultFieldValue, getAlternativeWidget, optionsList } from "../../utils";
 import CheckboxWidget from "./../widgets/CheckboxWidget";
 
-function BooleanField({schema, name, uiSchema, formData, required, onChange}) {
+function BooleanField(props) {
+  const {schema, name, uiSchema, formData, widgets, required, onChange} = props;
   const {title, description} = schema;
   const widget = uiSchema["ui:widget"];
   const commonProps = {
@@ -16,7 +17,7 @@ function BooleanField({schema, name, uiSchema, formData, required, onChange}) {
     required,
   };
   if (widget) {
-    const Widget = getAlternativeWidget(schema.type, widget);
+    const Widget = getAlternativeWidget(schema.type, widget, widgets);
     return <Widget options={optionsList({enum: [true, false]})} {... commonProps} />;
   }
   return <CheckboxWidget {...commonProps} />;
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 7b12f047d..4bcc77512 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -67,7 +67,7 @@ class ObjectField extends Component {
               formData={this.state[name]}
               onChange={this.onChange.bind(this, name)}
               registry={this.props.registry} />
-            );
+          );
         })
       }</fieldset>
     );
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index ffa3f2399..491accf13 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -98,6 +98,7 @@ if (process.env.NODE_ENV !== "production") {
   SchemaField.propTypes = {
     schema: PropTypes.object.isRequired,
     uiSchema: PropTypes.object,
+    registry: PropTypes.object,
   };
 }
 
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index d496a34a9..a58c39c74 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -5,7 +5,8 @@ import TextWidget from "./../widgets/TextWidget";
 import SelectWidget from "./../widgets/SelectWidget";
 
 
-function StringField({schema, name, uiSchema, formData, required, onChange}) {
+function StringField(props) {
+  const {schema, name, uiSchema, formData, widgets, required, onChange} = props;
   const {title, description} = schema;
   const widget = uiSchema["ui:widget"];
   const commonProps = {
@@ -19,13 +20,13 @@ function StringField({schema, name, uiSchema, formData, required, onChange}) {
   };
   if (Array.isArray(schema.enum)) {
     if (widget) {
-      const Widget = getAlternativeWidget(schema.type, widget);
+      const Widget = getAlternativeWidget(schema.type, widget, widgets);
       return <Widget options={optionsList(schema)} {...commonProps} />;
     }
     return <SelectWidget options={optionsList(schema)} {...commonProps} />;
   }
   if (widget) {
-    const Widget = getAlternativeWidget(schema.type, widget);
+    const Widget = getAlternativeWidget(schema.type, widget, widgets);
     return <Widget {...commonProps} />;
   }
   return <TextWidget {...commonProps} />;
diff --git a/src/utils.js b/src/utils.js
index 6ad084ce3..6513f22d3 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -43,13 +43,16 @@ export function defaultFieldValue(formData, schema) {
   return formData === null ? defaultTypeValue(schema.type) : formData;
 }
 
-export function getAlternativeWidget(type, widget) {
+export function getAlternativeWidget(type, widget, registeredWidgets={}) {
   if (typeof widget === "function") {
     return widget;
   }
   if (typeof widget !== "string") {
     throw new Error(`Unsupported widget definition: ${typeof widget}`);
   }
+  if (widget in registeredWidgets) {
+    return registeredWidgets[widget];
+  }
   if (!altWidgetMap.hasOwnProperty(type)) {
     throw new Error(`No alternative widget for type ${type}`);
   }


Test patch:
diff --git a/test/index_test.js b/test/index_test.js
index fd31aae7a..0a4918698 100644
--- a/test/index_test.js
+++ b/test/index_test.js
@@ -91,6 +91,26 @@ describe("Form", () => {
     });
   });
 
+  describe("Custom widgets", () => {
+    it("should support custom StringField widgets", () => {
+      const schema = {type: "string"};
+      const uiSchema = {"ui:widget": "foo"};
+      const foo = () => <div id="custom">yo</div>;
+      const {node} = createComponent({schema, uiSchema, widgets: {foo}});
+
+      expect(node.querySelector("#custom").textContent).eql("yo");
+    });
+
+    it("should support custom BooleanField widgets", () => {
+      const schema = {type: "boolean"};
+      const uiSchema = {"ui:widget": "foo"};
+      const foo = () => <div id="custom">yo</div>;
+      const {node} = createComponent({schema, uiSchema, widgets: {foo}});
+
+      expect(node.querySelector("#custom").textContent).eql("yo");
+    });
+  });
+
   describe("Object fields ordering", () => {
     const schema = {
       type: "object",