Skip to content

Commit a83d016

Browse files
authored
Fix #284: Introduce a field template API. (#304)
1 parent 10bd2c7 commit a83d016

File tree

6 files changed

+203
-70
lines changed

6 files changed

+203
-70
lines changed

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
4242
- [Placeholders](#placeholders)
4343
- [Form attributes](#form-attributes)
4444
- [Advanced customization](#advanced-customization)
45+
- [Field template](#field-template)
46+
- [Custom widgets and fields](#custom-widgets-and-fields)
4547
- [Custom widget components](#custom-widget-components)
4648
- [Custom component registration](#custom-component-registration)
4749
- [Custom widget options](#custom-widget-options)
@@ -551,7 +553,51 @@ Form component supports the following html attributes:
551553

552554
## Advanced customization
553555

554-
The API allows to specify your own custom *widgets* and *fields* components:
556+
### Field template
557+
558+
To take control over the inner organization of each field (each form row), you can define a *field template* for your form.
559+
560+
A field template is basically a React stateless component being passed field-related props so you can structure your form row as you like:
561+
562+
```jsx
563+
function CustomFieldTemplate(props) {
564+
const {id, classNames, label, help, required, description, errors, children} = props;
565+
return (
566+
<div className={classNames}>
567+
<label htmlFor={id}>{label}{required ? "*" : null}</label>
568+
{description}
569+
{children}
570+
{errors}
571+
{help}
572+
</div>
573+
);
574+
}
575+
576+
render((
577+
<Form schema={schema}
578+
FieldTemplate={CustomFieldTemplate} />,
579+
), document.getElementById("app"));
580+
```
581+
582+
The following props are passed to a custom field template component:
583+
584+
- `id`: The id of the field in the hierarchy. You can use it to render a label targetting the wrapped widget;
585+
- `classNames`: A string containing the base bootstrap CSS classes merged with any [custom ones](#custom-css-class-names) defined in your uiSchema;
586+
- `label`: The computed label for this field, as a string;
587+
- `description`: A component instance rendering the field description, if any defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined);
588+
- `children`: The field or widget component instance for this field row;
589+
- `errors`: A component instance listing any encountered errors for this field;
590+
- `help`: A component instance rendering any `ui:help` uiSchema directive defined;
591+
- `hidden`: A boolean value stating if the field should be hidden;
592+
- `required`: A boolean value stating if the field is required;
593+
- `readonly`: A boolean value stating if the field is read-only;
594+
- `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.
595+
596+
> 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.
597+
598+
### Custom widgets and fields
599+
600+
The API allows to specify your own custom *widget* and *field* components:
555601

556602
- A *widget* represents a HTML tag for the user to enter data, eg. `input`, `select`, etc.
557603
- 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.

playground/samples/date.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module.exports = {
2020
},
2121
alternative: {
2222
title: "Alternative",
23-
description: "These work on every platform.",
23+
description: "These work on most platforms.",
2424
type: "object",
2525
properties: {
2626
"alt-datetime": {

src/components/Form.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@ export default class Form extends Component {
125125
const fields = Object.assign({
126126
SchemaField: _SchemaField,
127127
TitleField: _TitleField,
128-
DescriptionField: _DescriptionField
128+
DescriptionField: _DescriptionField,
129129
}, this.props.fields);
130130
return {
131131
fields,
132+
FieldTemplate: this.props.FieldTemplate,
132133
widgets: this.props.widgets || {},
133134
definitions: this.props.schema.definitions || {},
134135
};
@@ -193,6 +194,7 @@ if (process.env.NODE_ENV !== "production") {
193194
PropTypes.object,
194195
])),
195196
fields: PropTypes.objectOf(PropTypes.func),
197+
FieldTemplate: PropTypes.func,
196198
onChange: PropTypes.func,
197199
onError: PropTypes.func,
198200
showErrorList: PropTypes.bool,

src/components/fields/DescriptionField.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import React, {PropTypes} from "react";
22

33
function DescriptionField(props) {
44
const {id, description} = props;
5+
if (!description) {
6+
return null;
7+
}
58
if (typeof description === "string") {
69
return <p id={id} className="field-description">{description}</p>;
710
} else {

src/components/fields/SchemaField.js

Lines changed: 75 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ import ObjectField from "./ObjectField";
1313
import StringField from "./StringField";
1414
import UnsupportedField from "./UnsupportedField";
1515

16+
1617
const REQUIRED_FIELD_SYMBOL = "*";
1718
const COMPONENT_TYPES = {
18-
"array": ArrayField,
19-
"boolean": BooleanField,
20-
"integer": NumberField,
21-
"number": NumberField,
22-
"object": ObjectField,
23-
"string": StringField,
19+
array: ArrayField,
20+
boolean: BooleanField,
21+
integer: NumberField,
22+
number: NumberField,
23+
object: ObjectField,
24+
string: StringField,
2425
};
2526

2627
function getFieldComponent(schema, uiSchema, fields) {
@@ -34,7 +35,8 @@ function getFieldComponent(schema, uiSchema, fields) {
3435
return COMPONENT_TYPES[schema.type] || UnsupportedField;
3536
}
3637

37-
function getLabel(label, required, id) {
38+
function Label(props) {
39+
const {label, required, id} = props;
3840
if (!label) {
3941
return null;
4042
}
@@ -45,7 +47,8 @@ function getLabel(label, required, id) {
4547
);
4648
}
4749

48-
function renderHelp(help) {
50+
function Help(props) {
51+
const {help} = props;
4952
if (!help) {
5053
return null;
5154
}
@@ -55,92 +58,79 @@ function renderHelp(help) {
5558
return <div className="help-block">{help}</div>;
5659
}
5760

58-
function ErrorList({errors}) {
61+
function ErrorList(props) {
62+
const {errors = []} = props;
63+
if (errors.length === 0) {
64+
return null;
65+
}
5966
return (
6067
<div>
6168
<p/>
6269
<ul className="error-detail bs-callout bs-callout-info">{
63-
(errors || []).map((error, index) => {
70+
errors.map((error, index) => {
6471
return <li className="text-danger" key={index}>{error}</li>;
6572
})
6673
}</ul>
6774
</div>
6875
);
6976
}
7077

71-
function Wrapper({
72-
type,
78+
function DefaultTemplate(props) {
79+
const {
80+
id,
7381
classNames,
74-
errorSchema,
7582
label,
83+
children,
84+
errors,
85+
help,
7686
description,
7787
hidden,
78-
help,
7988
required,
8089
displayLabel,
81-
id,
82-
children,
83-
DescriptionField,
84-
}) {
90+
} = props;
8591
if (hidden) {
8692
return children;
8793
}
88-
const errors = errorSchema.__errors;
89-
const isError = errors && errors.length > 0;
90-
const classList = [
91-
"form-group",
92-
"field",
93-
`field-${type}`,
94-
isError ? "field-error has-error" : "",
95-
classNames,
96-
].join(" ").trim();
9794
return (
98-
<div className={classList}>
99-
{displayLabel && label ? getLabel(label, required, id) : null}
100-
{displayLabel && description ?
101-
<DescriptionField id={`${id}__description`} description={description} /> : null}
95+
<div className={classNames}>
96+
{displayLabel ? <Label label={label} required={required} id={id} /> : null}
97+
{displayLabel && description ? description : null}
10298
{children}
103-
{isError ? <ErrorList errors={errors} /> : <div/>}
104-
{renderHelp(help)}
99+
{errors}
100+
{help}
105101
</div>
106102
);
107103
}
108104

109105
if (process.env.NODE_ENV !== "production") {
110-
Wrapper.propTypes = {
111-
type: PropTypes.string.isRequired,
106+
DefaultTemplate.propTypes = {
112107
id: PropTypes.string,
113108
classNames: PropTypes.string,
114109
label: PropTypes.string,
115-
description: PropTypes.oneOfType([
116-
PropTypes.string,
117-
PropTypes.element,
118-
]),
119-
help: PropTypes.oneOfType([
120-
PropTypes.string,
121-
PropTypes.element,
122-
]),
110+
children: PropTypes.node.isRequired,
111+
errors: PropTypes.element,
112+
help: PropTypes.element,
113+
description: PropTypes.element,
123114
hidden: PropTypes.bool,
124115
required: PropTypes.bool,
116+
readonly: PropTypes.bool,
125117
displayLabel: PropTypes.bool,
126-
children: PropTypes.node.isRequired,
127-
DescriptionField: PropTypes.func,
128118
};
129119
}
130120

131-
Wrapper.defaultProps = {
132-
classNames: "",
133-
errorSchema: {errors: []},
121+
DefaultTemplate.defaultProps = {
134122
hidden: false,
123+
readonly: false,
135124
required: false,
136125
displayLabel: true,
137126
};
138127

139128
function SchemaField(props) {
140129
const {uiSchema, errorSchema, idSchema, name, required, registry} = props;
141-
const {definitions, fields} = registry;
130+
const {definitions, fields, FieldTemplate = DefaultTemplate} = registry;
142131
const schema = retrieveSchema(props.schema, definitions);
143132
const FieldComponent = getFieldComponent(schema, uiSchema, fields);
133+
const {DescriptionField} = fields;
144134
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
145135
const readonly = Boolean(props.readonly || uiSchema["ui:readonly"]);
146136

@@ -162,25 +152,42 @@ function SchemaField(props) {
162152
displayLabel = false;
163153
}
164154

165-
return (
166-
<Wrapper
167-
label={props.schema.title || schema.title || name}
168-
description={props.schema.description || schema.description}
169-
errorSchema={errorSchema}
170-
hidden={uiSchema["ui:widget"] === "hidden"}
171-
help={uiSchema["ui:help"]}
172-
required={required}
173-
type={schema.type}
174-
displayLabel={displayLabel}
175-
id={idSchema.$id}
176-
classNames={uiSchema.classNames}
177-
DescriptionField={fields.DescriptionField}>
178-
<FieldComponent {...props}
179-
schema={schema}
180-
disabled={disabled}
181-
readonly={readonly} />
182-
</Wrapper>
155+
const field = (
156+
<FieldComponent {...props}
157+
schema={schema}
158+
disabled={disabled}
159+
readonly={readonly} />
183160
);
161+
162+
const {type} = schema;
163+
const id = idSchema.$id;
164+
const label = props.schema.title || schema.title || name;
165+
const description = props.schema.description || schema.description;
166+
const errors = errorSchema.__errors;
167+
const help = uiSchema["ui:help"];
168+
const hidden = uiSchema["ui:widget"] === "hidden";
169+
const classNames = [
170+
"form-group",
171+
"field",
172+
`field-${type}`,
173+
errors && errors.length > 0 ? "field-error has-error" : "",
174+
uiSchema.classNames,
175+
].join(" ").trim();
176+
177+
const fieldProps = {
178+
description: <DescriptionField id={id + "__description"} description={description} />,
179+
help: <Help help={help} />,
180+
errors: <ErrorList errors={errors} />,
181+
id,
182+
label,
183+
hidden,
184+
required,
185+
readonly,
186+
displayLabel,
187+
classNames,
188+
};
189+
190+
return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
184191
}
185192

186193
SchemaField.defaultProps = {
@@ -206,6 +213,7 @@ if (process.env.NODE_ENV !== "production") {
206213
])).isRequired,
207214
fields: PropTypes.objectOf(PropTypes.func).isRequired,
208215
definitions: PropTypes.object.isRequired,
216+
FieldTemplate: PropTypes.func,
209217
})
210218
};
211219
}

0 commit comments

Comments
 (0)