2. Refactor Form
In this step we'll make small change to refactor our form, and eliminate the getErrors()
and hasError()
callbacks from our form.
You can view the finished code for this step by checking out the create.2
branch of the completed project.
What's the problem?
Take a look at the code below, which reflects the essence of the form, where we have a body that renders some fields, and a footer that renders some actions:
...
getErrors: function(validatorDictionary, data) {
return _.mapValues(data, function(value, key) {
const validators = validatorDictionary[key];
let error = null;
if (validators) {
validators.forEach(function(validator) {
error = error || validator(value);
});
}
return error;
});
},
hasError: function(errors) {
const errorCount = _.reduce(errors, function(result, value, key) {
if (value) {
return result + 1;
}
return result;
}, 0);
return errorCount > 0;
},
render() {
const { data } = this.state;
const validators = this.getValidators(data);
const errors = this.getErrors(validators, data);
const hasError = this.hasError(errors);
return (
<div className="modal-dialog">
<div className="modal-content">
...
<div className="modal-body">
<div className="row">
<div className="col-md-12">
<Field name="text" data={data} errors={errors} onChange={this.onChange}>
...
</Field>
</div>
</div>
<div className="row">
<div className="col-md-12">
<Field name="userId" data={data} errors={errors} onChange={this.onChange}>
...
</Field>
</div>
</div>
</div>
<div className="modal-footer">
<div className="row">
<div className="col-md-12">
<button
type="button"
className="btn btn-default"
onClick={this.dismiss}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
disabled={hasError}
onClick={this.onSubmit}
>
Create
</button>
</div>
</div>
</div>
</div>
</div>
);
}
...
Once concern with this code is the very generic nature of the functions below:
getErrors()
which calculates errors for each fields based on what the user has enteredhasError()
which is a convenience function to generate a simple boolean as to whether the form has any errors
The reason we don't like these functions is because every form will need them, and they're unlikely to ever change.
How do we solve this?
One approach to solving this problem might be to convert them into utility functions, and simply import them into all the forms that use them. This would certainly reduce the problem, and eliminate ~20 lines of code.
But we're going to use an alternate approach, and import a Form
component from the lore-react-forms
package. This component will eliminate the need to define those functions at all, and also provide some added benefits that we'll see unfold in later steps.
You can learn more about the Form
component here.
Import Form
Start by importing Form
from lore-react-forms
.
import { Field, Form, PropBarrier } from 'lore-react-forms';
...
Then refactor the form code to look like this:
render() {
const { data } = this.state;
const validators = this.getValidators(data);
return (
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
...
</div>
<Form
data={data}
validators={validators}
onChange={this.onChange}
callbacks={{
onSubmit: this.onSubmit,
dismiss: this.dismiss
}}
>
{(form) => (
<PropBarrier>
<div className="modal-body">
<div className="row">
<div className="col-md-12">
<Field name="text" data={form.data} errors={form.errors} onChange={form.onChange}>
...
</Field>
</div>
</div>
<div className="row">
<div className="col-md-12">
<Field name="userId" data={form.data} errors={form.errors} onChange={form.onChange}>
...
</Field>
</div>
</div>
</div>
<div className="modal-footer">
<div className="row">
<div className="col-md-12">
<button
type="button"
className="btn btn-default"
onClick={form.callbacks.dismiss}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
disabled={form.hasError}
onClick={form.callbacks.onSubmit}
>
Create
</button>
</div>
</div>
</div>
</PropBarrier>
)}
</Form>
</div>
</div>
);
}
Review
This wasn't a big change, but it does provide a few benefits.
For starters, we no longer need to calculate errors
or hasError
as the Form
component does it for us, and makes those values available via form.errors
and form.hasError
. This means you can now delete the getErrors()
and hasError()
callbacks.
This also means some of the values passed to Field
are now obtained from form
, such as:
data
becomes form.data
errors
becomes form.errors
this.onChange
becomes form.onChange
this.dismiss
becomes form.callbacks.dismiss
this.onSubmit
becomes form.callbacks.onSubmit
Visual Check-in
If everything went well, clicking the create button will still launch a dialog that you can use to create a new tweet.

Code Changes
Below is a list of files modified during this step.
src/dialogs/tweet/create/index.js
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { Connect } from 'lore-hook-connect';
import { Form, Field, PropBarrier } from 'lore-react-forms';
export default createReactClass({
displayName: 'CreateTweetDialog',
propTypes: {
onCancel: PropTypes.func
},
getInitialState() {
return {
data: {
text: '',
userId: undefined
}
};
},
request(data) {
lore.actions.tweet.create(data);
},
onSubmit() {
const { data } = this.state;
this.request(data);
this.dismiss();
},
dismiss() {
this.props.onCancel();
},
onChange(name, value) {
const nextData = _.merge({}, this.state.data);
nextData[name] = value;
this.setState({
data: nextData
});
},
getValidators: function(data) {
return {
text: [function(value) {
if (!value) {
return 'This field is required';
}
}],
userId: [function(value) {
if (value === undefined) {
return 'This field is required'
}
}]
};
},
render() {
const { data } = this.state;
const validators = this.getValidators(data);
return (
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={this.dismiss}>
<span>×</span>
</button>
<h4 className="modal-title">
Create Tweet
</h4>
</div>
<Form
data={data}
validators={validators}
onChange={this.onChange}
callbacks={{
onSubmit: this.onSubmit,
dismiss: this.dismiss
}}
>
{(form) => (
<PropBarrier>
<div className="modal-body">
<div className="row">
<div className="col-md-12">
<Field name="text" data={form.data} errors={form.errors} onChange={form.onChange}>
{(field) => {
return (
<div className={`form-group ${field.touched && field.error ? 'has-error' : ''}`}>
<label>
Message
</label>
<textarea
className="form-control"
rows="3"
value={field.value}
placeholder="What's happening?"
onChange={(event) => {
field.onChange(event, event.target.value)
}}
onBlur={field.onBlur}
/>
{field.touched && field.error ? (
<span className="help-block">
{field.error}
</span>
) : null}
</div>
)
}}
</Field>
</div>
</div>
<div className="row">
<div className="col-md-12">
<Field name="userId" data={form.data} errors={form.errors} onChange={form.onChange}>
{(field) => {
return (
<div className={`form-group ${field.touched && field.error ? 'has-error' : ''}`}>
<label>
User
</label>
<Connect callback={(getState, props) => {
return {
options: getState('user.find')
};
}}>
{(connect) => {
return (
<select
className="form-control"
value={field.value}
onChange={(event) => {
const value = event.target.value;
field.onBlur();
field.onChange(event, value ? Number(value) : undefined)
}}
>
{[<option key="" value=""/>].concat(connect.options.data.map((datum) => {
return (
<option key={datum.id} value={datum.id}>
{datum.data.nickname}
</option>
);
}))}
</select>
)
}}
</Connect>
{field.touched && field.error ? (
<span className="help-block">
{field.error}
</span>
) : null}
</div>
);
}}
</Field>
</div>
</div>
</div>
<div className="modal-footer">
<div className="row">
<div className="col-md-12">
<button
type="button"
className="btn btn-default"
onClick={form.callbacks.dismiss}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
disabled={form.hasError}
onClick={form.callbacks.onSubmit}
>
Create
</button>
</div>
</div>
</div>
</PropBarrier>
)}
</Form>
</div>
</div>
);
}
});
Next Steps
Next we're going to convert the fields to functions.