Skip to content

Commit

Permalink
Added withTheme HOC (#1226)
Browse files Browse the repository at this point in the history
* Added withTheme HOC

* prettier run

* renamed restData variable

* data.Fields -> data.fields

* Created documentation

* doc: update doc

* test: add test

* removed custom form

* removed custom form from documentation

* Updated withTheme + docs to show pass-through nature of all the props

* Apply suggestions from code review

Co-Authored-By: Ashwin Ramaswami <aramaswamis@gmail.com>

* Update test/withTheme_test.js

Co-Authored-By: Ashwin Ramaswami <aramaswamis@gmail.com>

* Updated tests
  • Loading branch information
danbalarin authored and epicfaace committed May 25, 2019
1 parent 7530230 commit cc7e1b4
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 1 deletion.
94 changes: 94 additions & 0 deletions docs/theme-customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## Customizing with other frameworks

### withTheme Higher-Order Component
The `withTheme` component provides an easy way to extend the functionality of react-jsonschema-form by passing in a theme object that defines custom/overridden widgets and fields, as well as any of the other possible properties of the standard rjsf `Form` component. This theme-defining object is passed as the only parameter to the HOC (`withTheme(ThemeObj)`), and the HOC will return a themed-component which you use instead of the standard `Form` component.

### Usage

```jsx
import React, { Component } from 'react';
import { withTheme } from 'react-jsonschema-form';
import Bootstrap4Theme from 'react-jsonschema-form-theme-bs4';

const ThemedForm = withTheme(Bootstrap4Theme);
class Demo extends Component {
render() {
return <ThemedForm schema={{...}} uiSchema={{...}} />
}
}
```

### Theme object properties
The Theme object consists of the same properties as the rjsf `Form` component (such as **widgets** and **fields**). The themed-Form component merges together any theme-specific **widgets** and **fields** with the default **widgets** and **fields**. For instance, providing a single widget in **widgets** will merge this widget with all the default widgets of the rjsf `Form` component, but overrides the default if the theme's widget's name matches the default widget's name. Thus, for each default widget or field not specified/overridden, the themed-form will rely on the defaults from the rjsf `Form`. Note that you are not required to pass in either custom **widgets** or **fields** when using the custom-themed HOC component; you can make the essentially redefine the default Form by simply doing `const Form = withTheme({});`.

#### Widgets and fields
**widgets** and **fields** should be in the same format as shown [here](/advanced-customization/#custom-widgets-and-fields).

Example theme with custom widget:
```jsx
const MyCustomWidget = (props) => {
return (
<input type="text"
className="custom"
value={props.value}
required={props.required}
onChange={(event) => props.onChange(event.target.value)} />
);
};

const myWidgets = {
myCustomWidget: MyCustomWidget
};

const ThemeObject = {widgets: myWidgets};
export default ThemeObject;
```

The above can be similarly done for **fields**.

#### Templates
Each template should be passed directly into the theme object just as you would into the rjsf Form component. Here is an example of how to use a custom [ArrayFieldTemplate](/advanced-customization/#array-field-template) and [ErrorListTemplate](/advanced-customization/#error-list-template) in the theme object:
```jsx
function MyArrayFieldTemplate(props) {
return (
<div>
{props.items.map(element => element.children)}
{props.canAdd && <button type="button" onClick={props.onAddClick}></button>}
</div>
);
}

function MyErrorListTemplate(props) {
const {errors} = props;
return (
<ul>
{errors.map(error => (
<li key={error.stack}>
{error.stack}
</li>
))}
</ul>
);
}

const ThemeObject = {
ArrayFieldTemplate: MyArrayFieldTemplate,
ErrorList: MyErrorListTemplate,
widgets: myWidgets
};

export default ThemeObject;
```

### Overriding other Form props
Just as the theme can override **widgets**, **fields**, any of the field templates, and set default values to properties like **showErrorList**, you can do the same with the instance of the withTheme() Form component.
```jsx
const ThemeObject = {
ArrayFieldTemplate: MyArrayFieldTemplate,
fields: myFields,
showErrorList: false,
widgets: myWidgets
};
```

Thus, the user has higher priority than the withTheme HOC, and the theme has higher priority than the default values of the rjsf Form component (**User** > **Theme** > **Defaults**).
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ nav:
- Definitions: definitions.md
- Dependencies: dependencies.md
- Form Customization: form-customization.md
- Theme Customization: theme-customization.md
- Validation: validation.md
- Playground: https://mozilla-services.github.io/react-jsonschema-form/

markdown_extensions:
- toc:
permalink: true
permalink: true
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Form from "./components/Form";
import withTheme from "./withTheme";

export { withTheme };
export default Form;
28 changes: 28 additions & 0 deletions src/withTheme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import Form from "./";

function withTheme(themeProps) {
return class extends Component {
render() {
let { fields, widgets, ...directProps } = this.props;
fields = { ...themeProps.fields, ...fields };
widgets = { ...themeProps.widgets, ...widgets };
return (
<Form
{...themeProps}
{...directProps}
fields={fields}
widgets={widgets}
/>
);
}
};
}

withTheme.propTypes = {
widgets: PropTypes.object,
fields: PropTypes.object,
};

export default withTheme;
236 changes: 236 additions & 0 deletions test/withTheme_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { expect } from "chai";
import React from "react";

import { withTheme } from "../src";
import { createComponent, createSandbox } from "./test_utils";

describe("withTheme", () => {
let sandbox;

beforeEach(() => {
sandbox = createSandbox();
});

afterEach(() => {
sandbox.restore();
});

describe("With fields", () => {
it("should use the withTheme field", () => {
const fields = {
StringField() {
return <div className="string-field" />;
},
};
const schema = {
type: "object",
properties: {
fieldA: {
type: "string",
},
fieldB: {
type: "string",
},
},
};
const uiSchema = {};
let { node } = createComponent(withTheme({ fields }), {
schema,
uiSchema,
});
expect(node.querySelectorAll(".string-field")).to.have.length.of(2);
});

it("should use withTheme field and the user defined field", () => {
const themeFields = {
StringField() {
return <div className="string-field" />;
},
};
const userFields = {
NumberField() {
return <div className="number-field" />;
},
};
const schema = {
type: "object",
properties: {
fieldA: {
type: "string",
},
fieldB: {
type: "number",
},
},
};
const uiSchema = {};
let { node } = createComponent(withTheme({ fields: themeFields }), {
schema,
uiSchema,
fields: userFields,
});
expect(node.querySelectorAll(".string-field")).to.have.length.of(1);
expect(node.querySelectorAll(".number-field")).to.have.length.of(1);
});

it("should use only the user defined field", () => {
const themeFields = {
StringField() {
return <div className="string-field" />;
},
};
const userFields = {
StringField() {
return <div className="form-control" />;
},
};
const schema = {
type: "object",
properties: {
fieldA: {
type: "string",
},
fieldB: {
type: "string",
},
},
};
const uiSchema = {};
let { node } = createComponent(withTheme({ fields: themeFields }), {
schema,
uiSchema,
fields: userFields,
});
expect(node.querySelectorAll(".string-field")).to.have.length.of(0);
expect(node.querySelectorAll(".form-control")).to.have.length.of(2);
});
});

describe("With widgets", () => {
it("should use the withTheme widget", () => {
const widgets = {
TextWidget: () => <div id="test" />,
};
const schema = {
type: "string",
};
const uiSchema = {};
let { node } = createComponent(withTheme({ widgets }), {
schema,
uiSchema,
});
expect(node.querySelectorAll("#test")).to.have.length.of(1);
});

it("should use the withTheme widget as well as user defined widget", () => {
const themeWidgets = {
TextWidget: () => <div id="test-theme-widget" />,
};
const userWidgets = {
DateWidget: () => <div id="test-user-widget" />,
};
const schema = {
type: "object",
properties: {
fieldA: {
type: "string",
},
fieldB: {
format: "date",
type: "string",
},
},
};
const uiSchema = {};
let { node } = createComponent(withTheme({ widgets: themeWidgets }), {
schema,
uiSchema,
widgets: userWidgets,
});
expect(node.querySelectorAll("#test-theme-widget")).to.have.length.of(1);
expect(node.querySelectorAll("#test-user-widget")).to.have.length.of(1);
});

it("should use only the user defined widget", () => {
const themeWidgets = {
TextWidget: () => <div id="test-theme-widget" />,
};
const userWidgets = {
TextWidget: () => <div id="test-user-widget" />,
};
const schema = {
type: "object",
properties: {
fieldA: {
type: "string",
},
},
};
const uiSchema = {};
let { node } = createComponent(withTheme({ widgets: themeWidgets }), {
schema,
uiSchema,
widgets: userWidgets,
});
expect(node.querySelectorAll("#test-theme-widget")).to.have.length.of(0);
expect(node.querySelectorAll("#test-user-widget")).to.have.length.of(1);
});
});

describe("With templates", () => {
it("should use the withTheme template", () => {
const themeTemplates = {
FieldTemplate() {
return <div className="with-theme-field-template" />;
},
};
const schema = {
type: "object",
properties: {
fieldA: {
type: "string",
},
fieldB: {
type: "string",
},
},
};
const uiSchema = {};
let { node } = createComponent(withTheme({ ...themeTemplates }), {
schema,
uiSchema,
});
expect(
node.querySelectorAll(".with-theme-field-template")
).to.have.length.of(1);
});

it("should use only the user defined template", () => {
const themeTemplates = {
FieldTemplate() {
return <div className="with-theme-field-template" />;
},
};
const userTemplates = {
FieldTemplate() {
return <div className="user-field-template" />;
},
};

const schema = {
type: "object",
properties: { foo: { type: "string" }, bar: { type: "string" } },
};
let { node } = createComponent(withTheme({ ...themeTemplates }), {
schema,
...userTemplates,
});
expect(
node.querySelectorAll(".with-theme-field-template")
).to.have.length.of(0);
expect(node.querySelectorAll(".user-field-template")).to.have.length.of(
1
);
});
});
});

0 comments on commit cc7e1b4

Please sign in to comment.