Skip to content
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
- [Form submission](#form-submission)
- [Form error event handler](#form-error-event-handler)
- [Form data changes](#form-data-changes)
- [Form field blur events](#form-field-blur-events)
- [Form customization](#form-customization)
- [The uiSchema object](#the-uischema-object)
- [Alternative widgets](#alternative-widgets)
Expand Down Expand Up @@ -194,6 +195,10 @@ render((

If you plan on being notified everytime the form data are updated, you can pass an `onChange` handler, which will receive the same args as `onSubmit` any time a value is updated in the form.

#### Form field blur events

Sometimes you may want to trigger events or modify external state when a field has been touched, so you can pass an `onBlur` handler, which will receive the id of the input that was blurred and the field value.

## Form customization

### The `uiSchema` object
Expand Down Expand Up @@ -812,12 +817,14 @@ render((

The following props are passed to custom widget components:

- `id`: The generated id for this field;
- `schema`: The JSONSchema subschema object for this field;
- `value`: The current value for this field;
- `required`: The required status of this field;
- `disabled`: `true` if the widget is disabled;
- `readonly`: `true` if the widget is read-only;
- `onChange`: The value change event handler; call it with the new value everytime it changes;
- `onBlur`: The input blur event handler; call it with the the widget id and value;
- `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)).
- `formContext`: The `formContext` object that you passed to Form.

Expand Down
1 change: 1 addition & 0 deletions playground/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ class App extends Component {
onChange={this.onFormDataChange}
fields={{geo: GeoPosition}}
validate={validate}
onBlur={(id, value) => console.log(`Touched ${id} with value ${value}`)}
transformErrors={transformErrors}
onError={log("errors")} />}
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export default class Form extends Component {
});
};

onBlur = (...args) => {
if (this.props.onBlur) {
this.props.onBlur(...args);
}
}

onSubmit = (event) => {
event.preventDefault();
this.setState({status: "submitted"});
Expand Down Expand Up @@ -164,6 +170,7 @@ export default class Form extends Component {
idSchema={idSchema}
formData={formData}
onChange={this.onChange}
onBlur={this.onBlur}
registry={registry}
safeRenderCompletion={safeRenderCompletion}/>
{ children ? children :
Expand Down
23 changes: 16 additions & 7 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ class ArrayField extends Component {
disabled,
readonly,
autofocus,
registry
registry,
onBlur
} = this.props;
const title = (schema.title === undefined) ? name : schema.title;
const {items = []} = this.state;
Expand All @@ -306,7 +307,8 @@ class ArrayField extends Component {
itemErrorSchema,
itemData: items[index],
itemUiSchema: uiSchema.items,
autofocus: autofocus && index === 0
autofocus: autofocus && index === 0,
onBlur
});
}),
className: `field field-array field-array-of-${itemsSchema.type}`,
Expand All @@ -327,7 +329,7 @@ class ArrayField extends Component {
}

renderMultiSelect() {
const {schema, idSchema, uiSchema, disabled, readonly, autofocus} = this.props;
const {schema, idSchema, uiSchema, disabled, readonly, autofocus, onBlur} = this.props;
const {items} = this.state;
const {widgets, definitions} = this.props.registry;
const itemsSchema = retrieveSchema(schema.items, definitions);
Expand All @@ -339,6 +341,7 @@ class ArrayField extends Component {
id={idSchema && idSchema.$id}
multiple
onChange={this.onSelectChange}
onBlur={onBlur}
options={options}
schema={schema}
value={items}
Expand All @@ -349,7 +352,7 @@ class ArrayField extends Component {
}

renderFiles() {
const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus} = this.props;
const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus, onBlur} = this.props;
const title = schema.title || name;
const {items} = this.state;
const {widgets} = this.props.registry;
Expand All @@ -361,6 +364,7 @@ class ArrayField extends Component {
id={idSchema && idSchema.$id}
multiple
onChange={this.onSelectChange}
onBlur={onBlur}
schema={schema}
title={title}
value={items}
Expand All @@ -381,7 +385,8 @@ class ArrayField extends Component {
disabled,
readonly,
autofocus,
registry
registry,
onBlur
} = this.props;
const title = schema.title || name;
let {items} = this.state;
Expand Down Expand Up @@ -428,7 +433,8 @@ class ArrayField extends Component {
itemUiSchema,
itemIdSchema,
itemErrorSchema,
autofocus: autofocus && index === 0
autofocus: autofocus && index === 0,
onBlur
});
}),
onAddClick: this.onAddClick,
Expand All @@ -454,7 +460,8 @@ class ArrayField extends Component {
itemUiSchema,
itemIdSchema,
itemErrorSchema,
autofocus
autofocus,
onBlur
}) {
const {SchemaField} = this.props.registry.fields;
const {disabled, readonly, uiSchema} = this.props;
Expand All @@ -480,6 +487,7 @@ class ArrayField extends Component {
idSchema={itemIdSchema}
required={this.isItemRequired(itemSchema)}
onChange={this.onChangeForIndex(index)}
onBlur={onBlur}
registry={this.props.registry}
disabled={this.props.disabled}
readonly={this.props.readonly}
Expand Down Expand Up @@ -524,6 +532,7 @@ if (process.env.NODE_ENV !== "production") {
idSchema: PropTypes.object,
errorSchema: PropTypes.object,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func,
formData: PropTypes.array,
required: PropTypes.bool,
disabled: PropTypes.bool,
Expand Down
4 changes: 3 additions & 1 deletion src/components/fields/ObjectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ class ObjectField extends Component {
name,
required,
disabled,
readonly
readonly,
onBlur
} = this.props;
const {definitions, fields, formContext} = this.props.registry;
const {SchemaField, TitleField, DescriptionField} = fields;
Expand Down Expand Up @@ -136,6 +137,7 @@ class ObjectField extends Component {
idSchema={idSchema[name]}
formData={this.state[name]}
onChange={this.onPropertyChange(name)}
onBlur={onBlur}
registry={this.props.registry}
disabled={disabled}
readonly={readonly}/>
Expand Down
5 changes: 4 additions & 1 deletion src/components/fields/StringField.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ function StringField(props) {
readonly,
autofocus,
registry,
onChange
onChange,
onBlur
} = props;
const {title, format} = schema;
const {widgets, formContext} = registry;
Expand All @@ -37,6 +38,7 @@ function StringField(props) {
label={title === undefined ? name : title}
value={defaultFieldValue(formData, schema)}
onChange={onChange}
onBlur={onBlur}
required={required}
disabled={disabled}
readonly={readonly}
Expand All @@ -52,6 +54,7 @@ if (process.env.NODE_ENV !== "production") {
uiSchema: PropTypes.object.isRequired,
idSchema: PropTypes.object,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired,
formData: PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
Expand Down
9 changes: 6 additions & 3 deletions src/components/widgets/AltDateWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function readyForChange(state) {
}

function DateElement(props) {
const {type, range, value, select, rootId, disabled, readonly, autofocus, registry} = props;
const {type, range, value, select, rootId, disabled, readonly, autofocus, registry, onBlur} = props;
const id = rootId + "_" + type;
const {SelectWidget} = registry.widgets;
return (
Expand All @@ -29,7 +29,8 @@ function DateElement(props) {
disabled={disabled}
readonly={readonly}
autofocus={autofocus}
onChange={(value) => select(type, value)}/>
onChange={(value) => select(type, value)}
onBlur={onBlur}/>
);
}

Expand Down Expand Up @@ -101,7 +102,7 @@ class AltDateWidget extends Component {
}

render() {
const {id, disabled, readonly, autofocus, registry} = this.props;
const {id, disabled, readonly, autofocus, registry, onBlur} = this.props;
return (
<ul className="list-inline">{
this.dateElementProps.map((elemProps, i) => (
Expand All @@ -113,6 +114,7 @@ class AltDateWidget extends Component {
disabled= {disabled}
readonly={readonly}
registry={registry}
onBlur={onBlur}
autofocus={autofocus && i === 0}/>
</li>
))
Expand Down Expand Up @@ -140,6 +142,7 @@ if (process.env.NODE_ENV !== "production") {
readonly: PropTypes.bool,
autofocus: PropTypes.bool,
onChange: PropTypes.func,
onBlur: PropTypes.func,
time: PropTypes.bool,
};
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/widgets/BaseInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function BaseInput(props) {
value,
readonly,
autofocus,
onBlur,
options, // eslint-disable-line
schema, // eslint-disable-line
formContext, // eslint-disable-line
Expand All @@ -24,7 +25,8 @@ function BaseInput(props) {
readOnly={readonly}
autoFocus={autofocus}
value={typeof value === "undefined" ? "" : value}
onChange={_onChange} />
onChange={_onChange}
onBlur={onBlur && (event => onBlur(inputProps.id, event.target.value))}/>
);
}

Expand All @@ -46,6 +48,7 @@ if (process.env.NODE_ENV !== "production") {
readonly: PropTypes.bool,
autofocus: PropTypes.bool,
onChange: PropTypes.func,
onBlur: PropTypes.func
};
}

Expand Down
27 changes: 18 additions & 9 deletions src/components/widgets/SelectWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, {PropTypes} from "react";

import {asNumber} from "../../utils";


/**
* This is a silly limitation in the DOM where option change event values are
* always retrieved as strings.
Expand All @@ -18,6 +17,16 @@ function processValue({type, items}, value) {
return value;
}

function getValue(event, multiple) {
if (multiple) {
return [].slice.call(event.target.options)
.filter(o => o.selected)
.map(o => o.value);
} else {
return event.target.value;
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's take the opportunity to improve readability here:

function getValue(event, multiple) {
  if (multiple) {
    return [].slice.call(event.target.options)
      .filter(o => o.selected)
      .map(o => o.value);
  } else {
    return event.target.value;
  }
}


function SelectWidget({
schema,
id,
Expand All @@ -28,7 +37,8 @@ function SelectWidget({
readonly,
multiple,
autofocus,
onChange
onChange,
onBlur
}) {
const {enumOptions} = options;
return (
Expand All @@ -41,14 +51,12 @@ function SelectWidget({
disabled={disabled}
readOnly={readonly}
autoFocus={autofocus}
onBlur={onBlur && (event => {
const newValue = getValue(event, multiple);
onBlur(id, processValue(schema, newValue));
})}
onChange={(event) => {
let newValue;
if (multiple) {
newValue = [].filter.call(
event.target.options, o => o.selected).map(o => o.value);
} else {
newValue = event.target.value;
}
const newValue = getValue(event, multiple);
onChange(processValue(schema, newValue));
}}>{
enumOptions.map(({value, label}, i) => {
Expand All @@ -74,6 +82,7 @@ if (process.env.NODE_ENV !== "production") {
multiple: PropTypes.bool,
autofocus: PropTypes.bool,
onChange: PropTypes.func,
onBlur: PropTypes.func,
};
}

Expand Down
5 changes: 4 additions & 1 deletion src/components/widgets/TextareaWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ function TextareaWidget({
disabled,
readonly,
autofocus,
onChange
onChange,
onBlur
}) {
const _onChange = ({target: {value}}) => {
return onChange(value === "" ? undefined : value);
Expand All @@ -25,6 +26,7 @@ function TextareaWidget({
disabled={disabled}
readOnly={readonly}
autoFocus={autofocus}
onBlur={onBlur && (event => onBlur(id, event.target.value))}
onChange={_onChange} />
);
}
Expand All @@ -42,6 +44,7 @@ if (process.env.NODE_ENV !== "production") {
required: PropTypes.bool,
autofocus: PropTypes.bool,
onChange: PropTypes.func,
onBlur: PropTypes.func,
};
}

Expand Down
16 changes: 16 additions & 0 deletions test/ArrayField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,22 @@ describe("ArrayField", () => {
expect(comp.state.formData).eql(["foo", "bar"]);
});

it("should handle a blur event", () => {
const onBlur = sandbox.spy();
const {node} = createFormComponent({schema, onBlur});

const select = node.querySelector(".field select");
Simulate.blur(select, {
target: {options: [
{selected: true, value: "foo"},
{selected: true, value: "bar"},
{selected: false, value: "fuzz"},
]}
});

expect(onBlur.calledWith(select.id, ["foo", "bar"])).to.be.true;
});

it("should fill field with data", () => {
const {node} = createFormComponent({schema, formData: ["foo", "bar"]});

Expand Down
Loading