diff --git a/README.md b/README.md index cfd4077179..23b4eddfae 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Error List Display](#error-list-display) - [The case of empty strings](#the-case-of-empty-strings) - [Styling your forms](#styling-your-forms) + - [Bootstrap Field Groups](#bootstrap-field-groups) - [Schema definitions and references](#schema-definitions-and-references) - [JSON Schema supporting status](#json-schema-supporting-status) - [Tips and tricks](#tips-and-tricks) @@ -1370,6 +1371,54 @@ Here are some examples from the [playground](http://mozilla-services.github.io/r Last, if you really really want to override the semantics generated by the lib, you can always create and use your own custom [widget](#custom-widget-components), [field](#custom-field-components) and/or [schema field](#custom-schemafield) components. +## Bootstrap Field Groups + +As an additional helper for bootstrap users, we have implemented a special "object" group `"bootstrap"` + +By using the `bootstrap` object all nested fields will be wrapped in a `"row"` div for easier column creation. Additionally any fields that do not have a `col-xs-*` className added will have `col-xs-12` added to them. + +```js +const schema = { + type: "bootstrap", + title: "My Form Group", + properties: { + foo: {type: "string"}, + bar: {type: "string"} + } +}; + +const uiSchema = { + foo: { + classNames: "col-md-6" + }, + bar: { + classNames: "col-xs-4 col-md-6" + } +}; +``` + +will result in + +```html +
+ My Form Group +
+
+ +
+
+ +
+
+
+``` + ## Schema definitions and references This library partially supports [inline schema definition dereferencing]( http://json-schema.org/latest/json-schema-core.html#rfc.section.7.2.3), which is Barbarian for *avoiding to copy and paste commonly used field schemas*: diff --git a/package.json b/package.json index 883da01b38..9d0aa8bcdc 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "publish-to-gh-pages": "npm run build:playground && gh-pages --dist build/", "publish-to-npm": "npm run build:readme && npm run dist && npm publish", "start": "node devServer.js", - "tdd": "cross-env NODE_ENV=test mocha --compilers js:babel-register --watch --require ./test/setup-jsdom.js test/**/*_test.js", - "test": " cross-env NODE_ENV=test mocha --compilers js:babel-register --require ./test/setup-jsdom.js test/**/*_test.js" + "tdd": "cross-env NODE_ENV=test mocha --compilers js:babel-register --watch --require ./test/setup-jsdom.js test/**/*_test.js", + "test": "cross-env NODE_ENV=test mocha --compilers js:babel-register --require ./test/setup-jsdom.js test/**/*_test.js" }, "prettierOptions": "--jsx-bracket-same-line --trailing-comma es5 --semi", "lint-staged": { diff --git a/playground/app.js b/playground/app.js index 4757b35fb2..364d3d442a 100644 --- a/playground/app.js +++ b/playground/app.js @@ -57,89 +57,70 @@ const cmOptions = { }; const themes = { default: { - stylesheet: - "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", + stylesheet: "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", }, cerulean: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/cerulean/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/cerulean/bootstrap.min.css", }, cosmo: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/cosmo/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/cosmo/bootstrap.min.css", }, cyborg: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/cyborg/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/cyborg/bootstrap.min.css", editor: "blackboard", }, darkly: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/darkly/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/darkly/bootstrap.min.css", editor: "mbo", }, flatly: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/flatly/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/flatly/bootstrap.min.css", editor: "ttcn", }, journal: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/journal/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/journal/bootstrap.min.css", }, lumen: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/lumen/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/lumen/bootstrap.min.css", }, paper: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/paper/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/paper/bootstrap.min.css", }, readable: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/readable/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/readable/bootstrap.min.css", }, sandstone: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/sandstone/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/sandstone/bootstrap.min.css", editor: "solarized", }, simplex: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/simplex/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/simplex/bootstrap.min.css", editor: "ttcn", }, slate: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/slate/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/slate/bootstrap.min.css", editor: "monokai", }, spacelab: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/spacelab/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/spacelab/bootstrap.min.css", }, "solarized-dark": { - stylesheet: - "//cdn.rawgit.com/aalpern/bootstrap-solarized/master/bootstrap-solarized-dark.css", + stylesheet: "//cdn.rawgit.com/aalpern/bootstrap-solarized/master/bootstrap-solarized-dark.css", editor: "dracula", }, "solarized-light": { - stylesheet: - "//cdn.rawgit.com/aalpern/bootstrap-solarized/master/bootstrap-solarized-light.css", + stylesheet: "//cdn.rawgit.com/aalpern/bootstrap-solarized/master/bootstrap-solarized-light.css", editor: "solarized", }, superhero: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/superhero/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/superhero/bootstrap.min.css", editor: "dracula", }, united: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/united/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/united/bootstrap.min.css", }, yeti: { - stylesheet: - "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/yeti/bootstrap.min.css", + stylesheet: "//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/yeti/bootstrap.min.css", editor: "eclipse", }, }; diff --git a/playground/samples/bootstrap.js b/playground/samples/bootstrap.js new file mode 100644 index 0000000000..4409c4623c --- /dev/null +++ b/playground/samples/bootstrap.js @@ -0,0 +1,73 @@ +module.exports = { + schema: { + title: "A Bootstrap Grid registration form", + description: "A Bootstrap Grid registration form example.", + type: "bootstrap", + required: ["firstName", "lastName"], + properties: { + firstName: { + type: "string", + title: "First name", + }, + lastName: { + type: "string", + title: "Last name", + }, + age: { + type: "integer", + title: "Age", + }, + bio: { + type: "string", + title: "Bio", + }, + password: { + type: "string", + title: "Password", + minLength: 3, + }, + telephone: { + type: "string", + title: "Telephone", + minLength: 10, + }, + }, + }, + uiSchema: { + firstName: { + classNames: "col-xs-6 col-md-4", + "ui:autofocus": true, + "ui:emptyValue": "", + }, + lastName: { + classNames: "col-xs-6 col-md-4", + }, + age: { + classNames: "col-md-4", + "ui:widget": "updown", + "ui:title": "Age of person", + }, + bio: { + "ui:widget": "textarea", + }, + password: { + "ui:widget": "password", + "ui:help": "Hint: Make it strong!", + }, + date: { + "ui:widget": "alt-datetime", + }, + telephone: { + "ui:options": { + inputType: "tel", + }, + }, + }, + formData: { + firstName: "Chuck", + lastName: "Norris", + age: 75, + bio: "Roundhouse kicking asses since 1940", + password: "noneed", + }, +}; diff --git a/playground/samples/customArray.js b/playground/samples/customArray.js index ebd7f3e32d..ab984cb368 100644 --- a/playground/samples/customArray.js +++ b/playground/samples/customArray.js @@ -4,7 +4,7 @@ function ArrayFieldTemplate(props) { return (
{props.items && - props.items.map(element => + props.items.map(element => (
{element.children} @@ -30,7 +30,7 @@ function ArrayFieldTemplate(props) {
- )} + ))} {props.canAdd &&
diff --git a/playground/samples/date.js b/playground/samples/date.js index 206820d35c..30886ea5c5 100644 --- a/playground/samples/date.js +++ b/playground/samples/date.js @@ -5,8 +5,7 @@ module.exports = { properties: { native: { title: "Native", - description: - "May not work on some browsers, notably Firefox Desktop and IE.", + description: "May not work on some browsers, notably Firefox Desktop and IE.", type: "object", properties: { datetime: { diff --git a/playground/samples/index.js b/playground/samples/index.js index 68fa8d22d8..008b91b5ec 100644 --- a/playground/samples/index.js +++ b/playground/samples/index.js @@ -1,4 +1,5 @@ import arrays from "./arrays"; +import bootstrap from "./bootstrap"; import nested from "./nested"; import numbers from "./numbers"; import simple from "./simple"; @@ -17,6 +18,7 @@ import alternatives from "./alternatives"; export const samples = { Simple: simple, + bootstrap: bootstrap, Nested: nested, Arrays: arrays, Numbers: numbers, diff --git a/playground/samples/nested.js b/playground/samples/nested.js index adc5a6be59..fecf3f77d1 100644 --- a/playground/samples/nested.js +++ b/playground/samples/nested.js @@ -49,14 +49,12 @@ module.exports = { tasks: [ { title: "My first task", - details: - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + details: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", done: true, }, { title: "My second task", - details: - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur", + details: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur", done: false, }, ], diff --git a/playground/samples/validation.js b/playground/samples/validation.js index 02434ad36c..492bfedb35 100644 --- a/playground/samples/validation.js +++ b/playground/samples/validation.js @@ -19,8 +19,7 @@ function transformErrors(errors) { module.exports = { schema: { title: "Custom validation", - description: - "This form defines custom validation rules checking that the two passwords match.", + description: "This form defines custom validation rules checking that the two passwords match.", type: "object", properties: { pass1: { diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 4aa51bf861..932b0d08c3 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -464,8 +464,8 @@ class ArrayField extends Component { const itemUiSchema = additional ? uiSchema.additionalItems || {} : Array.isArray(uiSchema.items) - ? uiSchema.items[index] - : uiSchema.items || {}; + ? uiSchema.items[index] + : uiSchema.items || {}; const itemErrorSchema = errorSchema ? errorSchema[index] : undefined; return this.renderArrayFieldItem({ diff --git a/src/components/fields/BootstrapObjectField.js b/src/components/fields/BootstrapObjectField.js new file mode 100644 index 0000000000..35c416a7f8 --- /dev/null +++ b/src/components/fields/BootstrapObjectField.js @@ -0,0 +1,137 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { + orderProperties, + retrieveSchema, + getDefaultRegistry, +} from "../../utils"; + +class BootstrapObjectField extends Component { + static defaultProps = { + uiSchema: {}, + formData: {}, + errorSchema: {}, + idSchema: {}, + required: false, + disabled: false, + readonly: false, + }; + + isRequired(name) { + const schema = this.props.schema; + return ( + Array.isArray(schema.required) && schema.required.indexOf(name) !== -1 + ); + } + + onPropertyChange = name => { + return (value, options) => { + const newFormData = { ...this.props.formData, [name]: value }; + this.props.onChange(newFormData, options); + }; + }; + + render() { + const { + uiSchema, + formData, + errorSchema, + idSchema, + name, + required, + disabled, + readonly, + onBlur, + registry = getDefaultRegistry(), + } = this.props; + const { definitions, fields, formContext } = registry; + const { SchemaField, TitleField, DescriptionField } = fields; + const schema = retrieveSchema(this.props.schema, definitions); + const title = schema.title === undefined ? name : schema.title; + let orderedProperties; + try { + const properties = Object.keys(schema.properties); + orderedProperties = orderProperties(properties, uiSchema["ui:order"]); + } catch (err) { + return ( +
+

+ Invalid {name || "root"} object field configuration: + {err.message}. +

+
{JSON.stringify(schema)}
+
+ ); + } + return ( +
+ {title && + } + {schema.description && + } +
+ {orderedProperties.map((name, index) => { + if (!uiSchema[name]) { + uiSchema[name] = {}; + } + if (!/col\-xs\-.*/.test(uiSchema[name]["classNames"])) { + uiSchema[name]["classNames"] = + (uiSchema[name]["classNames"] || "") + " col-xs-12"; + } + return ( + + ); + })} +
+
+ ); + } +} + +if (process.env.NODE_ENV !== "production") { + BootstrapObjectField.propTypes = { + schema: PropTypes.object.isRequired, + uiSchema: PropTypes.object, + errorSchema: PropTypes.object, + idSchema: PropTypes.object, + onChange: PropTypes.func.isRequired, + formData: PropTypes.object, + required: PropTypes.bool, + disabled: PropTypes.bool, + readonly: PropTypes.bool, + registry: PropTypes.shape({ + widgets: PropTypes.objectOf( + PropTypes.oneOfType([PropTypes.func, PropTypes.object]) + ).isRequired, + fields: PropTypes.objectOf(PropTypes.func).isRequired, + definitions: PropTypes.object.isRequired, + formContext: PropTypes.object.isRequired, + }), + }; +} + +export default BootstrapObjectField; diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index 1b20d54ffc..93ea06e93b 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -15,6 +15,7 @@ const REQUIRED_FIELD_SYMBOL = "*"; const COMPONENT_TYPES = { array: "ArrayField", boolean: "BooleanField", + bootstrap: "BootstrapObjectField", integer: "NumberField", number: "NumberField", object: "ObjectField", diff --git a/src/components/fields/index.js b/src/components/fields/index.js index c8e92e5dde..937ca95239 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -1,5 +1,6 @@ import ArrayField from "./ArrayField"; import BooleanField from "./BooleanField"; +import BootstrapObjectField from "./BootstrapObjectField"; import DescriptionField from "./DescriptionField"; import NumberField from "./NumberField"; import ObjectField from "./ObjectField"; @@ -11,6 +12,7 @@ import UnsupportedField from "./UnsupportedField"; export default { ArrayField, BooleanField, + BootstrapObjectField, DescriptionField, NumberField, ObjectField, diff --git a/src/components/widgets/AltDateWidget.js b/src/components/widgets/AltDateWidget.js index 9618bd8187..b28f48e75e 100644 --- a/src/components/widgets/AltDateWidget.js +++ b/src/components/widgets/AltDateWidget.js @@ -121,7 +121,7 @@ class AltDateWidget extends Component { const { id, disabled, readonly, autofocus, registry, onBlur } = this.props; return (