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

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

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 it 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. The remaining code should look 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:

#import("content")

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

#extend("base"):

#endextend

This tells Leaf to extend the base template when rendering index.leaf. base.leaf requires one variable, content. Add the following, in between #extend and #endextend to define content:

#export("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>
        #endfor
      </tbody>
    </table>
  #else:
    <h2>There aren’t any acronyms yet!</h2>
  #endif
#endexport

This takes the HTML specific to index.leaf and wraps it in an #export tag. When Leaf renders base.leaf as required by index.leaf, it takes content and inserts it into the base template.

Save the files, then build and run. Open your browser and enter the URL http://localhost:8080/. The page renders as before:

Note: If you started fresh with the starter project from this chapter, you’ll need to set a custom working directory in Xcode. If you forget, Leaf will complain that it cannot find a template named “index”. See Chapter 14, “Templating with Leaf”, for more information.

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

#extend("base"):
  #export("content"):
    <h1>#(acronym.short)</h1>
    <h2>#(acronym.long)</h2>

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

Again, the changes made were:

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

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 web pages. 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">
  #import("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 it 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 #endif">
          <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 style sheets. 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 module to serve files.

app.middleware.use(
  FileMiddleware(publicDirectory: app.directory.publicDirectory)
)
<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 -->
#extend("base"):
  <!-- 2 -->
  #export("content"):
    <!-- 3 -->
    <h1>#(user.name)</h1>
    <!-- 4 -->
    <h2>#(user.username)</h2>
    
    <!-- 5 -->
    #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>
          <!-- 6 -->
          #for(acronym in acronyms):
            <tr>
              <td>
                <a href="/acronyms/#(acronym.id)">
                  #(acronym.short)
                </a>
              </td>
              <td>#(acronym.long)</td>
            </tr>
          #endfor
        </tbody>
      </table>
    #else:
      <h2>There aren’t any acronyms yet!</h2>
    #endif
  #endexport
#endextend
struct UserContext: Encodable {
  let title: String
  let user: User
  let acronyms: [Acronym]
}
// 1
func userHandler(_ req: Request) 
  -> EventLoopFuture<View> {
    // 2
    User.find(req.parameters.get("userID"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap { user in
        // 3
        user.$acronyms.get(on: req.db).flatMap { acronyms in
          // 4
          let context = UserContext(
            title: user.name,
            user: user,
            acronyms: acronyms)
          return req.view.render("user", context)
        }
    }
}
routes.get("users", ":userID", use: userHandler)
<p>Created by <a href="/users/#(user.id)/">#(user.name)</a></p>

Listing all users

The final page for you to implement in this chapter displays a list of all users. Create a new file in Resources/Views called allUsers.leaf. Open the file and add the following:

#extend("base"):
  <!-- 1 -->
  #export("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>
          #endfor
        </tbody>
      </table>
    #else:
      <h2>There aren’t any users yet!</h2>
    #endif
  #endexport
#endextend
struct AllUsersContext: Encodable {
  let title: String
  let users: [User]
}
// 1
func allUsersHandler(_ req: Request) 
  -> EventLoopFuture<View> {
    // 2
    User.query(on: req.db)
      .all()
      .flatMap { users in
        // 3
        let context = AllUsersContext(
          title: "All Users",
          users: users)
        return req.view.render("allUsers", context)
    }
}
routes.get("users", use: allUsersHandler)
<li class="nav-item #if(title == "All Users"): active #endif">
  <a href="/users" class="nav-link">All Users</a>
</li>

Sharing templates

The final thing to do in this chapter is to refactor your 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>
      #endfor
    </tbody>
  </table>
#else:
  <h2>There aren’t any acronyms yet!</h2>
#endif
#extend("acronymsTable")
#extend("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