Skip to content

Latest commit

 

History

History
 
 

06-submit-email-form

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Step 6 - Submit email form

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.

Restart Setup

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.

Tasks

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 call render() like calling setState 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 the emails array, but instead making a copy of emails and inserting newEmail at the beginning of it all via the spread operator. We never want to mutate state or the objects within it. Any time we need to update objects within state we use setState and make copies of the objects before changing them.

NOTE: The setState above differs from the ones we used to update this.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 of setState 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").

Exercises

  • Emails added via EmailForm do not have a date to display in EmailView. Update _handleFormSubmit to also pass along the current date/time as a string in the date property

Next

Go to Step 7 - Delete email.

Resources

Questions

Got questions? Need further clarification? Feel free to post a question in Ben Ilegbodu's AMA!