3. Convert Fields to Functions
In this step we'll be converting our form fields into functions.
You can view the finished code for this step by checking out the create.3
branch of the completed project.
What's the problem?
To illustrate the issue, take a look at the text field for the form, show below:
<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>
Most of this form field will be identical across all forms in the application, which includes the classes applied, where the error message appears, even the callback functions like onChange
and onBlur()
.
In fact, there is very little that's unique about this specific text field aside from the field name, the label and the placeholder text.
How do we solve this?
To solve this, we're going to convert out form fields into functions, and only provide the information that is truly unique to that field.
We're also going to store these functions in our application configuration, under config/dialogs.js
.
Create Config for Dialogs
Start by creating a new config file called dialogs.js
located at config/dialogs.js
, and paste the code below into that file:
import React from 'react';
import _ from 'lodash';
import { Connect } from 'lore-hook-connect';
import { Field } from 'lore-react-forms';
export default {
fieldMap: {
text: function(form, props, name) {
const {
label,
placeholder
} = props;
return (
<Field key={name} name={name} data={form.data} errors={form.errors} onChange={form.onChange}>
{(field) => {
return (
<div key={name} className={`form-group ${field.touched && field.error ? 'has-error' : ''}`}>
<label>
{label}
</label>
<textarea
className="form-control"
rows="3"
value={field.value}
placeholder={placeholder}
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>
);
},
select: function(form, props, name) {
const {
options,
label,
optionLabel
} = props;
return (
<Field key={name} name={name} data={form.data} errors={form.errors} onChange={form.onChange}>
{(field) => {
return (
<div className={`form-group ${field.touched && field.error ? 'has-error' : ''}`}>
<label>
{label}
</label>
<Connect callback={(getState, props) => {
return {
options: _.isFunction(options) ? options(getState, props) : options
};
}}>
{(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}>
{_.isFunction(optionLabel) ? optionLabel(datum) : datum.data[optionLabel]}
</option>
);
}))}
</select>
)
}}
</Connect>
{field.touched && field.error ? (
<span className="help-block">
{field.error}
</span>
) : null}
</div>
);
}}
</Field>
);
}
}
}
The code above defines a fieldMap
, which is an map that, when given a name like text
or select
will return a function that can generate that type of field.
Each function has an identical signature. The first argument is the form
, the second is the props
unique to that field (like label and placeholder text) and the third is the name
of the field.
Why functions?
You could argue that instead of creating a function, we could just create a component, and you'd be right. But as you'll see in later steps, the ultimate goal is to actually create an interface that allows us to describe what kind of form we need, instead of constructing it ourselves. And for that, we need to use functions instead of components.
Refactor Fields to Functions
With our fieldMap
created, let's use it in our form to replace our current fields. Update the render()
function to look like this:
render() {
const { data } = this.state;
const validators = this.getValidators(data);
const { fieldMap } = lore.config.dialogs;
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">
{fieldMap['text'](form, {
label: 'Message',
placeholder: "What's happening?"
}, 'text')}
</div>
</div>
<div className="row">
<div className="col-md-12">
{fieldMap['select'](form, {
label: 'User',
options: function(getState) {
return getState('user.find');
},
optionLabel: 'nickname'
}, 'userId')}
</div>
</div>
</div>
<div className="modal-footer">
...
</div>
</PropBarrier>
)}
</Form>
</div>
</div>
);
}
Review
With these changes in place, we've removed ~60 lines of code from our form, and created an interface that makes is much more obvious what is unique about each field.
Visual Check-in
If everything went well, clicking the create button will now launch a dialog that you can use to create a new tweet.
Code Changes
Below is a list of files modified during this step.
config/dialogs.js
import React from 'react';
import _ from 'lodash';
import { Connect } from 'lore-hook-connect';
import { Field } from 'lore-react-forms';
export default {
fieldMap: {
text: function(form, props, name) {
const {
label,
placeholder
} = props;
return (
<Field key={name} name={name} data={form.data} errors={form.errors} onChange={form.onChange}>
{(field) => {
return (
<div key={name} className={`form-group ${field.touched && field.error ? 'has-error' : ''}`}>
<label>
{label}
</label>
<textarea
className="form-control"
rows="3"
value={field.value}
placeholder={placeholder}
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>
);
},
select: function(form, props, name) {
const {
options,
label,
optionLabel
} = props;
return (
<Field key={name} name={name} data={form.data} errors={form.errors} onChange={form.onChange}>
{(field) => {
return (
<div className={`form-group ${field.touched && field.error ? 'has-error' : ''}`}>
<label>
{label}
</label>
<Connect callback={(getState, props) => {
return {
options: _.isFunction(options) ? options(getState, props) : options
};
}}>
{(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}>
{_.isFunction(optionLabel) ? optionLabel(datum) : datum.data[optionLabel]}
</option>
);
}))}
</select>
)
}}
</Connect>
{field.touched && field.error ? (
<span className="help-block">
{field.error}
</span>
) : null}
</div>
);
}}
</Field>
);
}
}
}
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 { Form, 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);
const { fieldMap } = lore.config.dialogs;
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">
{fieldMap['text'](form, {
label: 'Message',
placeholder: "What's happening?"
}, 'text')}
</div>
</div>
<div className="row">
<div className="col-md-12">
{fieldMap['select'](form, {
label: 'User',
options: function(getState) {
return getState('user.find');
},
optionLabel: 'nickname'
}, 'userId')}
</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 actions to functions.