Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

16. Making a Simple Web App, Part 1
Written by Tim Condon

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapters, you learned how to display data in a website and how to make the pages look nice with Bootstrap. In this chapter, you’ll learn how to create different models and how to edit acronyms.

Categories

You’ve created pages for viewing acronyms and users. Now it’s time to create similar pages for categories. Open WebsiteController.swift. At the bottom of the file, add a context for the “All Categories” page:

struct AllCategoriesContext: Encodable {
  // 1
  let title = "All Categories"
  // 2
  let categories: [Category]
}

Here’s what this does:

  1. Define the page’s title for the template.
  2. Define an array of categories to display in the page.

Next, add the following under allUsersHandler(_:) to create a new route handler for the “All Categories” page:

func allCategoriesHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  Category.query(on: req.db).all().flatMap { categories in
    // 2
    let context = AllCategoriesContext(categories: categories)
    // 3
    return req.view.render("allCategories", context)
  }
}

Here’s what this route handler does:

  1. Get all the categories from the database like before.
  2. Create an AllCategoriesContext. Notice that the context includes the query result directly, since Leaf can handle futures.
  3. Render the allCategories.leaf template with the provided context.

Create a new file in Resources/Views called allCategories.leaf for the “All Categories” page. Open the new file and add the following:

#extend("base"):
  <!-- 1 -->
  #export("content"):
    <h1>All Categories</h1>
    
    <!-- 2 -->
    #if(count(categories) > 0):
      <table class="table table-bordered table-hover">
        <thead class="thead-light">
          <tr>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          <!-- 3 -->
          #for(category in categories):
            <tr>
              <td>
                <a href="/categories/#(category.id)">
                  #(category.name)
                </a>
              </td>
            </tr>
          #endfor
        </tbody>
      </table>
    #else:
      <h2>There aren’t any categories yet!</h2>
    #endif
  #endexport
#endextend

This template is like the table for all acronyms, but the important points are:

  1. Set the content variable for use by base.leaf.
  2. Check if any categories exist.
  3. Loop through each category and add a row to the table with the name, linking to a category page.

Now, you need a way to display all of the acronyms in a category. Open, WebsiteController.swift and add the following context at the bottom of the file for the new category page:

struct CategoryContext: Encodable {
  // 1
  let title: String
  // 2
  let category: Category
  // 3
  let acronyms: [Acronym]
}

Here’s what the context contains:

  1. A title for the page; you’ll set this as the category name.
  2. The category for the page.
  3. The category’s acronyms.

Next, add the following under allCategoriesHandler(_:) to create a route handler for the page:

func categoryHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  Category.find(req.parameters.get("categoryID"), on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { category in
      // 2
      category.$acronyms.get(on: req.db).flatMap { acronyms in
        // 3
        let context = CategoryContext(
          title: category.name,
          category: category,
          acronyms: acronyms)
        // 4
        return req.view.render("category", context)
      }
  }
}

Here’s what the route handler does:

  1. Get the category from the request’s parameters and unwrap the returned future.
  2. Perform a query get all the acronyms for the category using Fluent’s helpers.
  3. Create a context for the page.
  4. Return a rendered view using the category.leaf template.

Create the new template file, category.leaf, in Resources/Views. Open the new file and add the following:

#extend("base"):
  #export("content"):
    <h1>#(category.name)</h1>
    
    #extend("acronymsTable")
  #endexport
#endextend

This is almost the same as the user’s page just with the category name for the title. Notice that you’re using the acronymsTable.leaf template to display the table to acronyms. This avoids duplicating yet another table and, again, shows the power of templates. Open base.leaf and add the following after the link to the all users page:

<li class="nav-item 
 #if(title == "All Categories"): active #endif">
  <a href="/categories" class="nav-link">All Categories</a>
</li>

This adds a new link to the navigation on the site for the all categories page. Finally, open WebsiteController.swift and, at the end of boot(routes:), add the following to register the new routes:

// 1
routes.get("categories", use: allCategoriesHandler)
// 2
routes.get("categories", ":categoryID", use: categoryHandler)

Here’s what this does:

  1. Register a route at /categories that accepts GET requests and calls allCategoriesHandler(_:).
  2. Register a route at /categories/<CATEGORY ID> that accepts GET requests and calls categoryHandler(_:).

Build and run, then go to http://localhost:8080/ in your browser. Click the new All Categories link in the menu and you’ll go to the new “All Categories” page:

Click a category and you’ll see the category information page with all the acronyms for that category:

Create acronyms

To create acronyms in a web application, you must actually implement two routes. You handle a GET request to display the form to fill in. Then, you handle a POST request to accept the data the form sends.

