Lesson Tuesday

For finished code, see repo here

OK, now we are finally ready!

To add the frontend routes and template changes we need in order to process and show changes to our Category objects. This will be our last long lesson on fleshing out our To Do List, but it is quite complex, so be sure to follow along closely.

First, let's add route handlers in App.java to pick up users' attempts to make changes. As we did with Task, we'll want full CRUD functionality, meaning we need to add the following routes:

  • Create
    • Add a Category
  • Read
    • Get A single Category
    • Get All categories
  • Update
    • Show a form to update a Category
    • Process a form to update a Category
  • Delete
    • Delete an individual Category
    • Delete all Categories

Phew! That's a lot of extra routes! (Double, in fact.) Additionally, we'll need to edit many of our existing routes because Tasks are currently nested inside of Categorys - No Task can be created outside of a parent Category, which provides its categoryId.

This is where the power of RESTful routing becomes obvious: If you look at the routes below, you can tell by the URL alone which data is in charge here, even if you know nothing more about this specific app! The fact that an individual Task is nested inside an individual Category tells us exactly which data model and template we need to have available. Take a look:

RESTful-routes-obj-obj

This is why we need to change our dynamic routes in lots of places.

Here is what our App.java should look like now. We've placed comments on/near lines that will need to be updated, and where new routes will need to go. Read through the file below carefully, and make sure you understand all necessary changes. It's easy to get lost here and end up with a half-working app! Edit your file manually, don't just copy and paste! Understand, don't blindly proceed!

src/main/java/App.java
import models.Task;
import dao.Sql2oTaskDao;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import org.sql2o.Sql2o;
import spark.ModelAndView;
import spark.template.handlebars.HandlebarsTemplateEngine;
import static spark.Spark.*;

public class App {
    public static void main(String[] args) {
        staticFileLocation("/public");
        String connectionString = "jdbc:h2:~/todolist.db;INIT=RUNSCRIPT from 'classpath:db/create.sql'";
        Sql2o sql2o = new Sql2o(connectionString, "", "");
        Sql2oTaskDao taskDao = new Sql2oTaskDao(sql2o);
        Sql2oCategoryDao categoryDao = new Sql2oCategoryDao(sql2o);

        //get: show all tasks in all categories and show all categories
        get("/", (req, res) -> {
          Map<String, Object> model = new HashMap<>();
          List<Task> tasks = taskDao.getAll();
          model.put("tasks", tasks);
          return new ModelAndView(model, "index.hbs");
        }, new HandlebarsTemplateEngine());

        //get: show a form to create a new category
        //  /categories/new

        //post: process a form to create a new category
        //  /categories

        //get: delete all categories and all tasks
        //  /categories/delete

        //get: delete all tasks
        get("/tasks/delete", (req, res) -> {
            Map<String, Object> model = new HashMap<>();
            taskDao.clearAllTasks();
            res.redirect("/");
            return null;
        }, new HandlebarsTemplateEngine());

        //get a specific category (and the tasks it contains)
        //  /categories/:category_id

        //get: show a form to update a category
        //  /categories/:id/edit

        //post: process a form to update a category
        //  /categories/:id

        //get: delete a category and tasks it contains
        //  /categories/:id/delete

        //get: delete an individual task
        get("/categories/:category_id/tasks/:task_id/delete", (req, res) -> {
            Map<String, Object> model = new HashMap<>();
            int idOfTaskToDelete = Integer.parseInt(req.params("task_id"));
            taskDao.deleteById(idOfTaskToDelete);
            res.redirect("/");
            return null;
        }, new HandlebarsTemplateEngine());

        //get: show new task form
        get("/tasks/new", (req, res) -> {
            Map<String, Object> model = new HashMap<>();
            return new ModelAndView(model, "task-form.hbs");
        }, new HandlebarsTemplateEngine());

        //task: process new task form
        post("/tasks", (req, res) -> { //URL to make new task on POST route
            Map<String, Object> model = new HashMap<>();
            String description = req.queryParams("description");
            Task newTask = new Task(description, 1 ); //ignore the hardcoded categoryId for now
            taskDao.add(newTask);
            res.redirect("/");
            return null;
        }, new HandlebarsTemplateEngine());

        //get: show an individual task that is nested in a category
        get("/categories/:category_id/tasks/:task_id", (req, res) -> {
          Map<String, Object> model = new HashMap<>();
          int idOfTaskToFind = Integer.parseInt(req.params("task_id"));
          Task foundTask = taskDao.findById(idOfTaskToFind);
          model.put("task", foundTask);
          return new ModelAndView(model, "task-detail.hbs");
        }, new HandlebarsTemplateEngine());

        //get: show a form to update a task
        get("/tasks/:id/edit", (req, res) -> {
            Map<String, Object> model = new HashMap<>();
            int idOfTaskToEdit = Integer.parseInt(req.params("id"));
            Task editTask = taskDao.findById(idOfTaskToEdit);
            model.put("editTask", editTask);
            return new ModelAndView(model, "task-form.hbs");
        }, new HandlebarsTemplateEngine());

        //task: process a form to update a task
        post("/tasks/:id", (req, res) -> { //URL to update task on POST route
            Map<String, Object> model = new HashMap<>();
            String newContent = req.queryParams("description");
            int idOfTaskToEdit = Integer.parseInt(req.params("id"));
            taskDao.update(idOfTaskToEdit, newContent, 1); //ignore the hardcoded categoryId for now
            res.redirect("/");
            return null;
        }, new HandlebarsTemplateEngine());
    }
}

Alright. Let's go through this step by step.

1. Edit layout.hbs

Let's edit our Handlebars template - as anywhere we access a route (a form submit, a link), those need to be changed now too, and we'll also need some new frontend to handle CRUD for categories.

Let's start with our layout. It would be cool to show all categories in the page's menu, so users can access them quickly. Luckily, we can use a Bootstrap navbar for this. Sweet!

src/main/resources/templates/layout.hbs
<!DOCTYPE html>
<html>
<head>
   <title>{{#block "title"}}Blog{{/block}}</title>
   <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css'>
</head>
<body>
<ul class="nav nav-tabs">
   <li role="presentation" class="active"><a href="/">Home</a></li>
   <li role="presentation"><a href="/tasks/new">Add a New Task</a></li>

   <li role="presentation" class="dropdown">
       <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
           Category Menu <span class="caret"></span>
       </a>
       <ul class="dropdown-menu">
           {{#if categories}}
               {{#each categories}}
                   <li><a href="/categories/{{id}}">{{name}}</a></li>
               {{/each}}
               <li><a href="/categories/new">Add a Category</a></li>

           {{else}}
               <li><a href="/categories/new">Add a Category</a></li>
           {{/if}}
       </ul>
   </li>
</ul>
<div class="container">
   <!--begin main template-->
   <div class="page-header">
       <img src="/images/rawpixel-com-296621.jpg" width="70%" alt="https://unsplash.com/search/task?photo=Lh_bn9SgRSY">
       <h1>Welcome to my To Do List. <small>Let's be organized, focused, and efficient.</small></h1>
   </div>

   <div class="col-md-12">
       {{#block "content"}}
       {{/block}}

   </div>
</div>
<!--end main template-->
{{#block "footer"}}
   <footer class="footer">
       <div class="container">
           <hr>
           <p class="text-muted">
              <a href="/categories/delete">delete all categories & tasks</a><br>
              <a href="/tasks/delete">delete all tasks</a>
           </p>
       </div>
   </footer>
   </body>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
   </html>
{{/block}}

Cool. Take a look at what kind of UI we're crafting with Handlebars logic, and you'll see we're showing the categories in a dropdown if they are available, and a link to the new category form template if they aren't. Don't forget to add jQuery and Bootstrap.min.js to provide functionality to the menu.

2. Make Categories Available for Each Route, Add Sql2oCategory DAO

Because each template (task-form.hbs, categories-detail.hbs) needs to have an up-to-date list of categories, we'll need to add a call to categoryDao.getAll() to each route.

(This is not super DRY, but we'll explore efficient way to do this soon.) Go ahead and add code to make all categories available to each route handler now. We'll need to create a new categoryDao object at the top of our main method.

src/main/java/App.java
...
Sql2oCategoryDao categoryDao = new Sql2oCategoryDao(sql2o);
...

//get: show all tasks in all categories and show all categories
get("/", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  List<Category> allCategories = categoryDao.getAll();
  model.put("categories", allCategories);
  List<Task> tasks = taskDao.getAll();
  model.put("tasks", tasks);
  return new ModelAndView(model, "index.hbs");
}, new HandlebarsTemplateEngine());

...

Add the above code that retrieves all categories and adds them to the model to each existing route.

Now, run your app, to test that everything still works. Our site should currently look like this:

blog-categories-example.png

2. Add New category-form.hbs Template

Create a new _category-form.hbs _ template containing the code below. This template includes frontend code to handle updating, which we'll implement later.

src/main/resources/templates/category-form.hbs
{{#partial "content"}}
   {{#if editCategory}}
       <h1>Edit this category!</h1>
       <form action="/categories/{{category.id}}" method="post">
         <label for="name">Edit this category's name</label>
         <input id="newCategoryName" name="newCategoryName" type="text" value="{{category.name}}">
         <button type="submit" class="btn btn-default">Go!</button>
       </form>
  {{else}}
       <h1>Add a new Category!</h1>
       <form action="/categories" method="post">
         <label for="name">Category Name</label>
         <input id="name" name="name" type="text">
         <button type="submit" class="btn btn-default">Go!</button>
       </form>
   {{/if}}
{{/partial}}

{{> layout.hbs}}

3. Update Route to Display a Form to Create a New Category

Next, let's add a route to App.java that will both show a form to create a new category and process that form when submitted:

App.java
...
//show new category form
get("/categories/new", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  List<Category> categories = categoryDao.getAll(); //refresh list of links for navbar
  model.put("categories", categories);
  return new ModelAndView(model, "category-form.hbs"); //new
}, new HandlebarsTemplateEngine());


//post: process new category form
post("/categories", (req, res) -> { //new
  Map<String, Object> model = new HashMap<>();
  String name = req.queryParams("name");
  Category newCategory = new Category(name);
  categoryDao.add(newCategory);
  res.redirect("/");
  return null;
}, new HandlebarsTemplateEngine());
...

Reload the server, add some Categorys, and see them populate the dropdown menu. Cool!!!

4. Add a Category Detail Template

Next, let's add a category-detail.hbs template so we can take a look at the Category. This template should list out the Categorys information, but also show us those Tasks listed in that Category.

Our detail page should look like this:

src/main/resources/templates/category-detail.hbs
{{#partial "content"}}
   <h1>{{category.name}}</h1>
   <p>Here are all the tasks listed in this category:</p>
   <ul>
       {{#each tasks}}
           <li>
               <p><a href="/categories/{{category.id}}/tasks/{{id}}">{{description}}</a></p>
           </li>
       {{/each}}
   </ul>
   <a href="/categories/{{category.id}}/edit">Edit this category</a><br>
   <a href="/categories/{{category.id}}/delete">DELETE this category</a>
{{/partial}}

{{> layout.hbs}}

5. Tweak Task Detail Page to Fit our New Structure

src/main/resources/templates/task-detail.hbs
{{#partial "content"}}

   <p>Description: {{ task.description }}</p>
   <p>Category of this task: {{ category.name }}</p>
   <h5>
       Task status:
       {{#if task.published }}
           <span class="glyphicon glyphicon-star" aria-hidden="true"></span>  Completed
       {{else}}
           <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Open
       {{/if}}
   </h5>
   <p>
       <a href="/tasks/{{task.id}}/edit">Edit this task</a><br>
       <a href="/categories/{{task.categoryId}}/tasks/{{task.id}}/delete">DELETE this task</a>
   </p>

{{/partial}}

{{> layout.hbs}}

6. Changes to index.hbs

src/main/resources/templates/index.hbs
{{#partial "content"}}

   <h1> List o' stuff to do!</h1>

   {{#if tasks}}
       {{#each tasks }}
           <p><a href="/categories/{{categoryId}}/tasks/{{id}}">{{description}}</a></p>
       {{/each}}
   {{else}}
       <p>No tasks!</p>
   {{/if}}
{{/partial}}

{{> layout.hbs}}

7. Adding a Route to View a Category and its Tasks

Sweet! We're making great progress here.

Let's add a route to serve our detail template to our App.java. It needs to have all categories (for the navbar), a specific category (for the template) and all tasks that belong to that category (also for the template) in the model.put, so it is quite a long route handler.

src/main/java/App.java
//get: show an individual category and tasks it contains
get("/categories/:id", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  int idOfCategoryToFind = Integer.parseInt(req.params("id")); //new
  Category foundCategory = categoryDao.findById(idOfCategoryToFind);
  model.put("category", foundCategory);
  List<Task> allTasksByCategory = categoryDao.getAllTasksByCategory(idOfCategoryToFind);
  model.put("tasks", allTasksByCategory);
  model.put("categories", categoryDao.getAll()); //refresh list of links for navbar
  return new ModelAndView(model, "category-detail.hbs"); //new
}, new HandlebarsTemplateEngine());

Run your app, and you should be able to visit a detail page and see basic category information. You won't see any tasks yet, as we haven't changed our frontend to allow for that to happen. We'll do that next.

6. Updating task-form.hbs to Assign Tasks to Categories

The next thing we should do is change our task-form.hbs, so that we can test our ability to add categories to tasks via our frontend. Using a dropdown menu might be a nice touch to allow our user to choose from categories.

src/main/resources/templates/task-form.hbs
{{#partial "content"}}
   {{#if editTask}}
       <h1>Edit this Task!</h1>
       <form action="/tasks/{{task.id}}" method="post">
          <br>
          <label for="description">Edit this task's description</label>
          <input id="description" name="description" type="text" value="{{task.description}}">
          <br>
          <label for="categoryId">Update this task's category:</label>
          <select id="categoryId" name="categoryId">
             {{#each categories}}
                <option value="{{id}}">{{name}}</option>
             {{/each}}
          </select>
          <p>Don't see the right category?<a href="/categories/new">Add a new one first.</a></p>
          <button type="submit" class="btn btn-default">Go!</button>
       </form>
   {{else}}
       <h2>Add a new Task!</h2>
       <form action="/tasks" method="post">
         {{#if categories}}
             <label for="description">Task Description</label>
             <input id="description" name="description" type="text">
             <label for="categoryId">Categorize this task as:</label>
             <select id="categoryId" name="categoryId">
                 {{#each categories}}
                     <option value="{{id}}">{{name}}</option>
                 {{/each}}
             </select>
             <p>Don't see the right category?<a href="/categories/new">Add a new one first.</a></p>
         {{else}}
             <h3> You'll need to add a category before you add a task. <a href="/categories/new">Add one here.</a> </h3>
         {{/if}}
         <button type="submit" class="btn btn-default">Go!</button>
       </form>
   {{/if}}
{{/partial}}

{{> layout.hbs}}

Take a look at the pieces of the page where where you can see new hbs syntax. Try and understand how the logic of this page works now before you proceed. I built a useful safety function that will help prevent a Task existing without a Category and improve our right into the frontend logic. Can you spot what I mean?

7. Tweak our Route to Create a New Task with the Correct categoryId

src/main/java/App.java
//post: process new task form
post("/tasks", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  List<Category> allCategories = categoryDao.getAll();
  model.put("categories", allCategories);
  String description = req.queryParams("description");
  int categoryId = Integer.parseInt(req.queryParams("categoryId"));
  Task newTask = new Task(description, categoryId ); //ignore the hardcoded categoryId
  taskDao.add(newTask);
  res.redirect("/");
  return null;
}, new HandlebarsTemplateEngine());

8. Updating Categories

Alright, we are nearly done working through our CRUD methods. Let's finish strong by allowing our users to update the name of the category, but also the category (and therefore categoryId) that a task is assigned to.

The code in our hbs files is already in place, but we still need to add the correct routes to our App.java to handle these requests.

src/main/java/App.java
//get: show a form to update a category
get("/categories/:id/edit", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  model.put("editCategory", true);
  Category category = categoryDao.findById(Integer.parseInt(req.params("id")));
  model.put("category", category);
  model.put("categories", categoryDao.getAll()); //refresh list of links for navbar
  return new ModelAndView(model, "category-form.hbs");
}, new HandlebarsTemplateEngine());


//post: process a form to update a category
post("/categories/:id", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  int idOfCategoryToEdit = Integer.parseInt(req.params("id"));
  String newName = req.queryParams("newCategoryName");
  categoryDao.update(idOfCategoryToEdit, newName);
  res.redirect("/");
  return null;
}, new HandlebarsTemplateEngine());

After implementing this,spin up the server and make sure everything works as it should.

9. Updating Tasks

Our routes and templates for updating tasks also need some small changes to account for the shift to nested objects. Our routes should look like this:

src/main/java/App.java
//get: show a form to update a task
get("/tasks/:id/edit", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  List<Category> allCategories = categoryDao.getAll();
  model.put("categories", allCategories);
  Task task = taskDao.findById(Integer.parseInt(req.params("id")));
  model.put("task", task);
  model.put("editTask", true);
  return new ModelAndView(model, "task-form.hbs");
}, new HandlebarsTemplateEngine());

//post: process a form to update a task
post("/tasks/:id", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  int taskToEditId = Integer.parseInt(req.params("id"));
  String newContent = req.queryParams("description");
  int newCategoryId = Integer.parseInt(req.queryParams("categoryId"));
  taskDao.update(taskToEditId, newContent, newCategoryId);
  res.redirect("/");
  return null;
}, new HandlebarsTemplateEngine());

10. Deleting - a Single Category, All Categories

We should already have our links in place to delete a specific category (I placed the links at the bottom of the category overview page). But we'll also need a link to delete all categories. I'm deciding to place mine in the footer of the layout page, next to delete all tasks.

In a more built out version of this project, this should belong in a secure admin area, but for now, this works fine.

src/main/java/App.java
//get: delete all categories and all tasks
get("/categories/delete", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  categoryDao.clearAllCategories();
  taskDao.clearAllTasks();
  res.redirect("/");
  return null;
}, new HandlebarsTemplateEngine());


//get: delete all tasks
get("/tasks/delete", (req, res) -> {
  Map<String, Object> model = new HashMap<>();
  taskDao.clearAllTasks();
  res.redirect("/");
  return null;
}, new HandlebarsTemplateEngine());

11. Checking Routes and Links / Troubleshooting

Go through your site carefully and check for missing and incorrect links, both in forms and hrefs. Make note of what route handlers are incorrect in App. There will undoubtedly be some broken links. Fix them diligently one by one using this document and the example repo as your guide.

When you see a 404, make look at the page that you came from before - is the route you are trying to access mapped in your App.java file? Did a form action path change? Did a link change?

When troubleshooting, it's useful to use the developer pane to inspect links and forms, as most errors come from incorrect routes.

Also check your terminal output IntelliJ - It should record attempts to access that aren't mapped.

You should also check the names of your templates for spelling errors, as these can also lead to 404's.

When you see a 500 server error, check your terminal output for hints on what went wrong.

If your app doesn't crash but your templates don't show the data you need, it is likely that you are missing some data that should have been added via model.put();

If methods don't seem to work correctly - set breakpoints and run your app in debugger mode to see if this can help you sleuth out the problem.

Good job everybody!

Wrap Up

We now have a basic, functional To Do List app with a Postgres database. Woohoo! There are many improvements that could be made, sure; for instance, our user interface could be more intuitive and we don't yet have safety features to prevent unauthorized use. But, we were able to create two data models, link them together, and provide complete CRUD capabilities (with thorough test coverage!) and successfully tie everything into our frontend!

While this project is certainly long-winded and sometimes confusing, it provides tons of valuable practice with basic Java syntax, and insight into how complex even simple projects can become. Imagine if we added a Votes class to up and downvote Tasks. Even seemingly simple functionality can introduce complexity.

For the rest of this week, use this project as your guide for routing, templating, structuring interfaces, modeling data, testing, and linking objects together. And, try to improve this teaching tool with your own approaches. Just make sure to stick to RESTful conventions as you proceed!

Next week, we'll expand on some of these concepts and introduce ways to make our App.java files shorter and more concise. Give yourself a pat on the back! Well done!


Completed Adding Category Routes and Frontend