The goal of this step is to "send" the email and have it added to the email list by actually submitting the form.
As always, if you run into trouble with the tasks or exercises, you can take a peek at the final source code.
If you didn't successfully complete the previous step, you can jump right in by copying the step and installing the dependencies.
Ensure you're in the root folder of the repo:
cd react-workshop
Remove the existing workshop directory if you had previously started elsewhere:
rm -rf workshop
Copy the previous step as a starting point:
cp -r 05-email-form workshop
Change into the workshop
directory:
cd workshop
Install all of the dependencies (yarn
is preferred):
# Yarn
yarn
# ...or NPM
npm install
Start the app:
# Yarn
yarn start
# ...or NPM
npm start
After the app is initially built, a new browser window should open up at http://localhost:3000/, and you should be able to continue on with the tasks below.
Add a submit button and an onSubmit
handler to the <form>
inside EmailForm
:
export default class EmailForm extends PureComponent {
// initialize state
// other helper methods
_handleSubmit(e) {
e.preventDefault();
let {from, to, subject, message} = this.state;
console.log('submitting', {from, to, subject, message});
}
render() {
let {from, to, subject, message} = this.state;
return (
<form className="email-form" onSubmit={this._handleSubmit.bind(this)}>
{/* from, to, subject & message fields */}
<footer>
<button type="submit">Send email</button>
</footer>
</form>
);
}
}
Add a required onSubmit
prop to EmailForm
and call it within _handleSubmit
when all the fields are filled:
export default class EmailForm extends PureComponent {
static propTypes = {
onSubmit: PropTypes.func.isRequired
}
// initialize state
// other helper methods
_handleSubmit(e) {
e.preventDefault();
let {from, to, subject, message} = this.state;
// super simple validation
if (from && to && subject && message) {
// call handler with email info
this.props.onSubmit({from, to, subject, message});
} else {
alert('fill out the form!');
}
}
}
Now when App
handles onSubmit
of EmailForm
it will receive the form field values as a convient object instead of having to mess around with an event
and having to pull data out of that. EmailForm
takes care of dealing with the DOM (i.e. e.preventDefault()
) so that the interface between it and its parent is clean.
After the form is submitted, also reset the form fields so that it's easy to send a new email:
const DEFAULT_FORM_VALUES = {
from: '',
to: '[email protected]',
subject: '',
message: ''
};
export default class EmailForm extends PureComponent {
// prop types
state = DEFAULT_FORM_VALUES
// other helper methods
_handleSubmit(e) {
e.preventDefault();
let {from, to, subject, message} = this.state;
// super simple validation
if (from && to && subject && message) {
// call handler with email info
this.props.onSubmit({from, to, subject, message});
// reset the form to initial values
this.setState(DEFAULT_FORM_VALUES);
} else {
alert('fill out the form!');
}
}
}
In the top-level App
component, add a handler to <EmailForm />
for its onSubmit
prop and call it _handleFormSubmit
. Just log the the new email to the console:
export default class App extends PureComponent {
// initialize state
// lifecycle methods
// other helper methods
_handleFormSubmit(newEmail) {
// logging the email info generated by email form
console.log(newEmail);
}
render() {
let {selectedEmailId} = this.state;
let selectedEmail = EMAILS.find(email => email.id === selectedEmailId);
let emailViewComponent;
if (selectedEmail) {
emailViewComponent = (
<EmailView
email={selectedEmail}
onClose={this._handleEmailViewClose.bind(this)}
/>
);
}
return (
<main className="app">
<EmailList
emails={EMAILS}
onItemSelect={this._handleItemSelect.bind(this)}
/>
{emailViewComponent}
<EmailForm onSubmit={this._handleFormSubmit.bind(this)} />
</main>
);
}
}
We would like for the newly added email to show up in the email list, but <EmailList />
is rendering EMAILS
, a constant list of emails. In order to be able to add newly created emails to the emal list, we need to maintain an array of emails in state
. Create a new state property called emails
and initialize it to the EMAILS
constant:
const EMAILS = [
...
];
export default class App extends PureComponent {
state = {
// Initialize emails state to the `EMAILS` constant
emails: EMAILS,
// Initialize selected email ID to -1, indicating nothing is selected.
// When an email is selected in EmailList, this will be updated to
// corresponding ID
selectedEmailId: -1
}
// helper methods
// render()
}
NOTE: Your first thought might be to update the
EMAILS
constant, but updating it will not cause React to callrender()
like callingsetState
does.
Now within render()
, instead of rendering from EMAILS
, we'll render from this.state.emails
:
const EMAILS = [
...
];
export default class App extends PureComponent {
state = {
// Initialize emails state to the `EMAILS` constant
emails: EMAILS,
// Initialize selected email ID to -1, indicating nothing is selected.
// When an email is selected in EmailList, this will be updated to
// corresponding ID
selectedEmailId: -1
}
// helper methods
render() {
// Also pull `emails` out from state
let {emails, selectedEmailId} = this.state;
let selectedEmail = emails.find(email => email.id === selectedEmailId);
let emailViewComponent;
if (selectedEmail) {
emailViewComponent = (
<EmailView
email={selectedEmail}
onClose={this._handleEmailViewClose.bind(this)}
/>
);
}
return (
<main className="app">
<EmailList
emails={emails}
onItemSelect={this._handleItemSelect.bind(this)}
/>
{emailViewComponent}
<EmailForm onSubmit={this._handleFormSubmit.bind(this)} />
</main>
);
}
}
Finally, back in _handleFormSubmit
, change the console logging to update this.state.emails
by prepending the newEmail
to it, adding id
to it:
export default class App extends PureComponent {
state = {
// Initialize emails state to the `EMAILS` constant
emails: EMAILS,
// Initialize selected email ID to -1, indicating nothing is selected.
// When an email is selected in EmailList, this will be updated to
// corresponding ID
selectedEmailId: -1
}
// other helper methods
_handleFormSubmit(newEmail) {
this.setState(({emails}) => {
// Create a full email info by spreading in `id`
// Then spread to front of emails state (since it's the newest)
let newEmails = [
{
...newEmail,
id: Date.now(),
},
...emails
];
// Set state with new updated emails list
return {emails: newEmails};
});
}
// render
NOTE: You will notice that we're not calling
.push()
on theemails
array, but instead making a copy ofemails
and insertingnewEmail
at the beginning of it all via the spread operator. We never want to mutatestate
or the objects within it. Any time we need to update objects withinstate
we usesetState
and make copies of the objects before changing them.
NOTE: The
setState
above differs from the ones we used to updatethis.state.selectedEmailId
. Here we're using the "updater function" version. It takes a function that's passed the current version of the entire state and is expected to return new versions of whatever state needs to be updated. You need to use the "updater function" version ofsetState
whenever the new state depends on the current state. We're appending a new email to the current emails list in order to return a new emails list.
You should now see the email show up at the top of the list when you add it. You should also be able to click it and view its details. Using the React Developer Tools, watch how the new email item is optimally added to the list. Nothing else in the UI is updated thanks to the reconciler (aka "Virtual DOM").
- Emails added via
EmailForm
do not have a date to display inEmailView
. Update_handleFormSubmit
to also pass along the current date/time as a string in thedate
property
Go to Step 7 - Delete email.
Got questions? Need further clarification? Feel free to post a question in Ben Ilegbodu's AMA!