Leaf Part 1 - Getting Started

Written by Tim on Sunday, November 12, 2017

This is the sixth tutorial of a series of tutorials on Vapor. For more, click here

Leaf

Up until now, everything we have been doing has been focused on an API and all the requests we have been sending have been sent through a REST client. This is great for iOS apps (and other mobile clients) but at some point we are going to want to have a presence on the web and a way of using the application through a website. We can serve HTML files via Vapor relatively easily but we will want a way to display content from our application. The way to achieve this is to use a templating language where we can use a template to display different things of the same type - for instance, a template to display a reminder. Vapor has it's own templating language, Leaf and in this tutorial we will learn how to use it!

Adding Leaf to your Project

To be able to use Leaf we need to add it as a dependency to our project. First add it to your Package.swift as a dependency (remember you need to add it to the list of your dependencies and also add it as a dependency to the App target):

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "Reminders",
    products: [
        .library(name: "App", targets: ["App"]),
        .executable(name: "Run", targets: ["Run"])
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
        .package(url: "https://github.com/vapor/fluent-provider.git", .upToNextMajor(from: "1.2.0")),
        .package(url: "https://github.com/vapor/mysql-provider.git", .upToNextMajor(from: "2.0.0")),
        .package(url: "https://github.com/vapor/leaf-provider.git", .upToNextMajor(from: "1.0.0")),
    ],
    targets: [
        .target(name: "App", dependencies: ["Vapor", "FluentProvider", "MySQLProvider", "LeafProvider"],
                exclude: [
                    "Config",
                    "Public",
                    "Resources",
                ]),
        .target(name: "Run", dependencies: ["App"])
    ]
)

Next, edit your droplet.json configuration file in the Config directory and add in the following line (make sure you add a , to the preceding line so it is valid JSON):

{
    ...
    "view": "leaf"
}

Now you can regenerate your Xcode project to pull in the new dependency with vapor xcode -y. Once you have your Xcode project open, edit the Config+Setup.swift file, import LeafProvider at the top and add the Leaf Provider to the list of providers:

private func setupProviders() throws {
    try addProvider(FluentProvider.Provider.self)
    try addProvider(MySQLProvider.Provider.self)
    try addProvider(LeafProvider.Provider.self)
}

Build and run the app to make sure everything is still working. The final thing that we need to do to be able to use Leaf is to create the directories where our templates live. By default, these live in the Resources/Views directory, so let's create those directories:

mkdir -p Resources/Views

Generating Views

Using a Template

The first thing we want to do is to create a page that will be displayed when someone visits our site at the root address - so http://localhost:8080 or https://www.brokenhands.io for example when we deploy. To do this, we will create a new controller to manage all of our public views, WebController. Because we need to render views with this controller we will use the dependency injection pattern to pass in the ViewRenderer to our controller so we can use it to generate references.

So let's create our new controller and regenerate our project like before:

touch Sources/App/Controllers/WebController.swift
vapor xcode -y

We can now create a new controller. So let's create a new controller as before with an addRoutes function and remember to pass in the ViewRenderer to the initialiser:

import Vapor

class WebController {

    let viewRenderer: ViewRenderer

    init(viewRenderer: ViewRenderer) {
        self.viewRenderer = viewRenderer
    }

    func addRoutes(to drop: Droplet) {

    }

}

Now that we have our controller we can create a new route handler for the index page:

func indexHandler(_ req: Request) throws -> ResponseRepresentable {
    return try viewRenderer.make("index")
}

This is really simple - all we are doing is telling the ViewRenderer to make the view using the index template. We will come back to this in a minute as first we need to hook up the rest of our stuff. Next, register the route with the Droplet in our addRoutes function:

func addRoutes(to drop: Droplet) {
    drop.get(handler: indexHandler)
}

Finally, set up the Controller in our Routes.swift file:

func setupRoutes() throws {
    let remindersController = RemindersController()
    remindersController.addRoutes(to: self)
    let usersControler = UsersController()
    usersControler.addRoutes(to: self)
    let categoriesController = CategoriesController()
    categoriesController.addRoutes(to: self)

    let webController = WebController(viewRenderer: view)
    webController.addRoutes(to: self)
}

Here we initialise the WebController with the Droplet's view which is a ViewRenderer implementation. Now that we have hooked everything up we can head back to our index template. What we are doing with our viewRenderer.make("index") is telling it to generate a view using a template named index.leaf. So let's create that:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Home | Reminders</title>
</head>

<body>
    <h1>Reminders</h1>
    <p>So we don't forget</p>
</body>
</html>

Save that in Resources/Views/index.leaf and then build and run the project. Hopefully, if you now go to http://localhost:8080 in your browser you should see:

Intiial Template

Adding Parameters

We have our first web page! But currently it isn't very 'templatey', it is only rendering a static page and we don't need Leaf to do that! So let's add some dynamic stuff to our template. In the <head> element we are specifying a title, but what if we want to pass that in? We can pass parameters in to our templates easily as a [String: NodeRepresentable] dictionary, so let's pass in the title:

func indexHandler(_ req: Request) throws -> ResponseRepresentable {
    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = "Home"

    return try viewRenderer.make("index", parameters)
}

We can then use this in our template:

<title>#(page_title) | Reminders</title>

If you build and rerun our app, when you reload the page it should work and look exactly the same! Now admittedly, changing a static title for an injected one isn't particular impressive but you can start to see how we can generate complex views.

Loop Statements

This is a reminders app after all so it isn't unreasonable to want to display all of our reminders on our homepage! So what we can do is get all of the reminders from the database and then pass them to our template. HTML unfortunately isn't strictly typed and only really works as text files so we need a way of converting our model into something that Leaf can generate into text. Vapor uses the Node object for this, so the first thing we need to do is to make our Reminder model conform to NodeRepresentable:

extension Reminder: NodeRepresentable {
    func makeNode(in context: Context?) throws -> Node {
        var node = Node([:], in: context)
        try node.set(Properties.id, id)
        try node.set(Properties.title, title)
        try node.set(Properties.description, description)
        return node
    }
}

For now we will only require the title and the description but we will also add in the id as it will come in useful later. Now that we can convert our Reminder model into something that Leaf can use, we can pass it through to our template in our WebController as a new parameter. To get all of the reminders from our database we can simply use Fluent and call Reminder.all() which will return us an array of all the reminders! So our new index handler will look like:

func indexHandler(_ req: Request) throws -> ResponseRepresentable {
    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = "Home"
    parameters["reminders"] = try Reminder.all()

    return try viewRenderer.make("index", parameters)
}

Finally we can use our reminders in our Leaf template. To display all of the reminders on the page we will use a simple table. We can use the loop tag in Leaf to loop through all of our reminders and display them in the table. So change the body of our template to look like:

<body>
    <h1>Reminders</h1>
    <table>
        <thead><td>Title</td><td>Description</td></thead>
        #loop(reminders, "reminder") {
            <tr><td>#(reminder.title)</td><td>#(reminder.description)</td></tr>
        }
    </table>
</body>

If you build and run the app you should see an empty table as we don't have any data! You can use the API to add some data to the app, but you can run this bash script if you want any easy way of doing it. Once you have added some data, if you reload the page it should look something like:

Reminders Table

Splitting Up Templates

Now that we have a page for all our reminders, next we will want a page for a single reminder. This should show all the information for a single reminder, but most of the page will actually be pretty much the same, especially since we pass the title in. We could copy the template and edit the <body> but copying code is bad - if we want to change the <head> for our site, we would have to go and do it in every template, which is not good! To avoid this we can use a base template and then just write templates for the bits that are different between each page.

First copy our index.leaf template and rename it to base.leaf. Inside base.leaf replace everything inside the <body> tags with #import("body"). Your base.leaf should now look like:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>#(page_title) | Reminders</title>
</head>

<body>
    #import("body")
</body>
</html>

Now in the index.leaf we can extend the base template and define the body to be imported:

#extend("base")

#export("body") {
    <h1>Reminders</h1>
    <table>
        <thead><td>Title</td><td>Description</td></thead>
        #loop(reminders, "reminder") {
            <tr><td>#(reminder.title)</td><td>#(reminder.description)</td></tr>
        }
    </table>
}

This tells Leaf to use the base.leaf template as its base and then to inject the #import("body") part in base.leaf with what we have defined in the #export. The result is the same, but it now means we can add new pages easily. So we can now add a new template for a single reminder.

Leaf's If Statement

Let's create a new template called reminder.leaf and in this we want to display the title and description of a reminder, it's creator and any categories. However, not every reminder will have a category, so we can use the #if() tag to check for this. This works by testing the parameter to see if it exists and/or if it true; we can simply use it to test for nil - if it is not nil then we will loop through the categories, otherwise we won't show anything. So our template will look something like:

#extend("base")

#export("body") {
    <h1>#(reminder.title)</h1>
    <h2>Created by #(user.name)</h2>
    <p>#(reminder.description)</p>

    #if(categories) {
        <h3>Categories</h3>
        <ul>
        #loop(categories, "category") {
            <li>#(category.name)</li>
        }
        </ul>
    }
}

So in this template we will pass in the reminder with the parameter name reminder. We will pass in the user as user and if any categories exist we will pass them in under the parameter categories. You may have noticed that we will need to conform both User and Category to NodeRepresentable, so let's do that. First, for User:

extension User: NodeRepresentable {
    func makeNode(in context: Context?) throws -> Node {
        var node = Node([:], in: context)
        try node.set(Properties.id, id)
        try node.set(Properties.name, name)
        return node
    }
}

And Category, which will be identical:

extension Category: NodeRepresentable {
    func makeNode(in context: Context?) throws -> Node {
        var node = Node([:], in: context)
        try node.set(Properties.id, id)
        try node.set(Properties.name, name)
        return node
    }
}

Finally we can create a new route handler in our WebController:

func reminderHandler(_ req: Request) throws -> ResponseRepresentable {
    let reminder = try req.parameters.next(Reminder.self)

    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = reminder.title
    parameters["reminder"] = reminder
    parameters["user"] = try reminder.user.get()

    if try reminder.categories.count() > 0 {
        parameters["categories"] = try reminder.categories.all()
    }

    return try viewRenderer.make("reminder", parameters)
}

You will notice in this request handler we are extracting the reminder as a parameter just like our API. We can finally register the route in our addRoutes function:

func addRoutes(to drop: Droplet) {
    drop.get(handler: indexHandler)
    drop.get("reminders", Reminder.parameter, handler: reminderHandler)
}

Now if we build and run our app and add some data to the database we should be able to navigate to http://localhost:8080/reminders/1/ and see something like:

Reminder Page

However the final thing we want to do for our reminder page is to make it easy to access! We don't want users having to type in the address, we should provide links on the homepage. So in the loop for all our reminders, let's set them up as links, using the reminder's ID we pass into the Node object:

#loop(reminders, "reminder") {
    <tr><td><a href="/reminders/#(reminder.id)">#(reminder.title)</a></td><td>#(reminder.description)</td></tr>
}

Now if we visit the home page we can click through to each reminder.

Expanding our Website

Now that we have the basics of our site we can add more pages, mainly the users and categories pages.

Users

We want two pages for our users - one to display all of them and one to display an individual user. This will be very similar to our reminders, so let's create a new template users.leaf. Inside it will simply be:

#extend("base")

#export("body") {
    <h1>Users</h1>
    <ul>
        #loop(users, "user") {
            <li><a href="/users/#(user.id)">#(user.name)</a></li>
        }
    </ul>
}

Inside this we will simply loop through the users parameter which we will pass in and display them in a list. Each user will be linked to a user's page which we will create later. So we can create our route handler in our WebController:

func allUsersHandler(_ req: Request) throws -> ResponseRepresentable {
    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = "Users"
    parameters["users"] = try User.all()

    return try viewRenderer.make("users", parameters)
}

We can then create the template for our individual user. This will list the user's name and all of the reminders they have created. So create a new template user.leaf and it will look like:

#extend("base")

#export("body") {
    <h1>#(user.name)</h1>

    #if(reminders) {
        <h2>#(user.name)'s Reminders</h2>
        <table>
            <thead><td>Title</td><td>Description</td></thead>
            #loop(reminders, "reminder") {
                <tr><td><a href="/reminders/#(reminder.id)">#(reminder.title)</a></td><td>#(reminder.description)</td></tr>
            }
        </table>
    }
}

Again we display the name from the object passed in and if we pass in any reminders, we will display them in a table. So our route handler will need to pass them in and look something like:

func userHandler(_ req: Request) throws -> ResponseRepresentable {
    let user = try req.parameters.next(User.self)

    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = user.name
    parameters["user"] = user

    if try user.reminders.count() > 0 {
        parameters["reminders"] = try user.reminders.all()
    }

    return try viewRenderer.make("user", parameters)
}

We then need to register our two new routes in the WebController:

func addRoutes(to drop: Droplet) {
    drop.get(handler: indexHandler)
    drop.get("reminders", Reminder.parameter, handler: reminderHandler)
    drop.get("users", handler: allUsersHandler)
    drop.get("users", User.parameter, handler: userHandler)
}

Finally we want to be able to link to our users, so let's add a new table to our home page to display them all. Underneath the reminders section in index.leaf add:

<h1>Users</h1>
<table>
    <thead><td>Name</td></thead>
    #loop(users, "user") {
        <tr><td><a href="/users/#(user.id)">#(user.name)</a></td></tr>
    }
</table>
<p><a href="/users/">All Users</a></p>

We then need to add the users as a parameter to our index page:

func indexHandler(_ req: Request) throws -> ResponseRepresentable {
    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = "Home"
    parameters["reminders"] = try Reminder.all()
    parameters["users"] = try User.all()

    return try viewRenderer.make("index", parameters)
}

If you build and run the app, you should see the updated home screen with your list of users you can click through to. The final thing we can do for our new user pages is to link through to the user on the reminder page. If you change the 'Created By' part of our reminder.leaf template to include a link then users of the site can easily click through to the user:

<h2>Created by <a href="/users/#(user.id)">#(user.name)</a></h2>

Categories

We basically want to duplicate everything we have done for our users for categories! So start by creating a categories.leaf template which will look like:

#extend("base")

#export("body") {
    <h1>Categories</h1>
    <ul>
        #loop(categories, "category") {
            <li><a href="/categories/#(category.id)">#(category.name)</a></li>
        }
    </ul>
}

Then we can create the route handler:

func allCategoriesHandler(_ req: Request) throws -> ResponseRepresentable {
    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = "Categories"
    parameters["categories"] = try Category.all()

    return try viewRenderer.make("categories", parameters)
}

Then as before, create a template for an individual category, category.leaf, which will look like:

#extend("base")

#export("body") {
    <h1>#(category.name)</h1>

    #if(reminders) {
        <h2>Reminders under #(category.name)</h2>
        <table>
            <thead><td>Title</td><td>Description</td></thead>
            #loop(reminders, "reminder") {
                <tr><td><a href="/reminders/#(reminder.id)">#(reminder.title)</a></td><td>#(reminder.description)</td></tr>
            }
        </table>
    }
}

Then the corresponding route handler, which looks like:

func categoryHandler(_ req: Request) throws -> ResponseRepresentable {
    let category = try req.parameters.next(Category.self)

    var parameters: [String: NodeRepresentable] = [:]
    parameters["page_title"] = category.name
    parameters["category"] = category

    if try category.reminders.count() > 0 {
        parameters["reminders"] = try category.reminders.all()
    }

    return try viewRenderer.make("category", parameters)
}

We then need to register our two new route handlers, so our addRoutes function will look like:

func addRoutes(to drop: Droplet) {
    drop.get(handler: indexHandler)
    drop.get("reminders", Reminder.parameter, handler: reminderHandler)
    drop.get("users", handler: allUsersHandler)
    drop.get("users", User.parameter, handler: userHandler)
    drop.get("categories", handler: allCategoriesHandler)
    drop.get("categories", Category.parameter, handler: categoryHandler)
}

Finally let's add the links to our categories. First, edit index.leaf and add a section after our new users section for the categories:

<h1>Categories</h1>
<table>
    <thead><td>Name</td></thead>
    #loop(categories, "category") {
        <tr><td><a href="/categories/#(category.id)">#(category.name)</a></td></tr>
    }
</table>
<p><a href="/categories/">All Categories</a></p>

Finally, edit our reminder.leaf page to link to the categories when we list them inside our loop:

<li><a href="/categories/#(category.id)">#(category.name)</a></li>

If you build and run the app now you should be able to link through to all our pages.

Conclusion

Hopefully after this tutorial you should understand the basics of Leaf and how to create dynamic pages from templates. In the next tutorial we will continue to dig deeper into Leaf and look at improving the navigation and look of our site, how to create reminders from our website and what Node's Context is for.

As ever, all of the code for these tutorials can be found on Github, with this tutorial under the tutorial-6/leaf-1-getting-started tags.