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
.
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 namedtitle
#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 Vaporlet 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”.
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.
- We ensure
username
andpassword
field was correctly given when calling this routes and that’s a string data. - We check credentials validity here (don’t take a look here, that’s just for the example)
- 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.
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.
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.