In the companion tutorial, Templating With Leaf, you started building a powerful, dynamic website with . The web pages, however, only use simple HTML and aren’t styled — they don’t look great!

In this tutorial, 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.

Note: This tutorial assumes you have some experience with using Vapor to build web apps. See Getting Started with Server-side Swift with Vapor if you’re new to Vapor. You’ll need to at least use the steps in that tutorial to install the Vapor Toolbox in order to follow along with this tutorial. It will also benefit you greatly if you first work through the companion tutorial Templating With Leaf.

Getting Started

First, download the starter project for this tutorial using the Download Materials button at the top or bottom of the page. You can open the starter project in Xcode by running the following command from the root directory of the project:


vapor xcode -y

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="http://www.raywenderlich.com/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.

Before you run the app, you need to have PostgreSQL running on your system. You’ll run the Postgres server in a Docker container. Open `Terminal` and enter the following command:


docker run --name postgres -e POSTGRES_DB=vapor 
  -e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=password 
  -p 5432:5432 -d postgres    

To check that your database is running, enter the following in Terminal to list all active containers:


docker ps

To add acronyms into your database, use the instruction steps for the RESTed macOS app in the companion tutorial.

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

Acronyms page

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:

Running with 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 is an open-source, front-end framework for , 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.

To use Bootstrap go to getbootstrap.com and click Get Started. Bootstrap provides a CSS file to provide the styling and Javascript files that provide functionality for Bootstrap components. You need to include these files in all pages. Since you’ve created a base.leaf template, this is easy to do!

On the Get Started page, find the Starter template section.

In the starter template’s <head> section, copy the two <meta> tags — labeled “Required meta tags” — and the <link> tag for the CSS — labeled “Bootstrap CSS.” Replace the current <meta> tag in base.leaf with the new tags.

At the bottom of the starter template, copy the three <script> tags. Put them in the base.leaf template, under #get(content) and before the </body> tag.

Save the file then, in your browser, visit http://localhost:8080. You’ll notice the page looks a bit different. The page is now using Bootstrap’s styling, but you need to add Bootstrap-specific components to make your page really shine.

Open base.leaf and replace #get(content) with the following:


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

This wraps the page’s content in a container, which is a basic layout element in Bootstrap. The <div> also applies a margin at the top of the container. If you save the file and refresh your webpage, you’ll see the page now has some space around the sides and top, and no longer looks cramped:

Adding a container

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

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. Specify a root link to the homepage.
  3. Create a button that toggles the navigation bar for small screen sizes. This shows and hides the navbarSupportedContent section defined in the next element.
  4. Create a collapsable section for small screens.
  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. 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:

Nav bar on smaller screens

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

Nav bar on larger screens

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="thead-light">

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

Adding a table

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. Find the section that begins // Register middleware and add the following under var middlewares = MiddlewareConfig() (uncomment it if the line already exists):


middlewares.use(FileMiddleware.self)

This adds FileMiddleware to MiddlewareConfig to serve files. By default, this 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="http://www.raywenderlich.com/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:

Header image

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.

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


#// 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="http://www.raywenderlich.com/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")

Here’s what the new page does:

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

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

Here’s what the route handler does:

  1. Define the route handler for the user page that returns Future.
  2. Get the user from the request’s parameters and unwrap the future.
  3. Get the user’s acronyms using the computed property 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(router:):


router.get("users", User.parameter, 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="http://www.raywenderlich.com/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 page

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:


#// 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="http://www.raywenderlich.com/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")

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

Here’s what the new route handler does:

  1. Define a route handler for the “All Users” page that returns Future.
  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(router:):


router.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 #if(title == "All Users"){active}">
  <a href="http://www.raywenderlich.com/users" class="nav-link">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 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!

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="thead-light">
      <tr>
        <th>Short</th>
        <th>Long</th>
      </tr>
    </thead>
    <tbody>
      #for(acronym in acronyms) {
        <tr>
          <td>
            <a href="http://www.raywenderlich.com/acronyms/#(acronym.id)">
              #(acronym.short)
            </a>
          </td>
          <td>#(acronym.long)</td>
        </tr>
      }
    </tbody>
  </table>
} else {
  <h2>There aren’t any acronyms yet!</h2>
}

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


#embed("acronymsTable")

Like using base.leaf, this embeds 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:


#embed("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 3.

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!



Source link https://www.raywenderlich.com/1004734---websites-with-leaf-and-bootstrap

LEAVE A REPLY

Please enter your comment!
Please enter your name here