Skip to content

Commit

Permalink
Fix #284: Introduce a field template API.
Browse files Browse the repository at this point in the history
  • Loading branch information
n1k0 committed Aug 18, 2016
1 parent 10bd2c7 commit 70db7a2
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 59 deletions.
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
127 changes: 69 additions & 58 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,23 +58,27 @@ 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({
function DefaultTemplate({
type,
classNames,
errorSchema,
errors,
label,
description,
hidden,
Expand All @@ -80,55 +87,38 @@ function Wrapper({
displayLabel,
id,
children,
DescriptionField,
}) {
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 = {
DefaultTemplate.propTypes = {
type: PropTypes.string.isRequired,
id: PropTypes.string,
classNames: PropTypes.string,
label: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
help: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
description: PropTypes.element,
help: PropTypes.element,
hidden: PropTypes.bool,
required: PropTypes.bool,
readonly: PropTypes.bool,
displayLabel: PropTypes.bool,
children: PropTypes.node.isRequired,
DescriptionField: PropTypes.func,
};
}

Wrapper.defaultProps = {
DefaultTemplate.defaultProps = {
classNames: "",
errorSchema: {errors: []},
hidden: false,
Expand All @@ -138,9 +128,10 @@ Wrapper.defaultProps = {

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 +153,44 @@ 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 isError = errors && errors.length > 0;
const help = uiSchema["ui:help"];
const hidden = uiSchema["ui:widget"] === "hidden";
const classNames = [
"form-group",
"field",
`field-${type}`,
isError ? "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} />,
type,
id,
label,
hidden,
required,
readonly,
displayLabel,
classNames,
};

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

SchemaField.defaultProps = {
Expand All @@ -206,6 +216,7 @@ if (process.env.NODE_ENV !== "production") {
])).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
FieldTemplate: PropTypes.func,
})
};
}
Expand Down
74 changes: 74 additions & 0 deletions test/Form_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,80 @@ describe("Form", () => {
});
});

describe("Custom field template", () => {
const schema = {
type: "object",
title: "root object",
required: ["foo"],
properties: {
foo: {
type: "string",
description: "this is description",
minLength: 32,
}
}
};

const uiSchema = {
foo: {
"ui:help": "this is help"
}
};

const formData = {foo: "invalid"};

function FieldTemplate(props) {
const {id, classNames, label, help, required, description, errors, children} = props;
return (
<div className={"my-template " + classNames}>
<label htmlFor={id}>{label}{required ? "*" : null}</label>
{description}
{children}
{errors}
{help}
</div>
);
}

let node;

beforeEach(() => {
node = createFormComponent({
schema,
uiSchema,
formData,
FieldTemplate,
liveValidate: true,
}).node;
});

it("should use the provided field template", () => {
expect(node.querySelector(".my-template")).to.exist;
});

it("should use the provided template for labels", () => {
expect(node.querySelector(".my-template > label").textContent)
.eql("root object");
expect(node.querySelector(".my-template .field-string > label").textContent)
.eql("foo*");
});

it("should use the provided template for descriptions", () => {
expect(node.querySelector("#root_foo__description").textContent)
.eql("this is description");
});

it("should use the provided template for errors", () => {
expect(node.querySelectorAll(".error-detail li"))
.to.have.length.of(1);
});

it("should use the provided template for help", () => {
expect(node.querySelector(".help-block").textContent)
.eql("this is help");
});
});

describe("Custom submit buttons", () => {
it("should submit the form when clicked", () => {
const onSubmit = sandbox.spy();
Expand Down

0 comments on commit 70db7a2

Please sign in to comment.