Home Server-Side Swift Tutorials

Templating Vapor Applications with Leaf

Use Leaf, Vapor’s templating engine, to build a front-end website to consume your server-side Swift API!

5/5 1 Rating

Version

  • Swift 5, macOS 11, Xcode 12
Update Note: Tim Condon updated this tutorial for Vapor 4. Tim Condon also wrote the original tutorial.

Creating APIs in Vapor and building out an iOS app as the front end may be something you’re fairly familiar with, but you can create nearly any type of client to consume your API. In this tutorial, you’ll create a website-based client against your Vapor API. You’ll use Leaf, Vapor’s templating language, to create dynamic websites in Vapor applications.

You’ll use a Vapor app named TIL (Today I Learned) that hosts acronyms entered by users.

Note: This tutorial assumes you have some experience with using Vapor to build web apps.

See Getting Started with Server-side Swift with Vapor 4 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.

You’ll also need some familiarity with Docker (and have it installed). If you need to refresh your Docker knowledge, see Docker on macOS: Getting Started.

What is Leaf?

Leaf is Vapor’s templating language. A templating language allows you to pass information to a page so it can generate the final HTML server-side without knowing everything up front.

For example, in the TIL application, you don’t know every acronym that users will create when you deploy your application. Templating allows you handle this with ease.

Templating languages also allow you to reduce duplication in your webpages. Instead of multiple pages for acronyms, you create a single template and set the properties specific to displaying a particular acronym. If you decide to change the way you display an acronym, you only need to make the change to your code once for all pages to show the new format.

Finally, templating languages allow you to embed templates into other templates. For example, if you have navigation on your website, you can create a single template that generates the code for your navigation so that all templates that need navigation don’t duplicate code.

Getting Started

Download the starter project for this tutorial using the “Download Materials” button at the top or bottom of this page.

To use Leaf, you need to add it to your project as a dependency.

Using the starter project from this tutorial, open Package.swift.

Update Package.swift so that:

  1. The TILApp package depends on the Leaf package
  2. The App target depends on the Leaf target to ensure it links properly

Your Package.swift should look like the following:

// swift-tools-version:5.2
import PackageDescription

let package = Package(
  name: "TILApp",
  platforms: [
    .macOS(.v10_15)
  ],
  dependencies: [
    // 💧 A server-side Swift web framework.
    .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
    .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
    .package(url: "https://github.com/vapor/fluent-postgres-driver.git", 
             from: "2.0.0"),
    // Leaf package dependency
    .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0")
  ],
  targets: [
    .target(
      name: "App",
      dependencies: [
        .product(name: "Fluent", package: "fluent"),
        .product(
          name: "FluentPostgresDriver", 
          package: "fluent-postgres-driver"),
        .product(name: "Vapor", package: "vapor"),
        // Leaf target dependency
        .product(name: "Leaf", package: "leaf")
      ],
      swiftSettings: [
        .unsafeFlags(
          ["-cross-module-optimization"], 
          .when(configuration: .release))
      ]
    ),
    .target(name: "Run", dependencies: [.target(name: "App")]),
    .testTarget(name: "AppTests", dependencies: [
      .target(name: "App"),
      .product(name: "XCTVapor", package: "vapor"),
    ])
  ]
)

By default, Leaf expects templates to be in the Resources/Views directory.

In Terminal, type the following to create these directories:

mkdir -p Resources/Views

Rendering a Page

In Xcode, create a new Swift file named WebsiteController.swift in Sources/App/Controllers.

This controller will hold all the website routes, such as one that will return a template that contains an index of all acronyms.

Open WebsiteController.swift and replace its contents with the following:

import Vapor
import Leaf

// 1
struct WebsiteController: RouteCollection {
  // 2
  func boot(routes: RoutesBuilder) throws {
    // 3
    routes.get(use: indexHandler)
  }

  // 4
  func indexHandler(_ req: Request) -> EventLoopFuture<View> {
    // 5
    return req.view.render("index")
  }
}

Here’s what this does:

  1. Declare a new WebsiteController type that conforms to RouteCollection.
  2. Implement boot(routes:) as required by RouteCollection.
  3. Register indexHandler(_:) to process GET requests to the router’s root path, i.e., a request to /.
  4. Implement indexHandler(_:) that returns EventLoopFuture<View>.
  5. Render the index template and return the result. You’ll learn about req.view in a moment.

Leaf generates a page from a template called index.leaf inside the Resources/Views directory.

Note that the file extension’s not required by the render(_:) call.

Create Resources/Views/index.leaf and insert the following:

<!DOCTYPE html>
<!-- 1 -->
<html lang="en">
<head>
  <meta charset="utf-8" />
  <!-- 2 -->
  <title>Hello World</title>
</head>
<body>
  <!-- 3 -->
  <h1>Hello World</h1>
</body>
</html>

Here’s what this file does:

  1. Declare a basic HTML 5 page with a <head> and <body>.
  2. Set the page title to Hello World — this is the title displayed in a browser’s tab.
  3. Set the body to be a single <h1> title that says Hello World.
