Donny's Programming Blog

Making an SSR Web App with Spring Boot (Part 3)

March 02, 2019

Introduction

To recap, we now have a functioning web app that displays books in our fictional publishing house. We now need to manage looking at an individual book as well as accepting new entries from our users.

Individual Book Page

The first thing we want to avoid is having to make an individual page for every book we have. Thymeleaf to the rescue again! Let’s take a look at what our template will look like and then get our Controller wired up.

<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
  <head>
    <title th:text="${book.title}">Default Title</title>
  </head>
  <body>
    <header>
      <section>
        <h1 th:text="${book.title}">Default Title</h1>
        <h2 th:text="${book.author}">Default Author</h2>
      </section>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/books">Books</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <small th:text="${book.genre}">Default Genre</small>
      <p>More book info coming soon...</p>
    </main>
    <footer>
      <small>&copy; Literate Octo Dollop 2019</small>
    </footer>
  </body>
</html>

So we’re referencing this book object but we’re not currently connecting that. Time to head back to our controller.

Single Book Mapping

So now we need to specify a route that our users will go to when they want to see a single book. Here’s what we need to add to our Controller:

package com.donhamiltoniii.literateoctodollop.controllers;

import java.util.Optional;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.donhamiltoniii.literateoctodollop.models.Book;
import com.donhamiltoniii.literateoctodollop.repositories.BookRepository;

@Controller
@RequestMapping("/books")
public class BookController {

	@Resource
	BookRepository bookRepo;

	@GetMapping({ "", "/", "/index" })
	public String getAllBooks(Model model) {
		model.addAttribute("books", bookRepo.findAll());
		return "books/all";
	}

	@GetMapping("/{id}")
	public String getBook(@PathVariable Long id, Model model) throws Exception {
		Optional<Book> bookOptional = bookRepo.findById(id);
		if (bookOptional.isPresent()) {
			model.addAttribute("book", bookOptional.get());
		} else {
			throw new Exception("The requested book doesn't exist");
		}
		return "books/single";
	}
}

We now have a mapping for single books! Now we only need one template for all of our books because all books contain the same information. So we just inject the same type of info from all books into a single template. Our bookRepo returns us an Optional and not a Book. For that reason we have to perform a conditional check to make sure that the Optional contains the Book object that we expect. If it does, we send that Book object to the View template for rendering, otherwise we throw an exception and stop our app. Now the links on our books page work! Let’s look into adding new books from our application. We’ll do that on the books page

<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
  <head>
    <title>Literate Octo Dollop: Books</title>
  </head>
  <body>
    <header>
      <section>
        <h1>Literate Octo Dollop</h1>
        <h2>Books</h2>
      </section>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/books">Books</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <ul>
        <li th:each="book : ${books}">
          <h3>
            <a th:href="@{|/books/${book.id}|}" th:text="${book.title}">
              Default Title
            </a>
          </h3>
          <h4 th:text="|By: ${book.author}|">Default Author</h4>
          <small th:text="${book.genre}">Default Genre</small>
        </li>
      </ul>
      <form action="/books/add" method="POST">
        <label>Title: <input type="text" name="title"/></label>
        <label>Author: <input type="text" name="author"/></label>
        <label>Genre: <input type="text" name="genre"/></label>
        <button type="submit">Submit</button>
      </form>
    </main>
    <footer>
      <small>&copy; Literate Octo Dollop 2019</small>
    </footer>
  </body>
</html>

With our HTML in place, we have the part our user will interact with. Now we need to wire up the Controller to handle that form submission. Let’s have a look at what we need to do there:

package com.donhamiltoniii.literateoctodollop.controllers;

import java.util.Optional;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.donhamiltoniii.literateoctodollop.models.Book;
import com.donhamiltoniii.literateoctodollop.repositories.BookRepository;

@Controller
@RequestMapping("/books")
public class BookController {

	@Resource
	BookRepository bookRepo;

	@GetMapping({ "", "/", "/index" })
	public String getAllBooks(Model model) {
		model.addAttribute("books", bookRepo.findAll());
		return "books/all";
	}

	@GetMapping("/{id}")
	public String getBook(@PathVariable Long id, Model model) throws Exception {
		Optional<Book> bookOptional = bookRepo.findById(id);
		if (bookOptional.isPresent()) {
			model.addAttribute("book", bookOptional.get());
		} else {
			throw new Exception("The requested book doesn't exist");
		}
		return "books/single";
	}

	@PostMapping("/add")
	public String addBook(String title, String author, String genre) {
		bookRepo.save(new Book(title, author, genre));
		return "redirect:/books";
	}
}

So we aren’t adding much code to our Controller but what we’re adding is extremely powerful. So we’re getting all of the values we need to make a new book from our form. Note that the name attribute you give to your HTML input elements need to correspond to the parameters you create in your POST Controller method. This is how our browser environment sends messages to our server environment. We’re then using that new information to create a new book and save it to our repository. Once that new book is saved, our Controller method redirects the application back to /books which should now display our newly added book!

In the next section we will add more functionality to our Book class as well as add some new relationships to our application. Check out Part 4 here.


Don Hamilton III

Written by Don Hamilton III who lives and works in Columbus, OH teaching folx to program at We Can Code IT. You should follow him on Twitter