Skip to content

Commit

Permalink
Merge pull request #67 from mozilla-services/register-widgets
Browse files Browse the repository at this point in the history
Refs #63: Custom widgets registration.
  • Loading branch information
n1k0 committed Mar 16, 2016
2 parents 0c52f01 + f56d2d7 commit 22b8e8e
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 15 deletions.
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
"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",
"build:playground": "rimraf build && webpack --config webpack.config.prod.js --optimize-minimize && cp playground/index.prod.html build/index.html",
"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')"
Expand All @@ -22,8 +23,8 @@
],
"engineStrict": false,
"engines": {
"npm": "^2.14.7",
"node": ">=4"
"npm": "^2.14.7",
"node": ">=4"
},
"peerDependencies": {
"react": "^0.14.3",
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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> }
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/components/fields/BooleanField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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} />;
Expand Down
2 changes: 1 addition & 1 deletion src/components/fields/ObjectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class ObjectField extends Component {
formData={this.state[name]}
onChange={this.onChange.bind(this, name)}
registry={this.props.registry} />
);
);
})
}</fieldset>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ if (process.env.NODE_ENV !== "production") {
SchemaField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object,
registry: PropTypes.object,
};
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/fields/StringField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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} />;
Expand Down
5 changes: 4 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down
20 changes: 20 additions & 0 deletions test/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 22b8e8e

Please sign in to comment.