Templating Vapor Applications with Leaf
Use Leaf, Vapor’s templating engine, to build a front-end website to consume your server-side Swift API! By Tim Condon.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Templating Vapor Applications with Leaf
20 mins
Dynamic Views with Variables
Right now, the template is just a static page and not very impressive. Let’s make it more dynamic by having it display the title as defined by the route!
Open index.leaf and change the <title>
line to the following:
<title>#(title) | Acronyms</title>
This will display the value of a variable called title
using the #()
Leaf function.
Like much of Vapor, Leaf takes advantage of Codable
to handle data. However, Leaf only accepts input data, so your models only need to conform to Encodable
. These data models are much like view models in the MVVM design pattern.
At the bottom of WebsiteController.swift, create a new struct that will contain the title, like the following:
struct IndexContext: Encodable {
let title: String
}
Next, change indexHandler(_:)
to pass an IndexContext
to the template as the second parameter. The method’s implementation should look like the following:
func indexHandler(_ req: Request) -> EventLoopFuture<View> {
// 1
let context = IndexContext(title: "Home page")
// 2
return req.view.render("index", context)
}
Here’s what the new code does:
-
Create an
IndexContext
containing the desired title. -
Pass the
context
to Leaf as the second parameter torender(_:_:)
.
Build and run, then refresh the page in the browser. You’ll see the updated title:
Advanced Templates
So far the the current index.leaf template is only using the #()
Leaf function to display a variable’s value. Leaf’s functions are referred to as Tags.
There are more advanced tags, such as #if()
and #for()
, and you can even write your own – but we’re going to focus on the two just mentioned in the rest of this tutorial.
Conditional Rendering
Since the index.leaf template is for home page of the TIL website, it should display a list of all the acronyms. However, it also needs to handle the possibility that no acronyms are in the database.
This is a perfect use case for Leaf’s #if()
tag!
In WebsiteController.swift, add a new property to IndexContext
underneath title
:
let acronyms: [Acronym]?
This property will store the list of acronyms in the database or nil
if none are found. An Optional
will represent the empty state because Leaf’s #if()
tag can check for nil
values as well as evaluating boolean expressions.
Next, update WebsiteController.indexHandler(_:)
to get all the acronyms from the database using Fluent and insert them in the IndexContext
.
Replace the implementation once more with the following:
func indexHandler(_ req: Request) -> EventLoopFuture<View> {
// 1
Acronym.query(on: req.db).all().flatMap { acronyms in
// 2
let acronymsData = acronyms.isEmpty ? nil : acronyms
let context = IndexContext(
title: "Home page",
acronyms: acronymsData)
return req.view.render("index", context)
}
}
Here’s what this does:
Note: If you’re unfamiliar with Fluent, review the tutorial Using Fluent and Persisting Models in Vapor.
-
Use a Fluent query to get all the acronyms from the database.
-
Add the acronyms to
IndexContext
if there are any, otherwise set the variable tonil
.
Finally open index.leaf and replace the parts between the <body>
tags with the following:
<!-- 1 -->
<h1>Acronyms</h1>
<!-- 2 -->
#if(acronyms):
<!-- 3 -->
<table>
<thead>
<tr>
<th>Short</th>
<th>Long</th>
</tr>
</thead>
<tbody>
<!-- 4 -->
</tbody>
</table>
<!-- 5 -->
#else:
<h2>There aren’t any acronyms yet!</h2>
#endif
Here’s what the updated template does:
- Declare a new heading, “Acronyms”.
-
Uses Leaf’s
#if()
tag to see if theacronyms
variable is set. -
If
acronyms
is set, create an HTML table. The table has a header row —<thead>
— with two columns, Short and Long. -
Leave an empty place to return to in the next section that will loop through all acronyms to display using Leaf’s
#for
tag. - If there are no acronyms, print a suitable message.
Looping Through Values
Since the template is displaying an unknown amount of acronyms, it needs to loop through the provided array and display each element’s values.
This is where Leaf’s #for()
tag is used. It works similarly to Swift’s for
loop.
In index.leaf, where the table body – <tbody>
– was left blank, add the following code:
<!-- 1 -->
#for(acronym in acronyms):
<!-- 2 -->
<tr>
<!-- 3 -->
<td>#(acronym.short)</td>
<td>#(acronym.long)</td>
</tr>
<!-- 4 -->
#endfor
Here’s what the new template code does:
-
Uses Leaf’s
#for()
tag to loop through all of the acronyms. -
Creates a table row —
<tr>
— for each acronym. -
Creates a table cell –
<td>
– to display each property of the acronym using Leaf’s#()
tag. -
Tells Leaf that the
#for()
tag has finished.
Build and run, then refresh the page in the browser.
If you have no acronyms in the database, you’ll see the correct message:
If there are acronyms in the database, you’ll see them in the table:
Inserting Acronyms
To test the page with acronyms, you can use the RESTed macOS app to add acronyms into the database, as described in our Fluent tutorial.
First, add a user into the database by sending a POST request to http://localhost:8080/api/users
in RESTed as follows:
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:
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 Detail Page
Now, you need a page to show the details for each acronym.
At the end of WebsiteController.swift, create another struct to pass as input to Leaf input which will represent all the details of an acronym:
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 after WebsiteController.indexHandler(_:)
:
// 1
func acronymHandler(_ req: Request) -> EventLoopFuture<View> {
// 2
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 3
acronym.$user.get(on: req.db).flatMap { user in
// 4
let context = AcronymContext(
title: acronym.short,
acronym: acronym,
user: user)
return req.view.render("acronym", context)
}
}
}
Here’s what this route handler does:
-
Declares a new route handler,
acronymHandler(_:)
, that returnsEventLoopFuture<View>
. - Searches for the acronym with the request’s parameters and unwraps the result. If no acronym is found with the given ID, it will return a 404 Not Found.
- Gets the user that created the acronym and unwraps the result.
-
Creates an
AcronymContext
that contains the appropriate details and renders the page using the acronym.leaf template.
Finally register the route handler at the bottom of WebsiteController.boot(routes:)
for the /acronyms/<ACRONYM ID>
route path:
routes.get("acronyms", ":acronymID", use: acronymHandler)
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:
- Declares an HTML5 page like index.leaf.
- Sets the title to the value that’s passed in.
-
Prints the acronym’s
short
property in an<h1>
heading. -
Prints the acronym’s
long
property in an<h2>
heading. -
Displays the user’s name that created the acronym in a
<p>
block
Now that the detail template is made, the index.leaf template can be updated to provide a link to navigate to a given acronym’s detail page.
Open index.leaf and replace the first table cell – <td>#(acronym.short)</td>
– for each acronym 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:
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: