Creating Seussology App - Part 3

Download the Completed Project

Download the Seussology Files.

Unzip the file and copy the app, public, resources, and routes folders replace the existing folders in your laravel project.

Get Insomnia

For today exercise, we will be using the Insomnia REST client. It will make it easier to test our application.

Go to https://insomnia.rest/ to download the software

Creating an API

Setting the API Routes

1. Open api.php in the routes directory

2. Add the routes

Add the following routes to api.php

Route::get('/books', 'BooksController@index');

Route::get('/books/{id}', 'BooksController@show');

Route::post('/books', 'BooksController@store');

Route::put('/books/{book}', 'BooksController@update');

Route::delete('/books/{id}', 'BooksController@delete');

Updating the Controller

Because we are creating an API, the controller will return only the raw data instead of a view.

1. Open BooksController.php in the app/Http/Controllers directory

2. Update the index() method

Replace the return statement, so that it will return raw JSON data instead of the books view.

Note

It is recommend to comment out the old return statement, instead of deleting it.





 


public function index ()
{
    $books = Book::all();
    // return view('books', ['books' => $books]);
    return response()->json($books);
}

Testing in Insomnia

Test the /books API in Insomnia

1. Open Insomnia

Open Insomnia

2. Create a New Request

Create a new request name it "GET BOOKS" and click create.

New Request

3. Add the URL

Add the URL, http://mtm6331-laravel.local/api/books to the GET BOOKS request

Add URL

4. Send the request

Send the request by clicking on the send button. The response should appear on the right.

Send Request

Updating the Controller Methods

1. Open BooksController.php in the app/Http/Controllers directory

2. Update the show() method

Add the $quotes to the $book array and replace the return statement so that raw JSON is returned.

public function show ($id)
{
    $book = Book::find($id);
    $quotes = Book::find($id)->quotes;
    $book['quotes'] = $quotes;
    //return view('book', ['book' => $book, 'quotes' => $quotes]);
    return response()->json($book);
}

3. Update the update() method

Replace the return statement so that raw JSON is returned

public function update (Request $request, Book $book)
{
    $book->update($request->all());
    //return $this->edit($id);
    return response()->json($book, 200);
}

4. Update the delete() method

Replace the return statement so that no content is returned with a 204 code.

public function delete ($id)
{
    $book = Book::find($id);
    $book->delete();

    // return view('delete', ['title' => $book['book_title']]);
    return response()->json(null, 204);
}

Testing the API in Insomnia

1. Add the "GET BOOK" request

Create a new request with the name "GET BOOK". Set the url to http://mtm6331-laravel.local/api/books/13

Test the request by clicking the "Send" button.

2. Add the "POST BOOK" request

Create a new request with the name "POST BOOK". Set the url to http://mtm6331-laravel.local/api/books and set method to POST

Set the body to JSON and add the following:

{
  "id": 50,
  "book_title": "In a People House",
  "book_title_sort": "In a People House",
  "book_year": 1970,
  "book_description": "Learn what there is in a people house.",
  "book_pages": 36,
  "category_id": 1
}

Test the request by clicking the "Send" button.

3. Add the "PUT BOOK" request

Create a new request with the name "PUT BOOK". Set the url to http://mtm6331-laravel.local/api/books/50 and set method to PUT.

Set the body to JSON and add the following:

{
  "book_year": 1972
}

Test the request by clicking the "Send" button.

4. Add the "DELETE BOOK" request

Create a new request with the name "DELETE POST". Set the url to http://mtm6331-laravel.local/api/books/50 and set method to DELETE.

Test the request by clicking the "Send" button.

Creating a SPA

A Single Page Application uses JavaScript to update all the page content to avoid any page refresh.

Creating the View

1. Create a new file in resources/views named spa.blade.php

Enter the following HTML into the file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Seussology SPA</title>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous">
    <link rel="stylesheet" href="/css/seussology.css">

</head>
<body>
    <nav id="nav" class="nav">
      <a href="/quotes" class="nav-link">Quotes</a>
      <a href="/" class="nav-link">
          <img id="logo" class="nav-image" src="/images/seussology-logo.svg" alt="Seussology">
      </a>
      <a href="/books/new/" class="nav-link ">New Book</a>
    </nav>
    <header id="header" class="header">
        <h1 id="title" class="header-title">Books</h1>
    </header>
    <main id="main" class="books"></main>


    <script src="/js/seussology.js"></script>
</body>
</html>

2. Open web.php in the routes directory

3. Update the index route

Update the index route to load the spa view for all the possible urls

Route::get('/{books?}/{id?}', function () {
    return view('spa');
});

Creating the Books Page

Retrieve the books data from the API and dynamically create the HTML using JavaScript.

1. Create a new file in public/js directory with the name seussology.js

2. Retrieve the DOM elements

Retrieve the <main> tag and the page title.

const main = document.getElementById('main')
const title = document.getElementById('title')

3. Create a routes object

Create a routes object that will hold the application routes and related functions.

const routes = {
  '/': getBooks
}

4. Create a templates object

Create a template object that will hold the HTML templates for each view. Each template will be a function.

const templates = {
  'books': function (books) {
    main.innerHTML = books.map(book => `
      <div class="book">
        <a class="book-image" data-id="${book['id']}" href="/books/#${book['id']}">
          <img src="${book['book_image']}" alt="${book['book_title']}">
        </a>
      </div>`).join('')
  }
}

4. Create the getBooks() function

Create the getBooks() function

function getBooks () {

}

5. Retrieve the books data

Inside the getBooks() function, retrieve the books data using the fetch method.

Set the <main> tag class name to books and the page title to Books. Call the books() templates function passing the data retrieved from the fetch method.


 
 
 
 
 
 
 
 
 


function getBooks () {
  fetch('http://mtm6331-laravel.local/api/books')
    .then(function (response) {
      return response.json()
    })
    .then(function (books) {
      title.textContent = 'Books'
      main.className = 'books'
      templates.books(books)
    })
}

6. Call the current route function

Use location.pathname to call the current route function

routes[location.pathname]()

Creating the Book Details Page

1. Add a new route to the routes object

Add a new route for the the Book Details page to the routes object and set it to getBook



 


const routes = {
  '/': getBooks,
  '/books/': getBook
}

2. Add the book template to the templates object

Add a book template as a function inside the templates object










 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


const templates = {
  'books': function (books) {
    main.innerHTML = books.map(book => `
      <div class="book">
        <a class="book-image" data-id="${book['id']}" href="/books/#${book['id']}">
          <img src="${book['book_image']}" alt="${book['book_title']}">
        </a>
      </div>`).join('')
  },
  'book': function (book) {
    main.innerHTML = `
      <div class="details-controls">
        <a href="/books/edit/#${book['id']}" class="details-control"><i class="far fa-edit"></i></a>
        <a class="details-control" data-action="delete"><i class="far fa-trash-alt"></i></a>
      </div>
      <div class="details-image">
          <img src="${book['book_image']}" alt="${book['book_title']}">
      </div>
      <div>
        <h2 class="details-title">${book['book_title']}</h2>
        <p>${book['book_description']}</p>
        <p>Published: ${book['book_year']}<br>
        Number of Pages: ${book['book_pages']}</p>
        ${book.quotes.map(quote => `
          <blockquote class="details-quote">
              <p>${quote['quote']}</p>
          </blockquote>`).join('')}
      </div>`
  }
}

3. Create the getBook() function

Create the getBook() function to retrieve the book data using the fetch method.

Set the <main> tag class name to details and the page title to Book Details. Call the book() templates function passing the data retrieved from the fetch method.

function getBook () {
  const index = location.hash.substr(1)

  fetch(`http://mtm6331-laravel.local/api/books/${index}`)
    .then(function (response) {
      return response.json()
    })
    .then(function (book) {
      main.className = 'details'
      title.textContent = 'Book Details'
      templates.book(book)
    })
}

Creating a JavaScript Navigation System

1. Add a event listener

Add a event listener on the document to listen for any clicks.

document.addEventListener('click', function (e) {
 
})

Stop links and buttons from performing they default function.


 


document.addEventListener('click', function (e) {
  e.preventDefault()
})

3. Check for an anchor tag

Check for an anchor tag by using the closest() method and checking for an href attribute.




 
 
 


document.addEventListener('click', function (e) {
  e.preventDefault()
  const link = e.target.closest('a')
  if (link && link.href) {
    
  }
})

4. Add a page to the browser history

Use the history API and the pushState() function to manually a new page to browser history.





 



document.addEventListener('click', function (e) {
  e.preventDefault()
  const link = e.target.closest('a')
  if (link && link.href) {
    history.pushState(null, 'Seussology', link.href)
  }
})

5. Display the new content

Use the routes object to display the new content base on the location.pathname






 



document.addEventListener('click', function (e) {
  e.preventDefault()
  const link = e.target.closest('a')
  if (link && link.href) {
    history.pushState(null, 'Seussology', link.href)
    routes[location.pathname]()
  }
})

Adding and Updating Data

1. Add new routes to the routes object

Add the following items to the routes object.




 
 


const routes = {
  '/': getBooks,
  '/books/': getBook,
  '/books/new/': getForm,
  '/books/edit/': getForm
}

2. Add a form template to templates object

Add the a form template to templates object as a function. This template will be used to add new data and update existing data.

'form': function (book) {
  const categories = [
    '',
    'Beginner Books',
    'Big Books',
    'Short Stories'
  ]

  main.innerHTML = `
    <form id="form" class="form" enctype="multipart/form-data">
      <div class="form-group title">
        <label class="form-label">Title</label>
        <input id="book_title" class="form-input" type="text" name="title" value="${book['book_title']}">
      </div>

      <div class="form-group category">
        <label class="form-label">Category</label>
        <select id="category_id" class="form-input">
          ${categories.map((cat, index) => `<option value="${index}" ${(index === book['category_id'] ? 'selected = "selected"' : '')}>${cat}</option>`).join('')}
        </select>
      </div>

      <div class="form-group year">
          <label class="form-label">Published Year</label>
          <input id="book_year" class="form-input" type="text" maxlength="4" name="year" value="${book['book_year']}">
      </div>

      <div class="form-group pages">
          <label class="form-label">Number of Pages</label>
          <input id="book_pages" class="form-input" type="number" name="pages" value="${book['book_pages']}">
      </div>

      <div class="form-group image">
          <label class="form-label">Cover Image</label>
          <input id="book_image" class="form-input" type="text" name="image" value="${book['book_image']}">
      </div>


      <div class="form-group description">
        <label class="form-label">Description</label>
        <textarea id="book_description" class="form-input" name="description">${book['book_description']}</textarea>
      </div>

      <div class="form-group">
        <button class="button">Submit</button>
      </div>
    </form>`
}

3. Create the getForm() function

Create the getForm() function to display the form template. The function will retrieve the book data using the fetch method if a book id is provided or create a empty book object if not.

function getForm () {
  const index = location.hash.substr(1)

  if (index) {
    fetch(`http://mtm6331-laravel.local/api/books/${index}`)
      .then(function (response) {
        return response.json()
      })
      .then(function (book) {
        main.className = 'container'
        title.textContent = 'Edit Book'
        templates.form(book)
      })
  } else {
    const book = {
      'book_title': '',
      'category_id': '',
      'book_year': '',
      'book_pages': '',
      'book_image': '',
      'book_description': ''
    }

    main.className = 'container'
    title.textContent = 'New Book'
    templates.form(book)
  }
}

4. Udpate the event listener

Update the event listener to prevent the form from submitting and pass the form data to the setBook() function. Check for the button class do know if a form submission was attempted. Access all the form data by retrieve each input by their id.







 
 
 
 
 
 
 
 
 
 
 
 


document.addEventListener('click', function (e) {
  e.preventDefault()
  const link = e.target.closest('a')
  if (link && link.href) {
    history.pushState(null, 'Seussology', link.href)
    routes[location.pathname]()
  } else if (e.target.classList.contains('button')) {
    const data = {
      'book_title': document.getElementById('book_title').value,
      'category_id': document.getElementById('category_id').value,
      'book_year': document.getElementById('book_year').value,
      'book_pages': document.getElementById('book_pages').value,
      'book_image': document.getElementById('book_image').value,
      'book_description': document.getElementById('book_description').value
    }

    setBook(data)
  }
})

5. Create the setForm() function

The setForm() function will handle the sending to the data to the API. It will do this also through fetch method. Since this will be used to both add and update data, the location.hash will be used to be used to identify which task will occur. In both cases, the function to redirect to the book details page and display the book details for the book that was just added or udpated.

function setBook (data) {
  const index = (location.hash) ? `/${location.hash.substr(1)}` : ''
  const method = (index) ? 'put' : 'post'

  fetch(`http://mtm6331-laravel.local/api/books${index}`, {
    method: method,
    headers: {
      'Content-Type': 'application/json; charset=utf-8'
    },
    body: JSON.stringify(data)
  })
    .then(function (response) {
      return response.json()
    })
    .then(function (book) {
      history.pushState(null, 'Suessology', `/books/#${book['id']}`)
      routes[location.pathname]()
    })
}

Removing Data

1. Do NOT add a route

There will be no route for deleting so that books cannot be deleted by just entering the URL.

2. Update the Event Listener

Update the event listener to check for the delete button using the data-action attribute and check if the value is equal to delete. Call the removeBook() function.


















 
 
 
 
 
 


document.addEventListener('click', function (e) {
  e.preventDefault()
  const link = e.target.closest('a')
  if (link && link.href) {
    history.pushState(null, 'Seussology', link.href)
    routes[location.pathname]()
  } else if (e.target.classList.contains('button')) {
    const data = {
      'book_title': document.getElementById('book_title').value,
      'category_id': document.getElementById('category_id').value,
      'book_year': document.getElementById('book_year').value,
      'book_pages': document.getElementById('book_pages').value,
      'book_image': document.getElementById('book_image').value,
      'book_description': document.getElementById('book_description').value
    }

    setBook(data)
  } else {
    const link = e.target.closest('[data-action]')
    if (link.dataset.action === 'delete') {
      removeBook()
    }
  }
})

3. Create the removeBook() function

Create the removeBook() function. Use the location.hash to get the desire book and call the API using the fetch method. Redirect and display the books page.

function removeBook () {
  const index = location.hash.substr(1)

  if (index) {
    fetch(`http://mtm6331-laravel.local/api/books/${index}`, {
      method: 'delete'
    })
      .then(function () {
        history.pushState(null, 'Suessology', '/')
        routes[location.pathname]()
      })
  }
}