Skip to content

Fix #284: Introduce a field template API. #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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