36

Templating Vapor Applications with Leaf [FREE]

 5 years ago
source link: https://www.tuicool.com/articles/hit/eiyIfuQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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, and along the way you’ll learn how to use Leaf 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 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), which you can learn more about inthis tutorial.

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

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 . Replace its contents with the following:

// swift-tools-version:4.0
import PackageDescription

let package = Package(
  name: "TILApp",
  dependencies: [
    .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
    .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
    .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0")
  ],
  targets: [
    .target(name: "App", dependencies: ["FluentPostgreSQL", "Vapor", "Leaf"]),
    .target(name: "Run", dependencies: ["App"]),
    .testTarget(name: "AppTests", dependencies: ["App"]),
  ]
)

The changes made were:

  • Make the TILApp package depend upon the Leaf package.
  • Make the App target depend upon the Leaf target to ensure it links properly.

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

Finally, you must create new routes for the website. Create a new controller to contain these routes. In Terminal, type the following:

touch Sources/App/Controllers/WebsiteController.swift

With everything configured, regenerate the Xcode project to start using Leaf. In Terminal, type the following:

vapor xcode -y

Rendering a Page

Open WebsiteController.swift and 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(router: Router) throws {
    // 3
    router.get(use: indexHandler)
  }

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

Here’s what this does:

  1. Declare a new WebsiteController type that conforms to RouteCollection .
  2. Implement boot(router:) 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 Future .
  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 this file 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 add the following to the end of routes(_:) :

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

Next, you must register the Leaf service. Open configure.swift and add the following to the imports section below import Vapor :

import Leaf

Next, after try services.register(FluentPostgreSQLProvider()) , add the following:

try services.register(LeafProvider())

Using the generic req.view() to obtain a 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 to verify against, rather than parsing HTML output in your test cases.

req.view() asks Vapor to provide a type that conforms to ViewRenderer . TemplateKit — the module that Leaf is built upon — provides PlaintextRenderer and Leaf provides LeafRenderer . In configure.swift add the following to the end of configure(_:_:_:) :

config.prefer(LeafRenderer.self, for: ViewRenderer.self)

This tells Vapor to use LeafRenderer when asked for a ViewRenderer type.

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

Now build and run the application in Xcode, 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-650x320.png

Injecting Variables

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

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

This extracts a parameter called title using the #() Leaf function. Like a lot of Vapor, Leaf uses Codable to handle data.

At the bottom of WebsiteController.swift , add the following, to create a new type to contain the title:

struct IndexContext: Encodable {
  let title: String
}

As data only flows to Leaf, you only need to conform to Encodable . IndexContext is the data for your view, similar to a view model in the MVVM design pattern. Next, change indexHandler(_:) to pass an IndexContext to the template. Replace the implementation with the following:

func indexHandler(_ req: Request) throws -> Future<View> {
  // 1
  let context = IndexContext(title: "Home page")
  // 2
  return try 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:

injected-title-650x320.png

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

This is an optional array of acronyms; it can be nil as there may be no acronyms in the database.

Next, change indexHandler(_:) to get all the acronyms and insert them in the IndexContext .

Replace the implementation once more with the following:

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

Here’s what this does:

  1. Use a Fluent query to get all the acronyms from the database. You can learn more about Fluent inthis tutorial.
  2. Add the acronyms to IndexContext if there are any, otherwise set the variable to nil . Leaf can check for nil in the template.

Finally open index.leaf and change the parts between the <body> tags to the following:

#// 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>
      }
    </tbody>
  </table>
#// 6
} else {
  <h2>There aren’t any acronyms yet!</h2>
}

Here’s what the new code does:

  1. Declare a new heading, “Acronyms”.
  2. Use Leaf’s #if() tag to see if the acronyms variable is set. #if() can validate variables for nullability, work on booleans or even evaluate expressions.
  3. If acronyms is set, create an HTML table. The table has a header row — <thead> — with two columns, Short and Long .
  4. Use Leaf’s #for() tag to loop through all the acronyms. This works in a similar way to Swift’s for loop.
  5. Create a row for each acronym. Use Leaf’s #() function to extract the variable. Since everything is Encodable , you can use dot notation to access properties on acronyms, just like Swift!
  6. If there are no acronyms, print a suitable message.

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

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

no-acronyms-650x320.png

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

acronyms-table-650x320.png

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

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

users-639x500.png

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:

acronyms-639x500.png

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-618x500.png

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
}

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 under indexHandler(_:) :

// 1
func acronymHandler(_ req: Request) throws -> Future<View> {
  // 2
  return try req.parameters.next(Acronym.self).flatMap(to: View.self) { acronym in
      // 3
      return acronym.user.get(on: req).flatMap(to: View.self) { user in
          // 4
          let context = AcronymContext(title: acronym.short, acronym: acronym,
                                       user: user)
          return try req.view().render("acronym", context)
      }
  }
}

Here’s what this route handler does:

  1. Declare a new route handler, acronymHandler(_:) , that returns Future .
  2. Extract the acronym from the request’s parameters and unwrap the result.
  3. Get the user for acronym and unwrap the result.
  4. Create an AcronymContext that contains the appropriate details and render the page using the acronym.leaf template.

Finally register the route at the bottom of boot(router:) :

router.get("acronyms", Acronym.parameter, use: acronymHandler)

This registers the acronymHandler route for /acronyms/ < ACRONYM ID> , similar to the API. 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. Declare an HTML5 page like index.leaf .
  2. Set the title to the value that’s passed in.
  3. Print the acronym’s short property in an <h1> heading.
  4. Print the acronym’s long property in an <h2> heading.
  5. Print the acronym’s user in a <p> block

Finally, change index.leaf so you can navigate to the page. Replace the first column in the table for each acronym ( <td>#(acronym.short)</td> ) 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:

index-page-with-links-650x320.png

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-page-650x320.png

Where to Go From Here?

This tutorial introduced Leaf and showed you how to start building a dynamic website to consume 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 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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK