Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · 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

15. Beautifying Pages
Written by Tim Condon

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

In the previous chapter, you started building a powerful, dynamic website with Leaf. The web pages, however, only use simple HTML and aren’t styled — they don’t look great! In this chapter, you’ll learn how to use the Bootstrap framework to add styling to your pages. You’ll also learn how to embed templates so you only have to make changes in one place. Finally, you’ll also see how to serve files with Vapor.

Embedding templates

Currently, if you change the index page template to add styling, you’ll affect only that page. You’d have to duplicate the styling in the acronym detail page, and any other future pages.

Leaf allows you to embed templates into other templates. This enables you to create a “base” template that contains the code common to all pages and use that across your site.

In Resources/Views create a new file, base.leaf. Copy the contents of index.leaf into base.leaf. Remove everything between the <body> and </body> tags. This remaining code looks similar to the following:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>#(title) | Acronyms</title>
</head>
<body>

</body>
</html>

This forms your base template and will be the same for all pages. Between the <body> and </body> tags add:

#get(content)

This uses Leaf’s #get() tag to retrieve the content variable. To create this variable, open index.leaf replace its contents with the following:

#set("content") {
  <h1>Acronyms</h1>

  #if(acronyms) {
    <table>
      <thead>
        <tr>
          <th>
            Short
          </th>
          <th>
            Long
          </th>
        </tr>
      </thead>
      <tbody>
        #for(acronym in acronyms) {
          <tr>
            <td>
              <a href="/acronyms/#(acronym.id)">
                #(acronym.short)
              </a>
            </td>
            <td>#(acronym.long)</td>
          </tr>
        }
      </tbody>
    </table>
  } else {
    <h2>There aren't any acronyms yet!</h2>
  }
}

The changes made were:

  • Remove the HTML that now lives in base.leaf.
  • Wrap the remaining HTML with Leaf’s #set() tag and call the created variable content. You must wrap the variable name in #set() with quotations for Leaf to register it.

Finally at the bottom of the index.leaf add:

#embed("base")

This embeds the base.leaf template into the page and renders it. The base.leaf template uses #get() to get the content that’s set above. Save the files, then build and run. Open your browser and enter the URL http://localhost:8080/. The page renders as before:

Next, open acronym.leaf and change it to use the base template by replacing its contents with the following:

#set("content") {
  <h1>#(acronym.short)</h1>
  <h2>#(acronym.long)</h2>

  <p>Created by #(user.name)</p>
}

#embed("base")

Again, the changes made were:

  • Remove all the HTML that now lives in the base template.
  • Store the remaining HTML in the content variable, using Leaf’s #set() tag.
  • Embed the base template to bring in the common code and render content.

Save the file and, in your browser, navigate to an acronym page. The page renders as before with the new base template:

Note: In debug mode, you can refresh pages to pick up Leaf changes. In release mode, Leaf caches the pages for performance so you must restart your application to see changes.

Bootstrap

Bootstrap is an open-source, front-end framework for websites, originally built by Twitter. It provides easy-to-use components that you add to webpages. It’s a mobile-first library and makes it simple to build a site that works on screens of all sizes.

<div class="container mt-3">
  #get(content)
</div>

Navigation

The TIL website currently consists of two pages: a home page and an acronym detail page. As more and more pages are added, it can become difficult to find your way around the site. Currently, if you go to an acronym’s detail page, there is no easy way to get back to the home page! Adding navigation to a website makes the site more friendly for users.

#// 1
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
  #// 2
  <a class="navbar-brand" href="/">TIL</a>
  #// 3
  <button class="navbar-toggler" type="button"
   data-toggle="collapse" data-target="#navbarSupportedContent"
   aria-controls="navbarSupportedContent" aria-expanded="false"
   aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  #// 4
  <div class="collapse navbar-collapse"
   id="navbarSupportedContent">
    #// 5
    <ul class="navbar-nav mr-auto">
      #// 6
      <li class="nav-item #if(title == "Home page"){active}">
        <a href="/" class="nav-link">Home</a>
      </li>
    </ul>
  </div>
</nav>

Tables

Bootstrap provides classes to style tables with ease. Open index.leaf and replace the <table> tag with the following:

<table class="table table-bordered table-hover">
<thead class="thead-light">

Serving files