struct CreateAcronymContext: Encodable {
  let title = "Create An Acronym"
  let users: [User]
}
func createAcronymHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  User.query(on: req.db).all().flatMap { users in
    // 2
    let context = CreateAcronymContext(users: users)
    // 3
    return req.view.render("createAcronym", context)
  }
}
// 1
func createAcronymPostHandler(_ req: Request) throws 
  -> EventLoopFuture<Response> {
  // 2
  let data = try req.content.decode(CreateAcronymData.self)
  let acronym = Acronym(
    short: data.short, 
    long: data.long, 
    userID: data.userID)
  // 3
  return acronym.save(on: req.db).flatMapThrowing {
      // 4
      guard let id = acronym.id else {
        throw Abort(.internalServerError)
      }
      // 5
      return req.redirect(to: "/acronyms/\(id)")
  }
}
// 1
routes.get("acronyms", "create", use: createAcronymHandler)
// 2
routes.post("acronyms", "create", use: createAcronymPostHandler)
<!-- 1 -->
#extend("base"):
  #export("content"):
    <h1>#(title)</h1>

    <!-- 2 -->
    <form method="post">
      <!-- 3 -->
      <div class="form-group">
        <label for="short">Acronym</label>
        <input type="text" name="short" class="form-control"
        id="short"/>
      </div>

      <!-- 4 -->
      <div class="form-group">
        <label for="long">Meaning</label>
        <input type="text" name="long" class="form-control"
        id="long"/>
      </div>

      <div class="form-group">
        <label for="userID">User</label>
        <!-- 5 -->
        <select name="userID" class="form-control" id="userID">
          <!-- 6 -->
          #for(user in users):
            <option value="#(user.id)">
              #(user.name)
            </option>
          #endfor
        </select>
      </div>

      <!-- 7 -->
      <button type="submit" class="btn btn-primary">
        Submit
      </button>
    </form>
  #endexport
#endextend
<!-- 1 -->
<li class="nav-item 
 #if(title == "Create An Acronym"): active #endif">
  <!-- 2 -->
  <a href="/acronyms/create" class="nav-link">
    Create An Acronym
  </a>
</li>

Editing acronyms

You now know how to create acronyms through the website. But what about editing an acronym? Thanks to Leaf, you can reuse many of the same components to allow users to edit acronyms. Open WebsiteController.swift.

struct EditAcronymContext: Encodable {
  // 1
  let title = "Edit Acronym"
  // 2
  let acronym: Acronym
  // 3
  let users: [User]
  // 4
  let editing = true
}
func editAcronymHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  // 1
  let acronymFuture = Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
  // 2
  let userQuery = User.query(on: req.db).all()
  // 3
  return acronymFuture.and(userQuery)
    .flatMap { acronym, users in
      // 4
      let context = EditAcronymContext(
        acronym: acronym, 
        users: users)
      // 5
      return req.view.render("createAcronym", context)
  }
}
func editAcronymPostHandler(_ req: Request) throws 
  -> EventLoopFuture<Response> {
  // 1
  let updateData = 
    try req.content.decode(CreateAcronymData.self)
  // 2
  return Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { acronym in
      // 3
      acronym.short = updateData.short
      acronym.long = updateData.long
      acronym.$user.id = updateData.userID
      // 4
      guard let id = acronym.id else {
        let error = Abort(.internalServerError)
        return req.eventLoop.future(error: error)
      }
      // 5
      let redirect = req.redirect(to: "/acronyms/\(id)")
      return acronym.save(on: req.db).transform(to: redirect)
  }
}
routes.get(
  "acronyms", ":acronymID", "edit",
   use: editAcronymHandler)
routes.post(
  "acronyms", ":acronymID", "edit", 
  use: editAcronymPostHandler)
<input type="text" name="short" class="form-control"
 id="short" #if(editing): value="#(acronym.short)" #endif/>
<input type="text" name="long" class="form-control"
 id="long" #if(editing): value="#(acronym.long)" #endif/>
<option value="#(user.id)"
 #if(editing): #if(acronym.user.id == user.id): 
   selected #endif #endif>
  #(user.name)
</option>
<button type="submit" class="btn btn-primary">
  #if(editing): Update #else: Submit #endif
</button>
<a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit"
 role="button">Edit</a>

Deleting acronyms

Unlike creating and editing acronyms, deleting an acronym only requires a single route. However, with web browsers there’s no simple way to send a DELETE request.

func deleteAcronymHandler(_ req: Request)
  -> EventLoopFuture<Response> {
  Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { acronym in
      acronym.delete(on: req.db)
        .transform(to: req.redirect(to: "/"))
  }
}
routes.post(
  "acronyms", ":acronymID", "delete", 
  use: deleteAcronymHandler)
<!-- 1 -->
<form method="post" action="/acronyms/#(acronym.id)/delete">
  <!-- 2 -->
  <a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit"
   role="button">Edit</a>&nbsp;
  <!-- 3 -->
  <input class="btn btn-danger" type="submit" value="Delete" />
</form>

Where to go from here?

In this chapter, you learned how to display your categories and how to create, edit and delete acronyms. You still need to complete your support for categories, allowing your users to put acronyms into categories and remove them. You’ll learn how to do that in the next chapter!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now