Introduction to Leaf in Vapor 2 (Template Engine for Vapor)

613 Views

This tutorial will be an introduction to Vapor's templating language, Leaf.

First, download the starter project from Github here. This starter project has only 2 additions from the normal web template: 1) It has the Fluent package included, and 2) It has a simple Post model setup already for us.

Part 1: Most Common Use Case

Go to HelloController.swift. Perhaps the most common use case in using a Leaf, is the need to pass a variable from our controller to our view. This is so common, that the starter-template has included an example of this for us, as shown below:

    /// GET /hello
    func index(_ req: Request) throws -> ResponseRepresentable {
        return try view.make("hello", [
            "name": "World"
        ], for: req)
    }

In the view.make("hello", [ "name": "World" ], for: req) method, the second argument is where we can define our variables. Run your server, and navigate to localhost:8080/hello, and you'll see:

Hello, World!

  • Note: The parameters specified in the second argument must conform to NodeRepresentable.

Part 2: How it works

To make complete sense of this, look under Resources -> Views -> hello.leaf. Notice this code:

    #export("content") {
	 

Hello, #(name)!

}

In our controller, the view.make method's second argument defined the variable name. We assigned "World" to this, and then in our leaf file, we did #(name) so the content is read as leaf markup, and not as regular HTML.

Now, let's dissect the view.make("hello", [ "name": "World" ], for: req) method a bit more.

The first part, view in "view.make" is the ViewRenderer object of the controller. Our instance of the ViewRenderer is managed by our same instance of droplet. In droplet+setup.swift, we define our routes with:

    let routes = Routes(view)

The Routes class has an initializer, which takes an instance of ViewRenderer. This code is in an extension of Droplet, and we pass in view property of our instance of Droplet (drop.view). The ViewRenderer for a droplet is responsible for connecting your controller/application logic, and your view. Therefore, to render a view with leaf, our controller must have access to our droplet's viewRenderer.

  • Note: In order to use leaf properly, your droplet.json file must have the following key: "view": "leaf", or else your views will not load correctly.

Next, the "hello" in view.make("hello".... Is the filepath. This means that Vapor will look for a file named hello.leaf under Resources -> Views. If, for example, you want to create groups/folders under the Views directory, you must specify that.

For example, if your file is login.leaf, and it is under a LoginViews folder, such as: Resources -> Views -> LoginViews, then in order to render this, you will need to have:

    view.make("LoginViews/login"...)

The last argument, for: req), simply tells the viewRenderer which request this view was rendered for.

Another Example

Let's do another example. Let's use our post Model to create a Post object, and then print the name property of the object in our leaf template. Change the index function in HelloController.swift to read as follows:

    func index(_ req: Request) throws -> ResponseRepresentable {
        let post = Post(name: "testing Post")
        return try view.make("hello", [ "post": post], for: req)
    }

Then, in hello.leaf, change the #export ("content") { } to be:

    #export("content") {
	

Hello, #(post.name)!

}

So, in our controller, we defined a variable, post (using [ "post": post]), and then assigned its contents to be that of the post we initialized. Then, in our leaf file, we are accessing the name property, using post.name. Run the project and navigate to /hello

Oops! Internal Service Error? What could be wrong?

The error code in the console reads:

    [Node Error: No converters found for App.Post of type Post] [Identifier: Node.NodeError.noFuzzyConverter] [Possible Causes: You have not properly set the Node.fuzzy array] [Suggested Fixes: Conform App.Post of type Post to JSON, Conform App.Post of type Post to Node]

Well, as stated in the first part of this tutorial, our variables we define must conform to NodeRepresentable. We tried to assign our custom object, Post, to a variable, and our Post model does not conform to this protocol.

Add the following line of code to Post.swift:

    extension Post: NodeRepresentable { }

Small Break in the Action

Also, I should note that I forgot to include a few things in the starter project (sorry!), so take a minute to do a few things:

  • Create a file called fluent.json, and make it read exactly as the file here in this gist. Place this file in your Config folder.
  • Add the following to Config+Setup.swift:
  • func setupPreparations() {
        preparations.append(Post.self)
        }
    
  • Then, call this function inside the setup() function in the same file. Your setup() function should have three lines now
  •     Node.fuzzy = [JSON.self, Node.self]
        setupPreparations()
        try setupProviders()
    


Back to Your Regularly Scheduled Programming

Ok, we're back! To make sure you have the tutorial exactly as you should up to this point, you can download from [this branch](https://github.com/JoeyBodnar/LeafIntro/tree/MidwayCheck).

In HelloController.swift, change the index function to read:

    func index(_ req: Request) throws -> ResponseRepresentable {
        let post = Post(name: "testing Post")
        let node = try Node(node: post.makeJSON()) // 1
        return try view.make("hello", ["post": node], for: req) 
    }

Now, run and navigate to /hello. It works! Now, if you have an object with many properties, you can pass just the object in as a parameter, and then access all it's properties using the convenient dot-syntax (post.name, etc) in your Leaf file.

Looping

Next, let's cover looping. What if we have an array of posts, and we want to loop through and print all the post.name properties? Fairly simple as well. Change your index function in HelloController.swift to read:

    func index(_ req: Request) throws -> ResponseRepresentable {
       // 1
        let post1 = Post(name: "testing Post 1")
        let post2 = Post(name: "testing Post 2")
        let post3 = Post(name: "testing Post 3")
        let post4 = Post(name: "testing Post 4")
        
        // 2
        let node1 = try Node(node: post1.makeJSON())
        let node2 = try Node(node: post2.makeJSON())
        let node3 = try Node(node: post3.makeJSON())
        let node4 = try Node(node: post4.makeJSON())
        
        let nodeArray = [node1, node2, node3, node4] // 3
        return try view.make("hello", ["posts": nodeArray], for: req)
    }

This isn't exactly the way you would end up with an array of objects in a real application (you would usually be pulling them straight from the database), but to keep it simple and illustrate the concept we'll just do it all in-line.

* At **1**, we simply create our array of Posts whose data we will read. * At **2**, we create a node object for each Post object * At **3**, we pass in the nodeArray and assign it to the posts variable. We will use the posts variable in the leaf file and loop through it.

Next, change your hello.leaf to read:

    #extend("base")
    #export("title") { Hello! }
    #export("content") {
	

Hello!

    #loop(posts, "postItem") {
  1. #(postItem.name)
  2. }
}

The **#** tag tells the leaf file "the coming text contains leaf markup, do not interpret as direct HTML". The **posts** we loop through must be the same variable name that we passed from our controller. The **"post"** in quotes is the individual post that we are looping through. So we just loop through each Post object, and print out its name attribute.

Run the project and test. You should see:

    1. testing Post 1
    2. testing Post 2
    3. testing Post 3
    4. testing Post 4

Next, what if we would like to access the index of each variable in the array as we loop through? Just change the loop code to read:

    #loop(posts, "postItem") {
         
  • #(postItem.name) And the index is #(index)!
  • }

    Or, we can also get the offset (index+1) of each item as well:

        #loop(posts, "postItem") {
             
  • #(postItem.name) And the index is #(index) and the offset is #(offset)!
  • }

    Ok, that's all for the intro to Leaf tutorial. Part 2 of the Leaf Series will be here shortly, stay tuned! You can [download the project to this point here](https://github.com/JoeyBodnar/LeafIntro/tree/EndPart1).



    Enjoy this article? Consider supporting VaporForums by following us on Twitter! Get the latest Vapor articles, tutorials, and news.