Almost every website needs to be able to host static files, such as images or stylesheets. Most of the time, you’ll do this using a CDN (Content Delivery Network) or a server such as Nginx or Apache. However, Vapor provides a FileMiddleware to serve files.

middlewares.use(FileMiddleware.self)
<img src="/images/logo.png"
 class="mx-auto d-block" alt="TIL Logo" />

Users

The website now has a page that displays all the acronyms and a page that displays an acronym’s details. Next, you’ll add pages to view all the users and a specific user’s information.

#// 1
#set("content") {
  #// 2
  <h1>#(user.name)</h1>
  #// 3
  <h2>#(user.username)</h2>

  #// 4
  #if(count(acronyms) > 0) {
    <table class="table table-bordered table-hover">
      <thead class="thead-light">
        <tr>
          <th>
            Short
          </th>
          <th>
            Long
          </th>
        </tr>
      </thead>
      <tbody>
        #// 5
        #for(acronym in acronyms) {
          <tr>
            <td>
              <a href="/acronyms/#(acronym.id)">
                #(acronym.short)</a>
            </td>
            <td>#(acronym.long)</td>
          </tr>
        }
      </tbody>
    </table>
  } else {
    <h2>There aren't any acronyms yet!</h2>
  }
}

#// 6
#embed("base")
struct UserContext: Encodable {
  let title: String
  let user: User
  let acronyms: [Acronym]
}
// 1
func userHandler(_ req: Request) throws -> Future<View> {
  // 2
  return try req.parameters.next(User.self)
    .flatMap(to: View.self) { user in
      // 3
      return try user.acronyms
        .query(on: req)
        .all()
        .flatMap(to: View.self) { acronyms in
          // 4
          let context = UserContext(
            title: user.name,
            user: user,
            acronyms: acronyms)
          return try req.view().render("user", context)
     }
  }
}
router.get("users", User.parameter, use: userHandler)
<p>Created by <a href="/users/#(user.id)/">#(user.name)</a></p>

#// 1
#set("content") {

  #// 2
  <h1>All Users</h1>

  #// 3
  #if(count(users) > 0) {
    <table class="table table-bordered table-hover">
      <thead class="thead-light">
        <tr>
          <th>
            Username
          </th>
          <th>
            Name
          </th>
        </tr>
      </thead>
      <tbody>
        #for(user in users) {
          <tr>
            <td>
              <a href="/users/#(user.id)">
                #(user.username)
              </a>
            </td>
            <td>#(user.name)</td>
          </tr>
        }
      </tbody>
    </table>
  } else {
    <h2>There aren't any users yet!</h2>
  }
}

#embed("base")
struct AllUsersContext: Encodable {
  let title: String
  let users: [User]
}
// 1
func allUsersHandler(_ req: Request) throws -> Future<View> {
  // 2
  return User.query(on: req)
    .all()
    .flatMap(to: View.self) { users in
      // 3
      let context = AllUsersContext(
        title: "All Users",
        users: users)
      return try req.view().render("allUsers", context)
  }
}
router.get("users", use: allUsersHandler)
<li class="nav-item #if(title == "All Users"){active}">
  <a href="/users" class="nav-link">All Users</a>
</li>

Sharing templates

The final thing to do in this chapter is to refactor our acronyms table. Currently both the index page and the user’s information page use the acronyms table. However, you’ve duplicated the code for the table. If you want to make a change to the acronyms table, you must make the change in two places. This is a problem templates should solve!

#if(count(acronyms) > 0) {
  <table class="table table-bordered table-hover">
    <thead class="thead-light">
      <tr>
        <th>Short</th>
        <th>Long</th>
      </tr>
    </thead>
    <tbody>
      #for(acronym in acronyms) {
        <tr>
          <td>
            <a href="/acronyms/#(acronym.id)">
              #(acronym.short)
            </a>
          </td>
          <td>#(acronym.long)</td>
        </tr>
      }
    </tbody>
  </table>
} else {
  <h2>There aren’t any acronyms yet!</h2>
}
#embed("acronymsTable")
#embed("acronymsTable")
let acronyms: [Acronym]
let context = IndexContext(title: "Home page", acronyms: acronyms)

Where to go from here?

Now that you’ve completed the chapter, the website for the TIL application looks much better! Using the Bootstrap framework allows you to style the site easily. This makes a better impression on users visiting your application.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now