Using Swift on RaspberryPi — Rendering complex view with Leaf

Vapor come with a custom template langage: Leaf. But you could also use other template langage like Stencil (a Swift specific template, like Leaf) or Mustache.

In this article we will cover only Leaf because it’s the official Vapor template langage and built-in when create projet with Vapor Toolbox.

How Leaf work ?

That’s the first thing to see when using a template langage: how it work ? It’s pattern, rules and some other things.

In Leaf, you have to take care of one character: # (hashtag).

Leaf interpreter will check for each hashtag in a file, and for each, trying to intrepet it as a Leaf command. This hashtag represent a tag in Leaf language.

Associated to the a tag (or a hashtag), you could find three other element :

  • a name (just a string after the hashtag)
  • a parameter list (represented by () )
  • a body (represented by {} )

Because hashtag is considered to a special character in Leaf, to display raw hashtag in a page, you must use it with an empty parameter list :

#()

Common tag are provided with Leaf for template operation, but one related to displaying raw content is #raw . Body passed to the raw tag aren’t interpreted by Leaf :

#raw() {}
#raw() { <a href="#">Link</a> }

Here is the exhaustive list of common tag :

  • #equal(left, right) for equal comparaison between two object/value
  • #(variable) for output a given variable
  • #if(bool) / ##else for conditional output
  • #loop(object, "index") for repeated output
  • #index(object, "index") or #index(object, "key") for output value from array or dictionary
  • #import for importing leaf block #export in extended children page
  • #export for exporting leaf block using in mother page with #export
  • #embed for embed content of another leaf file directly
  • #extend for inherit from another leaf page

Great, not enough to be jealous of Mustache 💪. Next we will use all of this tag to create a home page and a login form.

Use Leaf in real context

For this article, I’ve created a new Vapor app named “LeafDemo”

vapor new LeafDemo

First, we will import a CSS framework to simplify our work and have a nice look an feel with zero effort 💤. I’m used to Bootstrap but for this time I will try Materialize, because Material Design is very great to use.

Install Materialize CSS framework

Download Materialize pre-compiled package and extract it. Normally, you will find a folder which content three subfolder: js, css and fonts.

Materialize CSS zip folder contents

You need to copy this three subfolder to your Vapor application. To use it in web page rendered by Leaf, you need to place it to the public exposed folder. This folder are normally named Public. The default template of Vapor contains already two subfolders: images and styles.

Copy folder js and fonts of Materialize inside the Public Vapor folder. Then copy CSS file in Materialize css folder inside Public/styles folder.

In my case, I choose to rename newly copied js folder scripts.

Create view template of the app

Next, we need to integrate Materialize inside our application. By default, Vapor create two Leaf file: welcome.leaf and base.leaf. Delete the first and open the second, replace all the content of this by this one :

<!DOCTYPE html>
<html>
<head>
<!--Import Google Icon Font-->
<link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!--Import materialize.css-->
<link type="text/css" rel="stylesheet" href="styles/materialize.min.css" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="styles/app.css" media="screen,projection"/>


<!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>#(title)</title>
#import("head")
</head>
<body>
<nav>
<div class="nav-wrapper">
<a href="#()" class="brand-logo center">#(title)</a>
<ul id="nav-mobile" class="right hide-on-med-and-down">
#if(username) {
} ##else() {
<li><a href="/login">Login</a></li>
}
</ul>
</div>
</nav>
<div class="container">
#import("body")
</div>

<!--Import jQuery before materialize.js-->
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="scripts/materialize.min.js"></script>
</body>
</html>

This file is a base template, it will be use and extended in each page but it allow us to have a same base template for each other page we create.

If you read this, you will see some HTML classic code for importing Materialize CSS/JS files and some Leaf tag :

  • #import("head") when extended, replace this tag by the tag #export("head")
  • #() write simply raw hastag # in the page
  • #(title) output a variable named title
  • #if(username) { } ##else { } used to rendering conditional navigation buttons if we’re logged in or not.
  • #import("body") when extended, replace this tag by the tag #export("body")

Next, we will create an other template for a login form. At this time, we only create view template but without associate it to a route of our app. Be patient, that the next step.

Create a new file named login.leaf and write this content :

#extend("base")
#export("head") {
<title>Login</title>
}

#export("body") {
<div class="row">
<div id="login-block" class="col offset-l3 l6 s12">
<form method="post" action="/login">
<div class="row">
<div class="input-field col s12">
<input id="username" type="text" class="validate" name="username">
<label for="username">Username</label>
</div>
<div class="input-field col s12">
<input id="password" type="password" class="validate" name="password">
<label for="password">Password</label>
</div>
<button class="btn btn-large waves-effect waves-light col s12" type="submit" name="action">
Login
</button>
</div>
</form>
</div>
</div>
}

If you take a look on this content, you see this first line with tag #extend("base"), that’s how we can use an other leaf template to extend it. That’s the same logic of inherit in object oriented programming.

Next, because we extend base.leaf we have to export expected import of this template: head and body. Nothing really important here for Leaf, just a HTML form for username/password and action to POST /login route.

Ok, so, at this time we have two leaf template, a login page and a base page. Next we will create an really simple home.leaf template for outputing the username :

#extend("base")
#export("head") {
}
#export("body") {
#if(username) {
<p>Hello #(username)</p>
}
}

Link view template to app routes

Great, we have our view template, but at this time, Vapor app don’t know how to use it. Go to the main.swift file and erase some lines to have only this text :

import Vapor
let drop = Droplet()
drop.run()

We removed all default routes because we want to create each one step by step. First, create a default route for the home.

drop.get { req in
return try drop.view.make("home", [
"title": "Home"
])
}

This code should be placed before drop.run() and after initialize of Droplet of course. As you can see, it only render the home page with the title variable to “Home”.

Home page rendering

If your remember home.leaf template, you will see that’s the conditional output for username aren’t sastified and no output was purposed in the body here. Also, in the base.leaf we have a conditonal rendering for the login button in the navigation which wasn’t satisfied here so we output the ##else {} block.

Next, we will create the login route for rendering login form. Return to your main.swift file and add two route on /login , a GET and a POST :

drop.get("login") { req in
return try drop.view.make("login", [
"title": "Login"
])
}
drop.post("login") { req in
guard let username = req.data["username"]?.string,
let password = req.data["password"]?.string else {
throw Abort.badRequest
}

guard username == "vincentsaluzzo" && password == "azerty" else {
throw Abort.badRequest
}

return try drop.view.make("home", [
"title": "Home",
"username": username
])
}

The first route was for rendering the login page. Nothing more than the previous home rendering. The second use the same name but for a POST HTTP method.

We’re not covering the authentification and the data model in this article, so we simply write a poor authentication method.

  1. We ensure username and password field was correctly given when calling this routes and that’s a string data.
  2. We check credentials validity here (don’t take a look here, that’s just for the example)
  3. Then we rending the home page with a title and a the username validated.

As you can see, for error handling here we use the throw principle of Swift. You can throw every error you want here, but Vapor give us a predefined enumeration for error throwing: Abort.

We use it to inform that call fail when data aren’t given correctly.

Now rebuild and reload the home page and click on Login button inside the navigation, you will see the login form.

Filled login form

Write the correct credentials, submit and magic! You see the same home as before but with a different output. That’s because conditional outputing have change: the username variable exist right now.

Different rendering in the home page

This example cover a little point of Leaf. Like in Mustache you can loop over array or dictionary to rendering list or tiles.

Bonus: Create your own tag

Leaf allow you to create you own tag. For this, simply create a new class inherit from BasicTag protocol (don’t forget to import Leaf package before)

This protocol need to implement one method:

func run(arguments: [Argument]) throws -> Node?

The arguments parameter correspond to each argument given when using in a Leaf template. After you create your tag, simply register it to your LeafRenderer :

if let leaf = drop.view as? LeafRenderer {
leaf.stem.register(Index())
}

Voila.

Leaf aren’t revolutionary, but his syntax are simple and easily extendable. The next time we will see how realize an API based on real model data.