diff --git a/README.md b/README.md index 3bd743b3ef..996e967c1d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Placeholders](#placeholders) - [Form attributes](#form-attributes) - [Advanced customization](#advanced-customization) + - [Field template](#field-template) + - [Custom widgets and fields](#custom-widgets-and-fields) - [Custom widget components](#custom-widget-components) - [Custom component registration](#custom-component-registration) - [Custom widget options](#custom-widget-options) @@ -551,7 +553,51 @@ Form component supports the following html attributes: ## Advanced customization -The API allows to specify your own custom *widgets* and *fields* components: +### Field template + +To take control over the inner organization of each field (each form row), you can define a *field template* for your form. + +A field template is basically a React stateless component being passed field-related props so you can structure your form row as you like: + +```jsx +function CustomFieldTemplate(props) { + const {id, classNames, label, help, required, description, errors, children} = props; + return ( +
+ + {description} + {children} + {errors} + {help} +
+ ); +} + +render(( +
, +), document.getElementById("app")); +``` + +The following props are passed to a custom field template component: + +- `id`: The id of the field in the hierarchy. You can use it to render a label targetting the wrapped widget; +- `classNames`: A string containing the base bootstrap CSS classes merged with any [custom ones](#custom-css-class-names) defined in your uiSchema; +- `label`: The computed label for this field, as a string; +- `description`: A component instance rendering the field description, if any defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined); +- `children`: The field or widget component instance for this field row; +- `errors`: A component instance listing any encountered errors for this field; +- `help`: A component instance rendering any `ui:help` uiSchema directive defined; +- `hidden`: A boolean value stating if the field should be hidden; +- `required`: A boolean value stating if the field is required; +- `readonly`: A boolean value stating if the field is read-only; +- `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI. + +> Note: you can only define a single field template for a form. If you need many, it's probably time to look for [custom fields](#custom-field-components) instead. + +### Custom widgets and fields + +The API allows to specify your own custom *widget* and *field* components: - A *widget* represents a HTML tag for the user to enter data, eg. `input`, `select`, etc. - A *field* usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels. diff --git a/playground/samples/date.js b/playground/samples/date.js index 7517699b08..cb5b25ecc1 100644 --- a/playground/samples/date.js +++ b/playground/samples/date.js @@ -20,7 +20,7 @@ module.exports = { }, alternative: { title: "Alternative", - description: "These work on every platform.", + description: "These work on most platforms.", type: "object", properties: { "alt-datetime": { diff --git a/src/components/Form.js b/src/components/Form.js index 9cd54957b0..0f33e8e2e0 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -125,10 +125,11 @@ export default class Form extends Component { const fields = Object.assign({ SchemaField: _SchemaField, TitleField: _TitleField, - DescriptionField: _DescriptionField + DescriptionField: _DescriptionField, }, this.props.fields); return { fields, + FieldTemplate: this.props.FieldTemplate, widgets: this.props.widgets || {}, definitions: this.props.schema.definitions || {}, }; @@ -193,6 +194,7 @@ if (process.env.NODE_ENV !== "production") { PropTypes.object, ])), fields: PropTypes.objectOf(PropTypes.func), + FieldTemplate: PropTypes.func, onChange: PropTypes.func, onError: PropTypes.func, showErrorList: PropTypes.bool, diff --git a/src/components/fields/DescriptionField.js b/src/components/fields/DescriptionField.js index 0082475caf..4d63845f1d 100644 --- a/src/components/fields/DescriptionField.js +++ b/src/components/fields/DescriptionField.js @@ -2,6 +2,9 @@ import React, {PropTypes} from "react"; function DescriptionField(props) { const {id, description} = props; + if (!description) { + return null; + } if (typeof description === "string") { return

{description}

; } else { diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index e905652353..2ede563498 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -13,14 +13,15 @@ import ObjectField from "./ObjectField"; import StringField from "./StringField"; import UnsupportedField from "./UnsupportedField"; + const REQUIRED_FIELD_SYMBOL = "*"; const COMPONENT_TYPES = { - "array": ArrayField, - "boolean": BooleanField, - "integer": NumberField, - "number": NumberField, - "object": ObjectField, - "string": StringField, + array: ArrayField, + boolean: BooleanField, + integer: NumberField, + number: NumberField, + object: ObjectField, + string: StringField, }; function getFieldComponent(schema, uiSchema, fields) { @@ -34,7 +35,8 @@ function getFieldComponent(schema, uiSchema, fields) { return COMPONENT_TYPES[schema.type] || UnsupportedField; } -function getLabel(label, required, id) { +function Label(props) { + const {label, required, id} = props; if (!label) { return null; } @@ -45,7 +47,8 @@ function getLabel(label, required, id) { ); } -function renderHelp(help) { +function Help(props) { + const {help} = props; if (!help) { return null; } @@ -55,12 +58,16 @@ function renderHelp(help) { return
{help}
; } -function ErrorList({errors}) { +function ErrorList(props) { + const {errors = []} = props; + if (errors.length === 0) { + return null; + } return (

@@ -68,79 +75,62 @@ function ErrorList({errors}) { ); } -function Wrapper({ - type, +function DefaultTemplate(props) { + const { + id, classNames, - errorSchema, label, + children, + errors, + help, description, hidden, - help, required, displayLabel, - id, - children, - DescriptionField, - }) { + } = props; if (hidden) { return children; } - const errors = errorSchema.__errors; - const isError = errors && errors.length > 0; - const classList = [ - "form-group", - "field", - `field-${type}`, - isError ? "field-error has-error" : "", - classNames, - ].join(" ").trim(); return ( -
- {displayLabel && label ? getLabel(label, required, id) : null} - {displayLabel && description ? - : null} +
+ {displayLabel ?