1. Refactor Fields
In this step we'll make small change to refactor our fields, and eliminate the onBlur
callback from our forms.
You can view the finished code for this step by checking out the create.1
branch of the completed project.
What's the problem?
Take a look at the code below, which reflects the field to capture text input:
<div className={`form-group ${touched['text'] && errors['text'] ? 'has-error' : ''}`}>
<label>
Message
</label>
<textarea
className="form-control"
rows="3"
value={data.text}
placeholder="What's happening?"
onChange={(event) => {
this.onChange('text', event.target.value)
}}
onBlur={() => {
this.onBlur('text');
}}
/>
{touched['text'] && errors['text'] ? (
<span className="help-block">
{errors['text']}
</span>
) : null}
</div>
One concern with this code is the number of places where the name of the field appears, which is text
in this example. For example:
- touched['text'] && errors['text']
- data.text
- this.onChange('text', event.target.value)
- this.onBlur('text')
- touched['text'] && errors['text']
- errors['text']
In this case, there are 8 places where text
appears, and it's not hard to picture a scenario where a bug is accidentally introduced just by mistyping the name, or forgetting to add or update one of those locations.
How do we solve it?
In this step, we're going to solve that by introducing a component called Field
that is part of the lore-react-forms
package. After introducing this component, we'll only need to specify the name of the field once, and reduce the risk of copy/paste errors.
You can learn more about the Field
component here.
Import Field
Start by importing Field
from lore-react-forms
.
import { Field } from 'lore-react-forms';
...
Then refactor the text
field to look like this:
<div className="row">
<div className="col-md-12">
<Field name="text" data={data} errors={errors} onChange={this.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>
And then refactor the userId
field to look like this:
<div className="row">
<div className="col-md-12">
<Field name="userId" data={data} errors={errors} onChange={this.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>
Review
This wasn't a big change, but it does provide a few benefits.
For starters, we no longer need to use the name of the field to access things like the value of the field, which simplifies some of the code:
touched['text'] && errors['text']
becomes field.touched && field.error
data.text
becomes field.value
this.onChange('text', event.target.value)
becomes field.onChange(event, event.target.value)
this.onBlur('text')
becomes field.onBlur
errors['text']
becomes field.error
Additionally, the Field
is now keeping track of whether the component has been touched or not, which means we no longer need to keep track of touched
in the forms state, and can also delete the onBlur()
callback.
So now, instead of referencing the name of the field, we only need to tell the Field
what it's called, and the component will automatically map data
into value
and errors
into error
.
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 { Field } 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'
}
}]
};
},
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-header">
<button type="button" className="close" onClick={this.dismiss}>
<span>×</span>
</button>
<h4 className="modal-title">
Create Tweet
</h4>
</div>
<div className="modal-body">
<div className="row">
<div className="col-md-12">
<Field name="text" data={data} errors={errors} onChange={this.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={data} errors={errors} onChange={this.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={this.dismiss}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
disabled={hasError}
onClick={this.onSubmit}
>
Create
</button>
</div>
</div>
</div>
</div>
</div>
);
}
});
Next Steps
Next we're going to refactor the form.