Skip to content

Instantly share code, notes, and snippets.

@leahgarrett
Last active June 27, 2019 00:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leahgarrett/ccadbea440c85bc48be6408606c3f338 to your computer and use it in GitHub Desktop.
Save leahgarrett/ccadbea440c85bc48be6408606c3f338 to your computer and use it in GitHub Desktop.
Forms with own AP

Forms with own API

Today we will create a full stack MERN app. The app will display books.

The back-end

The back-end we are using has already been written (thanks Anhar!) https://github.com/anharathoi/books-backend-mern

You will need to clone the back-end
git clone https://github.com/anharathoi/books-backend-mern.git

Change into the directory
cd books-backend-mern

Install dependencies
npm install

Run the server
npm start

The back-end is running on port 5000. Our front-end will run on port 3000.

To seed the data
http://localhost:5000/seed

To view the data
http://localhost:5000/books


Create a React app for the front-end

Create the client app
npx create-react-app books-front-end

Change into the app folder
cd books-front-end

Install dependencies
npm install

Run the app
npm start

See a spinning logo?

Back at terminal lets open Visual Studio Code
code .


Loading Books

First lets convert the App component to a class component and add a state variable to store our books.

/src/App.js

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      books: []
    };
  }

  render() {
    console.log('App is running render');
    return (
      <div>
        <h1>Books {this.state.books.length}</h1>
      </div>
    );
  }
}

We want to load the books when the component mounts. We may also load it after we make updates, so we will create loadBooks as a separate function we can call elsewhere.

/src/App.js

  loadBooks = () => {
    console.log('loading books')
    const url = 'http://localhost:5000/books';
    fetch(url)
    .then(resp => resp.json())
    .then(data => {
      console.log(data)
      this.setState({ books: data, adding: false });
    })
  }

  componentDidMount() {
    console.log('App is running componentDidMount');
    this.loadBooks();
  }

The heading should now display the number of books.

Now we have the books loading let's display them. We will create a separate method for displaying the books.

/src/App.js

bookList = () => {
    return (
      <div className="book-list">
          {this.state.books.map((item, index) => (
          <div >
            {item.genre}: {item.title} by {item.author} (rrp ${item.price}) (id: {item.id})
          </div>
        ))}
        </div>
    )
  }
  render() {
    console.log('App is running render');
    return (
      <div>
        <h1>Books {this.state.books.length}</h1>
        {this.bookList()}
      </div>
    );
  }

Adding books - App.js preparation

To add books we will first wire up displaying the a form

  • add a button
  • add a handler for the button
  • add a variable to state for adding
  • display the form when adding

Adding the button and handler:

/src/App.js

  handleClick = () => {
    this.setState({ adding: true });
  }
  render() {
    console.log('App is running render');
    return (
      <div>
        <h1>Books {this.state.books.length}</h1>
        <button onClick={this.handleClick}>Add</button>
        {this.bookList()}
      </div>
    );
  }

Adding the adding variable to state:

/src/App.js

  constructor(props) {
    super(props);
    this.state = { 
      books: [],
      adding: false
    };
  }

In the render method we will conditionally display the BookForm.

/src/App.js

  render() {
    console.log('App is running render');
    return (
      <div>
        <h1>Books {this.state.books.length}</h1>
        <button onClick={this.handleClick}>Add</button>
         {this.state.adding ? 
          <BookForm /> : 
          this.bookList()
         }
      </div>
    );
  }

Adding Books - setting up the form

Create new files for the BookForm and styling.

We will use the simple form styling from our wish-list project.

/src/components/BookForm.css

.book-form {
    margin-left: 20px;
}
form {
    font-size: 16px;
    max-width: 500px;

}
input {
    margin: 5px 0 25px 0;
    display: block;
    width: 100%;
    font-size: 16px;
  }
  
label {
    display: block;
    width: 100%;
  }
  
  button {
    width: 120px;
    padding: 10px 30px;
  }

/src/components/BookForm.js

import React, { Component } from 'react';
import './BookForm.css';

class BookForm extends Component {
  constructor(props) {
    super(props);
    this.state = { 
        title: '',
        author: '',
        genre: '',
        price: 0
    };
  }

  handleClick = (e) => {
    e.preventDefault();
    const newBook = {
        title: this.state.title, 
        author: this.state.author, 
        genre: this.state.genre, 
        price: this.state.price 
    }
    this.props.addNewBook(newBook)
  }

  handleChange = (e) => {
    this.setState({ [e.target.id]: e.target.value})
  }

  render(){
    console.log(this.state);
      return (
        <div className="book-form">
          <h1>Book Form</h1>
          <form>
            <label htmlFor="title">Title</label>
              <input onChange={this.handleChange} type="text" id="title" placeholder="title" />
              <label htmlFor="author">Author</label>
              <input onChange={this.handleChange} type="text" id="author" placeholder="author" />
              <label htmlFor="genre">Genre</label>
              <input onChange={this.handleChange} type="text" id="genre" placeholder="genre" />
              <label htmlFor="price">Price</label>
              <input onChange={this.handleChange} type="number" id="price" placeholder="price" />

              <button onClick={this.handleClick}>Save</button>
              <button onClick={this.handleCancel}>Cancel</button>
          </form>
        </div>
      );
    }
  }

export default BookForm;

handleCancel is not implemented but appears to be working? We will implement this for the challenge.


Adding books - saving the book in the App component

Lets wire up the 'addNewBook' method.

/src/App.js

  addNewBookHandler = (newBook) => {
    console.log(newBook);
    this.setState({ adding: false });
  }
  render() {
    console.log('App is running render');
    return (
      <div>
        <h1>Books {this.state.books.length}</h1>
        <button onClick={this.handleClick}>Add</button>
         {this.state.adding ? 
          <BookForm addNewBook={this.addNewBookHandler} /> : 
          this.bookList()
         }
      </div>
    );
  }

Update the addNewBookHandler to create a book id so we have a complete object to save.

/src/App.js

  addNewBookHandler = (newBook) => {
    const newId = this.state.books.length + 1;
    newBook.id = newId;
    console.log(newBook);
    this.setState({ adding: false });
  }

Now we have a the information we need to create new book we can ask our back end to create it. We will use a method for this. We will remove the setState from here as we will do it later. We do not want to setState more than necessary.

/src/App.js

  addNewBookHandler = (newBook) => {
    const newId = this.state.books.length + 1;
    newBook.id = newId;
    console.log(newBook);
    this.createBook(newBook);
  }

To create the book we will use a 'POST'. Once the 'POST' is complete we will use our loadBooks method to update the book list. loadBooks re-loads the books from the database and calls setState which will reload the page and hide the form.

/src/App.js

  createBook = (book) => {
    const url = 'http://localhost:5000/books/newbook';
    const data = JSON.stringify(book);
    console.log(data)
      return fetch(url, {
          method: 'POST', 
          headers: {
              'Content-Type': 'application/json',
              'Accept': 'application/json'
          },
          body: data, 
      })
      .then(response => {
          this.loadBooks();
        }
      ) 
  }

Editing books - App.js preparation

To edit a book we need to be able select a book to edit. We will add a click event to the book list items and a handler for the click event. The click event handler will use a state variable to switch to editing the selected book.

/src/App.js

  handleEditClick(book) {
    console.log(book)
    this.setState({ selectedBook: book })
  }

  bookList = () => {
    return (
      <div className="book-list">
          {this.state.books.map((item, index) => (
          <div onClick={() => this.handleEditClick(item)} key={index}>
            {item.genre}: {item.title} by {item.author} (rrp ${item.price}) (id: {item.id})
          </div>
        ))}
        </div>
    )
  }

We need to add this state variable to the constructor.

/src/App.js

  constructor(props) {
    super(props);
    this.state = { 
      books: [],
      adding: false,
      selectedBook: null
    };
  }

In the render method we will conditionally display the BookForm for editing. When editing we will pass the values for the selected book into the form for editing:

/src/App.js

  render() {
    console.log('App is running render');
    return (
      <div>
        <h1>Books {this.state.books.length}</h1>
        <button onClick={this.handleClick}>Add</button>
        {this.state.adding ? 
         <BookForm addNewBook={this.addNewBookHandler} /> : 
          this.state.selectedBook ? 
            <BookForm 
                addNewBook={this.addNewBookHandler} 
                title={this.state.selectedBook.title} 
                author={this.state.selectedBook.author} 
                genre={this.state.selectedBook.genre} 
                price={this.state.selectedBook.price} 
                id={this.state.selectedBook.id} 
            /> : 
            this.bookList()
        }
      </div>
    );
  }

Editing books - BookForm setup

The values have been passed into the BookForm via props, now we need to read them out. We will read them out in the constructor. This will initialize our Book values to the selectedBook.

/src/components/BookForm.js

  constructor(props) {
    super(props);
    this.state = { 
        title: props.title || '',
        author: props.author || '',
        genre: props.genre || '',
        price: props.price || 0,
        id: props.id || 0
    };
  }

