Beautifying Templated Websites with Leaf and Bootstrap

Use the Bootstrap framework to add styling to your templated Leaf pages, and see how to serve files with Vapor in this server-side Swift tutorial! By Tim Condon.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.

HTML defines a <nav> element to denote the navigation section of a page. Bootstrap supplies classes and utilities to extend this for styling and mobile support. Open base.leaf and add the following above <div class="container mt-3">:

<!-- 1 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container-fluid">
    <!-- 2 -->
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" 
     data-bs-target="#navbarSupportedContent" 
     aria-controls="navbarSupportedContent" aria-expanded="false" 
     aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <!-- 3 -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <!-- 4 -->
      <a class="navbar-brand" href="/">TIL</a>
      <!-- 5 -->
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <!-- 6 -->
        <li class="nav-item">
          <a class="nav-link #if(title == "Home page"): active #endif" aria-current="page" href="/">Home</a>
        </li>
      </ul>
    </div>
  </div>
</nav>

Here’s what this new code does:

  1. Define a <nav> element with some class names for styling. Bootstrap uses these classes to specify a Bootstrap navigation bar, allow the navigation bar to be full size in medium-sized screens, and apply a dark theme to the bar.
  2. Create a button that toggles the navigation bar for small screen sizes. This shows and hides the navbarSupportedContent section defined in the next element.
  3. Create a collapsable section for small screens.
  4. Specify a root link to the homepage.
  5. Define a list of navigation links to display. Bootstrap styles these nav-item list items for a navigation bar instead of a standard bulleted list.
  6. Add a link for the home page. This uses Leaf’s #if tag to check the page title. If the title is set to “Home page” then Leaf adds the active class to the item link. This styles the link differently when on that page.

Save the file and refresh the page in the browser. The page is starting to look professional! For small screens you’ll get a toggle button, which opens the navigation links:

TIL with Bootstrap on a small screen

On larger screens, the navigation bar shows all the links:

TIL with Bootstrap on large screen

Now when you’re on an acronym’s detail page, you can use the navigation bar to return to the home screen!

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">

This adds the following Bootstrap classes to the table:

  • table: apply standard Bootstrap table styling.
  • table-bordered: add a border to the table and table cells.
  • table-hover: enable a hover style on table rows so users can more easily see what row they are looking at.

Next, replace the <thead> tag with the following:

<thead class="table-light">

This makes the table head stand out. Save the file and refresh the page. The home page now looks even more professional!

Bootstrap tables

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.

To enable this, open configure.swift in Xcode. At the start of configure(_:) add the following (uncomment it if the line already exists)::

app.middleware.use(
  FileMiddleware(publicDirectory: app.directory.publicDirectory)
)

This adds FileMiddleware to the Application’s middleware to serve files. It serves files in the Public directory in your project. For example, if you had a file in Public/styles called stylesheet.css this would be accessible from the path /styles/stylesheet.css.

The starter project for this tutorial contains an images directory in the Public folder, with a logo inside for the website. Build and run, then open index.leaf.

Above <h1>Acronyms</h1> add the following:

<img src="/images/logo.png"
 class="mx-auto d-block" alt="TIL Logo" />

This adds an <img> tag — for an image — to the page. The page loads the image from /images/logo.png which corresponds to Public/images/logo.png served by the FileMiddleware. The mx-auto and d-block classes tell Bootstrap to align the image centrally in the page. Finally the alt value provides an alternative title for the image. Screen readers uses this to help accessibility users.

Save the file and visit http://localhost:8080 in the browser. The home page now displays the image, putting the final touches on the page:

TIL Homepage with Logo

Display Users Information

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.

Create a new file in Resources/Views called user.leaf. Implement the template like so:

<!-- 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="table-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

Here’s what the new page does:

  1. Extend the base template to bring in all the common HTML.
  2. Set the content variable for the base template.
  3. Display the user’s name in an <h1> heading.
  4. Display the user’s username in an <h2> heading.
  5. Use a combination of Leaf’s #if tag and count tag to see if the user has any acronyms.
  6. Display a table of acronyms from the injected acronyms property. This table is identical to the one in the index.leaf template.

In Xcode, open WebsiteController.swift. At the bottom of the file create a new context for the user page:

struct UserContext: Encodable {
  let title: String
  let user: User
  let acronyms: [Acronym]
}

This context has properties for:

  • The title of the page, which is the user’s name.
  • The user object to which the page refers.
  • The acronyms created by this user.

Next, add the following handler below acronymHandler(_:) for this page:

// 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)
        }
    }
}

Here’s what the route handler does:

  1. Define the route handler for the user page that returns EventLoopFuture<View>.
  2. Get the user from the request’s parameters and unwrap the future.
  3. Get the user’s acronyms using the @Children property wrapper’s project value and unwrap the future.
  4. Create a UserContext, then render user.leaf, returning the result. In this case, you’re not setting the acronyms array to nil if it’s empty. This is not required as you’re checking the count in template.

Finally, add the following to register this route at the end of boot(routes:):

routes.get("users", ":userID", use: userHandler)

This registers the route for /users/<USER ID>, like the API. Build and run.

Next, open acronym.leaf to add a link to the new user page by replacing <p>Created by #(user.name)</p> with the following:

<p>Created by <a href="/users/#(user.id)/">#(user.name)</a></p>

Save the file then open your browser. Go to http://localhost:8080 and click one of the acronyms. The page now displays a link to the creating user’s page. Click the link to visit your newly created page:

User's page with their acronyms