Note: You can create your .leaf files using any text editor you choose, including Xcode. If you use Xcode, choose Editor ▸ Syntax Coloring ▸ HTML in order to get proper highlighting of elements and indentation support.

You must register your new WebsiteController.

Open routes.swift and in route(_:) replace the code

app.get { req in
  return "It works!"
}

with the following:

let websiteController = WebsiteController()
try app.register(collection: websiteController)

Vapor now uses WebsiteController to handle the root / route.

Configuring Leaf

Next, you must tell Vapor to use Leaf.

Open configure.swift and add the following to the imports section below import Vapor:

import Leaf

Using the generic req.view to obtain the view renderer allows you to switch to different templating engines easily.

While this may not be useful when running your application, it’s extremely useful for testing. For example, it allows you to use a test renderer to produce plain text, rather than having to parse HTML, in your test cases.

req.view asks Vapor to provide a type that conforms to ViewRenderer.

Vapor only provides PlaintextRenderer, but LeafKit — the module Leaf is built upon — provides LeafRenderer.

In configure.swift, add the following after try app.autoMigrate().wait():

app.views.use(.leaf)

This tells Vapor to use Leaf when rendering views and LeafRenderer when asked for a ViewRenderer type.

Your First View

Next, you must tell Vapor where the app is running, because you might run the App from a standalone Xcode project or inside a workspace. To do this, set a custom working directory in Xcode.

Option-Click the Run button in Xcode to open the scheme editor.

On the Options tab, click to enable Use custom working directory and select the directory where the Package.swift file lives:

Set the custom working directory in Xcode

Finally, 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_database \
  -e POSTGRES_USER=vapor_username -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres    

Build and run the application, remembering to choose the Run scheme, then open your browser.

Enter the URL http://localhost:8080 and you’ll receive the page generated from the template:

Hello World page

Dynamic Views with Variables

Right now, the template is just a static page and not very impressive. Let’s make it more dynamic by having it display the title as defined by the route!

Open index.leaf and change the <title> line to the following:

<title>#(title) | Acronyms</title>

This will display the value of a variable called title using the #() Leaf function.

Like much of Vapor, Leaf takes advantage of Codable to handle data. However, Leaf only accepts input data, so your models only need to conform to Encodable. These data models are much like view models in the MVVM design pattern.

At the bottom of WebsiteController.swift, create a new struct that will contain the title, like the following:

struct IndexContext: Encodable {
  let title: String
}

Next, change indexHandler(_:) to pass an IndexContext to the template as the second parameter. The method’s implementation should look like the following:

func indexHandler(_ req: Request) -> EventLoopFuture<View> {
    // 1
    let context = IndexContext(title: "Home page")
    // 2
    return req.view.render("index", context)
}

Here’s what the new code does:

  1. Create an IndexContext containing the desired title.
  2. Pass the context to Leaf as the second parameter to render(_:_:).

Build and run, then refresh the page in the browser. You’ll see the updated title:

Updated title

Advanced Templates

So far the the current index.leaf template is only using the #() Leaf function to display a variable’s value. Leaf’s functions are referred to as Tags.

There are more advanced tags, such as #if() and #for(), and you can even write your own – but we’re going to focus on the two just mentioned in the rest of this tutorial.

Conditional Rendering

Since the index.leaf template is for home page of the TIL website, it should display a list of all the acronyms. However, it also needs to handle the possibility that no acronyms are in the database.

This is a perfect use case for Leaf’s #if() tag!

In WebsiteController.swift, add a new property to IndexContext underneath title:

let acronyms: [Acronym]?

This property will store the list of acronyms in the database or nil if none are found. An Optional will represent the empty state because Leaf’s #if() tag can check for nil values as well as evaluating boolean expressions.

Next, update WebsiteController.indexHandler(_:) to get all the acronyms from the database using Fluent and insert them in the IndexContext.

Replace the implementation once more with the following:

func indexHandler(_ req: Request) -> EventLoopFuture<View> {
    // 1
    Acronym.query(on: req.db).all().flatMap { acronyms in
        // 2
        let acronymsData = acronyms.isEmpty ? nil : acronyms
        let context = IndexContext(
          title: "Home page",
          acronyms: acronymsData)
        return req.view.render("index", context)
    }
}

Here’s what this does:

  1. Use a Fluent query to get all the acronyms from the database.

    Note: If you’re unfamiliar with Fluent, review the tutorial Using Fluent and Persisting Models in Vapor.

  2. Add the acronyms to IndexContext if there are any, otherwise set the variable to nil.

Finally open index.leaf and replace the parts between the <body> tags with the following:

<!-- 1 -->
<h1>Acronyms</h1>

<!-- 2 -->
#if(acronyms):
  <!-- 3 -->
  <table>
    <thead>
      <tr>
        <th>Short</th>
        <th>Long</th>
      </tr>
    </thead>
    <tbody>
      <!-- 4 -->
    </tbody>
  </table>
<!-- 5 -->
#else:
  <h2>There aren’t any acronyms yet!</h2>
#endif

Here’s what the updated template does:

  1. Declare a new heading, “Acronyms”.
  2. Uses Leaf’s #if() tag to see if the acronyms variable is set.
  3. If acronyms is set, create an HTML table. The table has a header row — <thead> — with two columns, Short and Long.
  4. Leave an empty place to return to in the next section that will loop through all acronyms to display using Leaf’s #for tag.
  5. If there are no acronyms, print a suitable message.

Looping Through Values

Since the template is displaying an unknown amount of acronyms, it needs to loop through the provided array and display each element’s values.

This is where Leaf’s #for() tag is used. It works similarly to Swift’s for loop.

In index.leaf, where the table body – <tbody> – was left blank, add the following code:

<!-- 1 -->
#for(acronym in acronyms):
  <!-- 2 -->
  <tr>
    <!-- 3 -->
    <td>#(acronym.short)</td>
    <td>#(acronym.long)</td>
  </tr>
<!-- 4 -->
#endfor

Here’s what the new template code does:

  1. Uses Leaf’s #for() tag to loop through all of the acronyms.
  2. Creates a table row — <tr> — for each acronym.
  3. Creates a table cell – <td> – to display each property of the acronym using Leaf’s #() tag.
  4. Tells Leaf that the #for() tag has finished.

Build and run, then refresh the page in the browser.

If you have no acronyms in the database, you’ll see the correct message:

App with no acronyms

If there are acronyms in the database, you’ll see them in the table:

App withacronyms

Inserting Acronyms

To test the page with acronyms, you can use the RESTed macOS app to add acronyms into the database, as described in our Fluent tutorial.

First, add a user into the database by sending a POST request to http://localhost:8080/api/users in RESTed as follows:

Adding a User

Then, grab the id of the new user, and add it (as parameter userID) into another POST request to http://localhost:8080/api/acronyms that is setup as follows:

Add an acronym

Once you’ve sent the request to add a new acronym, you can refresh your page in the browser to see acronyms pulled from the database.

Acronym added

Acronym Detail Page

Now, you need a page to show the details for each acronym.

At the end of WebsiteController.swift, create another struct to pass as input to Leaf input which will represent all the details of an acronym:

struct AcronymContext: Encodable {
  let title: String
  let acronym: Acronym
  let user: User
}

This AcronymContext contains a title for the page, the acronym itself and the user who created the acronym.

Create the following route handler for the acronym detail page after WebsiteController.indexHandler(_:):

// 1
func acronymHandler(_ req: Request) -> EventLoopFuture<View> {
  // 2
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      // 3
      acronym.$user.get(on: req.db).flatMap { user in
        // 4
        let context = AcronymContext(
          title: acronym.short, 
          acronym: acronym, 
          user: user)
        return req.view.render("acronym", context)
      }
  }
}

Here’s what this route handler does:

  1. Declares a new route handler, acronymHandler(_:), that returns EventLoopFuture<View>.
  2. Searches for the acronym with the request’s parameters and unwraps the result. If no acronym is found with the given ID, it will return a 404 Not Found.
  3. Gets the user that created the acronym and unwraps the result.
  4. Creates an AcronymContext that contains the appropriate details and renders the page using the acronym.leaf template.

Finally register the route handler at the bottom of WebsiteController.boot(routes:) for the /acronyms/<ACRONYM ID> route path:

routes.get("acronyms", ":acronymID", use: acronymHandler)

Create the acronym.leaf template inside the Resources/Views directory and open the new file and add the following:

<!DOCTYPE html>
<!-- 1 -->
<html lang="en">
<head>
  <meta charset="utf-8" />
  <!-- 2 -->
  <title>#(title) | Acronyms</title>
</head>
<body>
  <!-- 3 -->
  <h1>#(acronym.short)</h1>
  <!-- 4 -->
  <h2>#(acronym.long)</h2>

  <!-- 5 -->
  <p>Created by #(user.name)</p>
</body>
</html>

Here’s what this template does:

  1. Declares an HTML5 page like index.leaf.
  2. Sets the title to the value that’s passed in.
  3. Prints the acronym’s short property in an <h1> heading.
  4. Prints the acronym’s long property in an <h2> heading.
  5. Displays the user’s name that created the acronym in a <p> block

Now that the detail template is made, the index.leaf template can be updated to provide a link to navigate to a given acronym’s detail page.

Open index.leaf and replace the first table cell – <td>#(acronym.short)</td> – for each acronym with:

<td><a href="/acronyms/#(acronym.id)">#(acronym.short)</a></td>

This wraps the acronym’s short property in an HTML <a> tag, which is a link. The link sets the URL for each acronym to the route registered above.

Build and run, then refresh the page in the browser:

Acronyms with links

You’ll see that each acronym’s short form is now a link.

Click the link and the browser navigates to the acronym’s page:

Acronym detail page

Where to Go From Here?

This tutorial introduced Leaf and showed you how to start building a dynamic website to display data provided by your Vapor API.

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 comment section below!

Average Rating

5/5

Add a rating for this content

1 rating

More like this

Contributors

Comments