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

14. Templating with Leaf
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 a previous section of the book, you learned how to create an API using Vapor and Fluent. You then learned how to create an iOS client to consume the API. In this section, you’ll create another client — a website. You’ll see how to use Leaf to create dynamic websites in Vapor applications.

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 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 change your code in one place and all acronym pages will 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. You embed the navigation template in all templates that need navigation rather than duplicating code.

Configuring Leaf

To use Leaf, you need to add it to your project as a dependency. Using the TIL application from Chapter 11, “Testing”, or the starter project from this chapter, open Package.swift. Replace its contents with 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"),
    .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"),
        .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"),
    ])
  ]
)
mkdir -p Resources/Views

Rendering a page

Open WebsiteController.swift and replace its contents with the following, to create a new type to hold all the website routes and a route that returns an index template:

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")
  }
}
<!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>
let websiteController = WebsiteController()
try app.register(collection: websiteController)
app.get { req in
  return "It works!"
}
import Leaf
app.views.use(.leaf)

Injecting variables

The template is currently just a static page and not at all impressive! To make the page more dynamic, open index.leaf and change the <title> line to the following:

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

Using tags

The home page of the TIL website should display a list of all the acronyms. Still in WebsiteController.swift, add a new property to IndexContext underneath title:

let acronyms: [Acronym]?
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)
    }
}
<!-- 1 -->
<h1>Acronyms</h1>

<!-- 2 -->
#if(acronyms):
  <!-- 3 -->
  <table>
    <thead>
      <tr>
        <th>Short</th>
        <th>Long</th>
      </tr>
    </thead>
    <tbody>
      <!-- 4 -->
      #for(acronym in acronyms):
        <tr>
          <!-- 5 -->
          <td>#(acronym.short)</td>
          <td>#(acronym.long)</td>
        </tr>
      #endfor
    </tbody>
  </table>
<!-- 6 -->
#else:
  <h2>There aren’t any acronyms yet!</h2>
#endif

Acronym detail page

Now, you need a page to show the details for each acronym. At the end of WebsiteController.swift, create a new type to hold the context for this page:

struct AcronymContext: Encodable {
  let title: String
  let acronym: Acronym
  let user: User
}
// 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)
        }
    }
}
routes.get("acronyms", ":acronymID", use: acronymHandler)
<!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>
<td><a href="/acronyms/#(acronym.id)">#(acronym.short)</a></td>

Where to go from here?

This chapter introduced Leaf and showed you how to start building a dynamic website. The next chapters in this section show you how to embed templates into other templates, beautify your application and create acronyms from the website.

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