Skip to content

Commit 70db7a2

Browse files
committed
Fix #284: Introduce a field template API.
1 parent 10bd2c7 commit 70db7a2

File tree

4 files changed

+149
-59
lines changed

4 files changed

+149
-59
lines changed

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: 69 additions & 58 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,23 +58,27 @@ 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({
78+
function DefaultTemplate({
7279
type,
7380
classNames,
74-
errorSchema,
81+
errors,
7582
label,
7683
description,
7784
hidden,
@@ -80,55 +87,38 @@ function Wrapper({
8087
displayLabel,
8188
id,
8289
children,
83-
DescriptionField,
8490
}) {
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 = {
106+
DefaultTemplate.propTypes = {
111107
type: PropTypes.string.isRequired,
112108
id: PropTypes.string,
113109
classNames: PropTypes.string,
114110
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-
]),
111+
description: PropTypes.element,
112+
help: PropTypes.element,
123113
hidden: PropTypes.bool,
124114
required: PropTypes.bool,
115+
readonly: PropTypes.bool,
125116
displayLabel: PropTypes.bool,
126117
children: PropTypes.node.isRequired,
127-
DescriptionField: PropTypes.func,
128118
};
129119
}
130120

131-
Wrapper.defaultProps = {
121+
DefaultTemplate.defaultProps = {
132122
classNames: "",
133123
errorSchema: {errors: []},
134124
hidden: false,
@@ -138,9 +128,10 @@ Wrapper.defaultProps = {
138128

139129
function SchemaField(props) {
140130
const {uiSchema, errorSchema, idSchema, name, required, registry} = props;
141-
const {definitions, fields} = registry;
131+
const {definitions, fields, FieldTemplate = DefaultTemplate} = registry;
142132
const schema = retrieveSchema(props.schema, definitions);
143133
const FieldComponent = getFieldComponent(schema, uiSchema, fields);
134+
const {DescriptionField} = fields;
144135
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
145136
const readonly = Boolean(props.readonly || uiSchema["ui:readonly"]);
146137

@@ -162,25 +153,44 @@ function SchemaField(props) {
162153
displayLabel = false;
163154
}
164155

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

186196
SchemaField.defaultProps = {
@@ -206,6 +216,7 @@ if (process.env.NODE_ENV !== "production") {
206216
])).isRequired,
207217
fields: PropTypes.objectOf(PropTypes.func).isRequired,
208218
definitions: PropTypes.object.isRequired,
219+
FieldTemplate: PropTypes.func,
209220
})
210221
};
211222
}

test/Form_test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,80 @@ describe("Form", () => {
4747
});
4848
});
4949

50+
describe("Custom field template", () => {
51+
const schema = {
52+
type: "object",
53+
title: "root object",
54+
required: ["foo"],
55+
properties: {
56+
foo: {
57+
type: "string",
58+
description: "this is description",
59+
minLength: 32,
60+
}
61+
}
62+
};
63+
64+
const uiSchema = {
65+
foo: {
66+
"ui:help": "this is help"
67+
}
68+
};
69+
70+
const formData = {foo: "invalid"};
71+
72+
function FieldTemplate(props) {
73+
const {id, classNames, label, help, required, description, errors, children} = props;
74+
return (
75+
<div className={"my-template " + classNames}>
76+
<label htmlFor={id}>{label}{required ? "*" : null}</label>
77+
{description}
78+
{children}
79+
{errors}
80+
{help}
81+
</div>
82+
);
83+
}
84+
85+
let node;
86+
87+
beforeEach(() => {
88+
node = createFormComponent({
89+
schema,
90+
uiSchema,
91+
formData,
92+
FieldTemplate,
93+
liveValidate: true,
94+
}).node;
95+
});
96+
97+
it("should use the provided field template", () => {
98+
expect(node.querySelector(".my-template")).to.exist;
99+
});
100+
101+
it("should use the provided template for labels", () => {
102+
expect(node.querySelector(".my-template > label").textContent)
103+
.eql("root object");
104+
expect(node.querySelector(".my-template .field-string > label").textContent)
105+
.eql("foo*");
106+
});
107+
108+
it("should use the provided template for descriptions", () => {
109+
expect(node.querySelector("#root_foo__description").textContent)
110+
.eql("this is description");
111+
});
112+
113+
it("should use the provided template for errors", () => {
114+
expect(node.querySelectorAll(".error-detail li"))
115+
.to.have.length.of(1);
116+
});
117+
118+
it("should use the provided template for help", () => {
119+
expect(node.querySelector(".help-block").textContent)
120+
.eql("this is help");
121+
});
122+
});
123+
50124
describe("Custom submit buttons", () => {
51125
it("should submit the form when clicked", () => {
52126
const onSubmit = sandbox.spy();

0 commit comments

Comments
 (0)