diff --git a/docs/theme-customization.md b/docs/theme-customization.md new file mode 100644 index 0000000000..7bdb1f6b75 --- /dev/null +++ b/docs/theme-customization.md @@ -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 + } +} +``` + +### 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 ( + 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 ( +
+ {props.items.map(element => element.children)} + {props.canAdd && } +
+ ); +} + +function MyErrorListTemplate(props) { + const {errors} = props; + return ( + + ); +} + +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**). diff --git a/mkdocs.yml b/mkdocs.yml index d3d8fcbdcc..0ef3f96276 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 \ No newline at end of file + permalink: true diff --git a/src/index.js b/src/index.js index ee2b982967..0e36f93a54 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ import Form from "./components/Form"; +import withTheme from "./withTheme"; +export { withTheme }; export default Form; diff --git a/src/withTheme.js b/src/withTheme.js new file mode 100644 index 0000000000..d3f2d10a1e --- /dev/null +++ b/src/withTheme.js @@ -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 ( +
+ ); + } + }; +} + +withTheme.propTypes = { + widgets: PropTypes.object, + fields: PropTypes.object, +}; + +export default withTheme; diff --git a/test/withTheme_test.js b/test/withTheme_test.js new file mode 100644 index 0000000000..dc963c4b78 --- /dev/null +++ b/test/withTheme_test.js @@ -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
; + }, + }; + 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
; + }, + }; + const userFields = { + NumberField() { + return
; + }, + }; + 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
; + }, + }; + const userFields = { + StringField() { + return
; + }, + }; + 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: () =>
, + }; + 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: () =>
, + }; + const userWidgets = { + DateWidget: () =>
, + }; + 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: () =>
, + }; + const userWidgets = { + TextWidget: () =>
, + }; + 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
; + }, + }; + 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
; + }, + }; + const userTemplates = { + FieldTemplate() { + return
; + }, + }; + + 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 + ); + }); + }); +});