Update the render method to initialize the input fields with the values from state.
value={this.state.title}

Here I have converted to a multiline layout for the input fields because it was getting hard to read on one line.

/src/components/BookForm.js

  render(){
    console.log(this.state);
      return (
        <div className="book-form">
          <h1>Book Form</h1>
          <form>
            <label htmlFor="title">Title</label>
              <input 
                onChange={this.handleChange} 
                type="text" 
                id="title" 
                placeholder="title" 
                value={this.state.title} 
              />
              <label htmlFor="author">Author</label>
              <input 
                onChange={this.handleChange} 
                type="text" 
                id="author" 
                placeholder="author" 
                value={this.state.author} 
              />
              <label htmlFor="genre">Genre</label>
              <input 
                onChange={this.handleChange} 
                type="text" 
                id="genre" 
                placeholder="genre" 
                value={this.state.genre} 
              />
              <label htmlFor="price">Price</label>
              <input 
                onChange={this.handleChange} 
                type="number" 
                id="price" 
                placeholder="price" 
                value={this.state.price} 
              />

              <button onClick={this.handleClick}>Save</button>
              <button onClick={this.handleCancel}>Cancel</button>
          </form>
        </div>
      );
    }

We need to read the all the values (including the id) out of the form state and pass them to the App component:

/src/components/BookForm.js

  handleClick = (e) => {
    e.preventDefault();
    const newBook = {
        title: this.state.title, 
        author: this.state.author, 
        genre: this.state.genre, 
        price: this.state.price,
        id: this.state.id 
    }
    console.log(newBook)
    this.props.addNewBook(newBook)
  }

Editing books - App.js saving and updating

Lets create a method in our App component to save the updated book. We will use 'PUT' and the corresponding endpoint in the back-end.

/src/App.js

  updateBook(book){
    const url = `http://localhost:5000/books/${book.id}`;
    const data = JSON.stringify(book);
    console.log(data)
      return fetch(url, {
          method: 'PUT', 
          headers: {
              'Content-Type': 'application/json',
              'Accept': 'application/json'
          },
          body: data, 
      })
      .then(response => {
          this.loadBooks();
        }
      )
  }

We will update our addNewBookHandler to handle both updating and creating. We can test to see if an id exists to see if the book needs to be created or updated.

/src/App.js

  addNewBookHandler = (newBook) => {
    console.log(newBook);
    if (newBook.id) {
      this.updateBook(newBook);
    }
    else {
      const newId = this.state.books.length + 1;
      newBook.id = newId;
      this.createBook(newBook);
    }
  }

Once we have finished updating we will need to reset our selectedBook state. The last step is loadBooks. We will add to the existing setState call.

/src/App.js

  loadBooks = () => {
    console.log('loading books')
    const url = 'http://localhost:5000/books';
    fetch(url)
    .then(resp => resp.json())
    .then(data => {
      console.log(data)
      this.setState({ books: data, adding: false, selectedBook: null });
    })
  }

Re-using a form for both New and Create is a common pattern.


Challenge

  1. The form is being re-used but we can make it clearer. Use conditionals to
  • display either Edit or Create New in the form heading
  • display either Update or Create New on the current save button
  1. The new methods are being used to createAndUpdate.
  • Change the name of the App component method addNewBookHandler to createAndUpdateBookHandler.
  • Rename the BookForm prop addNewBook to createAndUpdateBook.
  1. Improve the appearance of the book list by
  • displaying the prices with two decimal points
  • display a different background color for each genre
  • make the lines larger so they are easier to click on
  1. We can still see the number zero being displayed in the heading. Lets use a Loading component to display when the books array is empty.

  2. The add button is displayed when the form is displayed. Change the code in App render so the button is not displayed at the same time as the form.

  3. The button default behaviour for BookForm cancel button is causing the page to refresh. Implement handleCancel. It should be similar to addNewBook but not save a book.

  4. Add Notifications for

  • Book saved
  • book updated
  • add / update canceled
  1. Refactor the code to pass a book rather then all the separate props when editing a book. BookForm will also need to be changed. eg:
<BookForm addNewBook={this.addNewBookHandler} book={this.state.editedBook}/>
  1. Add links to apply sorting to the book list. Sort by
  • author
  • title
  • price
  1. Add a link to filter by genre

Beast

Add validation to the form. Make all the fields required. Make sure the price is not negative. Display error messages when fields are missing or invalid.


Beast++

Add the ability to delete a book.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment