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 3 of 3 of this article. Click here to view the first page.

Listing All Users

The final page for you to implement in this tutorial 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="table-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

Here’s what the new page does:

  1. Set the content variable for the base template.
  2. Display an <h1> heading for “All Users”.
  3. See if the context provides any users. If so, create a table that contains two columns: username and name. This is like the acronyms table.

Save the file and open WebsiteController.swift in Xcode. At the bottom of the file, create a new context for the page:

struct AllUsersContext: Encodable {
  let title: String
  let users: [User]
}

This context contains a title and an array of users. Next, add the following below userHandler(_:) to create a route handler for the new page:

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

Here’s what the new route handler does:

  1. Define a route handler for the “All Users” page that returns EventLoopFuture<View>.
  2. Get the users from the database and unwrap the future.
  3. Create an AllUsersContext and render the allUsers.leaf template, then return the result.

Next, register the route at the bottom of boot(routes:):

routes.get("users", use: allUsersHandler)

This registers the route for /users/, like the API. Build and run, then open base.leaf. Add a link to the new page in the navigation bar above the </ul> tag:

<li class="nav-item">
  <a class="nav-link #if(title == "All Users"): active #endif" 
   aria-current="page" href="/users">All Users</a>
</li>

This adds a link to /users and sets the link to active if the page title is “All Users”.

Save the file and open your browser.

Go to http://localhost:8080 and you’ll see a new link in the navigation bar. Click All Users and you’ll see your new “All Users” page:

All Users Page

Sharing Templates

The final thing to do in this tutorial is to refactor the 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!

Create a new file in Resources/Views called acronymsTable.leaf. Open user.leaf and copy the table code into acronymsTable.leaf. The new file should contain the following:

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

In user.leaf, remove the code that’s now in acronymsTable.leaf and insert the following in it’s place:

#extend("acronymsTable")

Like using base.leaf, this extends the contents of acronymsTable.leaf into your template. Save the file and in your browser, navigate to a user’s page — it should show the user’s acronyms, like before.

Open index.leaf and remove #if(acronyms) and all the code inside it. Again, insert the following in its place:

#extend("acronymsTable")

Save the file. Finally, open WebsiteController.swift and change IndexContext so acronyms is no longer optional:

let acronyms: [Acronym]

acronymsTable.leaf checks the count of the array to determine whether to show a table or not. This is easier to read and understand. In indexHandler(_:), remove acronymsData and pass the array of acronyms directory to IndexContext:

let context = IndexContext(title: "Home page", acronyms: acronyms)

Build and run the application and navigate to http://localhost:8080 in your browser. All the acronyms should still be there.

Where to Go From Here?

You can download the completed project from this tutorial using the Download Links buttons at the top and bottom of this page.

Now that you’ve completed the tutorial, 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.

If you enjoyed this tutorial, why not check out our full-length book on Vapor development: Server-Side Swift with Vapor?

If you’re a beginner to web development, but have worked with Swift for some time, you’ll find it’s easy to create robust, fully-featured web apps and web APIs with Vapor 4.

Whether you’re looking to create a backend for your iOS app, or want to create fully-featured web apps, Vapor is the perfect platform for you.

Questions or comments on this tutorial? Leave them in the comments below!