1. Add Create Dialog
In this step we'll be added a dialog that we can use to create new tweets. This dialog will allow you to enter the text for a tweet, as well as specify the user who said it.
Add Dialog
To start, create a new file called index.js
located at src/dialogs/tweet/create/index.js
. Paste the code below into this file.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { Connect } from 'lore-hook-connect';
export default createReactClass({
displayName: 'CreateTweetDialog',
propTypes: {
onCancel: PropTypes.func
},
getInitialState() {
return {
data: {
text: '',
userId: undefined
},
touched: {
text: false,
userId: false
}
};
},
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;
},
onBlur: function(field) {
const touched = this.state.touched;
touched[field] = true;
this.setState({
touched: touched
});
},
render() {
const { data, touched } = 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">
<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>
</div>
</div>
<div className="row">
<div className="col-md-12">
<div className={`form-group ${touched['userId'] && errors['userId'] ? 'has-error' : ''}`}>
<label>
User
</label>
<Connect callback={(getState, props) => {
return {
options: getState('user.find')
};
}}>
{(connect) => {
return (
<select
className="form-control"
value={data.userId}
onChange={(event) => {
const value = event.target.value;
this.onBlur('userId');
this.onChange('userId', 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>
{touched['userId'] && errors['userId'] ? (
<span className="help-block">
{errors['userId']}
</span>
) : null}
</div>
</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>
);
}
});
Review Dialog
Let's take a minute to review the form we just added, since we'll be making a series of modifications to simplify it over the next several steps.
Overall, the form is pretty typical. It has two fields; a text field for capturing the message, and a dropdown field for specifying which user said it. The form also has two actions; a cancel button which will dismiss the dialog, and a submit button which will trigger an API call to the create the tweet. Additionally, there is also some basic validation that marks both fields as required, and the submit button will only be enabled once the form is valid.
To provide the functionality just described, the form also defines several internal functions, described below:
getInitialState()
which stores the initial values for the formrequest()
which triggers the API call to create the tweetonSubmit()
which invokes the API request and dismisses the dialogdismiss()
which dismisses the dialogonChange()
which updates the state of the form as the user makes changesgetValidators()
which stores the validation logic for the formgetErrors()
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 errorsonBlur()
which is a callback used to keep track of which fields the user has interacted with
The Goal
All-in-all, this form is pretty basic, and comes in at 210 lines long. The length isn't really a problem, but it's a little concerning how much of the code doesn't provide unique value. There's a lot of boilerplate that would need to exist in every form, and that can lead to unintentional bugs developed by creating new forms by copy/pasting older forms, and also creating a situation where changes to one form (such as the design or behavior) need to then be made to all forms.
Over the next several steps, we're going to be aggressively "trimming the boilerplate", and attempting to reduce the form to only the code that is essential and unique to that specific form.
Launch Create Dialog
Next, open the CreateButton
component located at src/components/CreateButton.js
and modify the onClick()
callback to look like this:
...
import CreateTweetDialog from '../dialogs/tweet/create';
...
onClick() {
lore.dialog.show(function() {
return (
<CreateTweetDialog />
);
})
},
...
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.
src/components/CreateButton.js
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import CreateTweetDialog from '../dialogs/tweet/create/index.8';
export default createReactClass({
displayName: 'CreateButton',
onClick() {
lore.dialog.show(function() {
return (
<CreateTweetDialog />
);
})
},
render() {
return (
<button
type="button"
className="btn btn-primary btn-lg create-button"
onClick={this.onClick}>
+
</button>
);
}
});
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';
export default createReactClass({
displayName: 'CreateTweetDialog',
propTypes: {
onCancel: PropTypes.func
},
getInitialState() {
return {
data: {
text: '',
userId: undefined
},
touched: {
text: false,
userId: false
}
};
},
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;
},
onBlur: function(field) {
const touched = this.state.touched;
touched[field] = true;
this.setState({
touched: touched
});
},
render() {
const { data, touched } = 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">
<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>
</div>
</div>
<div className="row">
<div className="col-md-12">
<div className={`form-group ${touched['userId'] && errors['userId'] ? 'has-error' : ''}`}>
<label>
User
</label>
<Connect callback={(getState, props) => {
return {
options: getState('user.find')
};
}}>
{(connect) => {
return (
<select
className="form-control"
value={data.userId}
onChange={(event) => {
const value = event.target.value;
this.onBlur('userId');
this.onChange('userId', 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>
{touched['userId'] && errors['userId'] ? (
<span className="help-block">
{errors['userId']}
</span>
) : null}
</div>
</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 simplify the fields.