Skip to content

Commit

Permalink
Fix #284: Introduce a field template API. (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
n1k0 authored Aug 19, 2016
1 parent 10bd2c7 commit a83d016
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 70 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<div className={classNames}>
<label htmlFor={id}>{label}{required ? "*" : null}</label>
{description}
{children}
{errors}
{help}
</div>
);
}

render((
<Form schema={schema}
FieldTemplate={CustomFieldTemplate} />,
), 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.
Expand Down
2 changes: 1 addition & 1 deletion playground/samples/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {},
};
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/components/fields/DescriptionField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <p id={id} className="field-description">{description}</p>;
} else {
Expand Down
142 changes: 75 additions & 67 deletions src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand All @@ -45,7 +47,8 @@ function getLabel(label, required, id) {
);
}

function renderHelp(help) {
function Help(props) {
const {help} = props;
if (!help) {
return null;
}
Expand All @@ -55,92 +58,79 @@ function renderHelp(help) {
return <div className="help-block">{help}</div>;
}

function ErrorList({errors}) {
function ErrorList(props) {
const {errors = []} = props;
if (errors.length === 0) {
return null;
}
return (
<div>
<p/>
<ul className="error-detail bs-callout bs-callout-info">{
(errors || []).map((error, index) => {
errors.map((error, index) => {
return <li className="text-danger" key={index}>{error}</li>;
})
}</ul>
</div>
);
}

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 (
<div className={classList}>
{displayLabel && label ? getLabel(label, required, id) : null}
{displayLabel && description ?
<DescriptionField id={`${id}__description`} description={description} /> : null}
<div className={classNames}>
{displayLabel ? <Label label={label} required={required} id={id} /> : null}
{displayLabel && description ? description : null}
{children}
{isError ? <ErrorList errors={errors} /> : <div/>}
{renderHelp(help)}
{errors}
{help}
</div>
);
}

if (process.env.NODE_ENV !== "production") {
Wrapper.propTypes = {
type: PropTypes.string.isRequired,
DefaultTemplate.propTypes = {
id: PropTypes.string,
classNames: PropTypes.string,
label: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
help: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
children: PropTypes.node.isRequired,
errors: PropTypes.element,
help: PropTypes.element,
description: PropTypes.element,
hidden: PropTypes.bool,
required: PropTypes.bool,
readonly: PropTypes.bool,
displayLabel: PropTypes.bool,
children: PropTypes.node.isRequired,
DescriptionField: PropTypes.func,
};
}

Wrapper.defaultProps = {
classNames: "",
errorSchema: {errors: []},
DefaultTemplate.defaultProps = {
hidden: false,
readonly: false,
required: false,
displayLabel: true,
};

function SchemaField(props) {
const {uiSchema, errorSchema, idSchema, name, required, registry} = props;
const {definitions, fields} = registry;
const {definitions, fields, FieldTemplate = DefaultTemplate} = registry;
const schema = retrieveSchema(props.schema, definitions);
const FieldComponent = getFieldComponent(schema, uiSchema, fields);
const {DescriptionField} = fields;
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
const readonly = Boolean(props.readonly || uiSchema["ui:readonly"]);

Expand All @@ -162,25 +152,42 @@ function SchemaField(props) {
displayLabel = false;
}

return (
<Wrapper
label={props.schema.title || schema.title || name}
description={props.schema.description || schema.description}
errorSchema={errorSchema}
hidden={uiSchema["ui:widget"] === "hidden"}
help={uiSchema["ui:help"]}
required={required}
type={schema.type}
displayLabel={displayLabel}
id={idSchema.$id}
classNames={uiSchema.classNames}
DescriptionField={fields.DescriptionField}>
<FieldComponent {...props}
schema={schema}
disabled={disabled}
readonly={readonly} />
</Wrapper>
const field = (
<FieldComponent {...props}
schema={schema}
disabled={disabled}
readonly={readonly} />
);

const {type} = schema;
const id = idSchema.$id;
const label = props.schema.title || schema.title || name;
const description = props.schema.description || schema.description;
const errors = errorSchema.__errors;
const help = uiSchema["ui:help"];
const hidden = uiSchema["ui:widget"] === "hidden";
const classNames = [
"form-group",
"field",
`field-${type}`,
errors && errors.length > 0 ? "field-error has-error" : "",
uiSchema.classNames,
].join(" ").trim();

const fieldProps = {
description: <DescriptionField id={id + "__description"} description={description} />,
help: <Help help={help} />,
errors: <ErrorList errors={errors} />,
id,
label,
hidden,
required,
readonly,
displayLabel,
classNames,
};

return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
}

SchemaField.defaultProps = {
Expand All @@ -206,6 +213,7 @@ if (process.env.NODE_ENV !== "production") {
])).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
FieldTemplate: PropTypes.func,
})
};
}
Expand Down
Loading

0 comments on commit a83d016

Please sign in to comment.