Using code to write HTML markup is tedious so in this article we convert the markup code in the Bed Nap application into efan templates and introduce the concept of components.
Writing out large amounts of HTML with multiline strings or WebOutStream
is pretty awkward. It's difficult for designers to edit and you can't just cut'n'paste examples from the web. In general, it's pretty yukky. And so far we only have bare bones HTML and that code it's already a major part of our application!
It's time to switch to templating. We're going to use Embedded Fantom, or efan for short. Like JSP for Java and ERB for Ruby, efan turns our code inside out. While normal code has strings embedded within code, efan has code embedded within strings!
First of, let's install efan and add a dependency to our build.fan
:
fanr install -r http://eggbox.fantomfactory.org/fanr "afEfan 1.5.2 - 1.5"
Starting off, we will have 2 efan templates, 1 for the IndexPage
and 1 for the ViewPage
. We will then move the common parts out into a seperate Layout
template.
- Index Page Template
- View Page Template
- Use efan Templates
- Header and Footer Components
- Nested Templates
- Layout Component
- Source Code
Index Page Template
To make the transition easy, we'll isolate the code that's going to be converted into an efan template. That involves moving it to a static method with a single parameter called ctx
and having it return a Str
.
using afIoc::Inject using afBedSheet::Text using web::WebOutStream const class IndexPage { @Inject private const VisitService visitService new make(|This|in) { in(this) } Text render() { ctx := visitService.all html := renderCtx(ctx) return Text.fromHtml(html) } static Str renderCtx(Visit[] ctx) { htmlBuf := StrBuf() html := WebOutStream(htmlBuf.out) html.docType5 html.html html.head html.title.w("Bed Nap Index Page").titleEnd html.headEnd html.body html.h1.w("Bed Nap Tutorial").h1End html.h2.w("Summary Page").h2End html.table html.tr html.th.w("Name").thEnd html.th.w("Date").thEnd html.th.w("Rating").thEnd html.th.thEnd html.trEnd ctx.each { html.tr html.td("class='t-name'").w(it.name).tdEnd html.td("class='t-date'").w(it.date).tdEnd html.td("class='t-rate'").w(it.rating).tdEnd html.td html.a(`/view/${it.id}`, "class='t-view'").w("view").aEnd html.tdEnd html.trEnd } html.tableEnd html.bodyEnd html.htmlEnd return htmlBuf.toStr } }
Our new efan template will replace the renderCtx()
method.
We're going to keep the efan templates in the same directory as the code, so we need to update the resDirs
variable in build.fan
:
resDirs = [`fan/pages/`]
Now create IndexPage.efan
:
<!DOCTYPE html> <html> <head> <title>Bed Nap Index Page</title> </head> <body> <h1>Bed Nap Tutorial</h1> <h2>Summary Page</h2> <table> <tr> <th>Name</th> <th>Date</th> <th>Rating</th> <th></th> </tr> <% ctx.each { %> <tr> <td class="t-name"><%= it.name %></td> <td class="t-date"><%= it.date %></td> <td class="t-rate"><%= it.rating %></td> <td><a href="/view/<%= it.id%>" class="t-view">view</a></td> </tr> <% } %> </table> </body> </html>
Wow! It looks like real HTML again! And as you can see, there is very little efan markup.
Next we update IndexPage.fan
to read the .efan
file and pass it to an Efan
service. We can also delete the renderCtx()
method:
using afIoc::Inject using afBedSheet::Text using afEfan::Efan const class IndexPage { @Inject private const VisitService visitService @Inject private const Efan efan new make(|This|in) { in(this) } Text render() { ctx := visitService.all file := Pod.of(this).file(`/fan/pages/IndexPage.efan`) html := efan.renderFromFile(file, ctx) return Text.fromHtml(html) } }
Note that the afEfan
pod has nothing to do with IoC, so in order to inject Efan
we need to define it as a service ourselves. In AppModule
:
static Void defineServices(ServiceDefinitions defs) { defs.add(VisitService#) defs.add(Efan#) }
If we now build our pod and re-run our tests, they should all still pass!
View Page Template
Follow the same procedure as before. Isolate the template code in it's own method:
using afBedSheet::Text using web::WebOutStream const class ViewPage { Text render(Visit visit) { html := renderCtx(visit) return Text.fromHtml(html) } Str renderCtx(Visit ctx) { htmlBuf := StrBuf() html := WebOutStream(htmlBuf.out) ... return htmlBuf.toStr } }
Convert the renderCtx()
method into ViewPage.efan
:
<!DOCTYPE html> <html> <head> <title>Bed Nap View Page</title> </head> <body> <h1>Bed Nap Tutorial</h1> <h2>Visit View Page</h2> <div class="t-name"><%= ctx.name %> said:</div> <div class="t-comment"><%= ctx.comment %></div> <div class="t-date">on <%= ctx.date %></div> <div class="t-rate"><%= ctx.rating %> / 5 stars</div> <div><a href="/">< Back</a></div> </body> </html>
And update ViewPage.fan
:
using afIoc::Inject using afBedSheet::Text using afEfan::Efan const class ViewPage { @Inject private const Efan efan new make(|This|in) { in(this) } Text render(Visit visit) { file := Pod.of(this).file(`/fan/pages/ViewPage.efan`) html := efan.renderFromFile(file, visit) return Text.fromHtml(html) } }
And again, all out tests should still pass, because technically we've not changed the output of our application.
Note that at this point we can remove web
from our list of dependencies for it was only required to access WebOutStream
.
Use efan Templates
The efan docs say it's bad to constantly use the Efan.renderXXX()
methods because over time they could eventually cause a memory leak. (Each call to render dynamically creates a Fantom type.) As we call Efan.renderXXX()
every time a page is viewed, this is probably a bad thing!
So instead we will create EfanTemplateMeta instances and call render()
on those.
The .efan
files stay the same, we just update the code. IndexPage
first:
using afIoc::Inject using afBedSheet::Text using afEfan::Efan using afEfan::EfanTemplateMeta const class IndexPage { @Inject private const VisitService visitService private const EfanTemplateMeta template new make(Efan efan, |This|in) { in(this) efanFile := Pod.of(this).file(`/fan/pages/IndexPage.efan`) template = efan.compileFromFile(efanFile, Visit[]#) } Text render() { ctx := visitService.all html := template.render(ctx) return Text.fromHtml(html) } }
We create the efan template in the ctor and save it as a field. We then render it over and over in the IndexPage.render()
method.
Note we are now using Mixed Injection in the ctor!
Let's quickly do the same for ViewPage
:
using afIoc::Inject using afBedSheet::Text using afEfan::Efan using afEfan::EfanTemplateMeta const class ViewPage { private const EfanTemplateMeta template new make(Efan efan, |This|in) { in(this) efanFile := Pod.of(this).file(`/fan/pages/ViewPage.efan`) template = efan.compileFromFile(efanFile, Visit#) } Text render(Visit visit) { html := template.render(visit) return Text.fromHtml(html) } }
A quick check and yep, all our tests still pass!
Header and Footer Components
Now we're going to try something cool... what if an EfanTemplate
could render another EfanTemplate
? ... Woah!
Then we could then siphon off common parts of the pages, like the header and footer, to separate templates. Our page templates would then look like this (in pseudo code):
headerTemplate.render() render page specific stuff footerTemplate.render()
It's nice... but basic. And the footer would always need to stay in sync with the header. For example, if the header opened up an extra <div>
tag then the footer would need to be updated to close it.
So what if we turned this inside out. Rather than having pages that render components, we had a component that rendered pages!?
In pseudo code, our component template would look like:
render header stuff pageTemplate.render() render footer stuff
This is good... but hold that thought! Before we go any further, let's talk about...
Nested Templates
Nested templates is an efan term.
I best understand the concept in terms of HTML / XML... Let's assume we had an arbitrary <template>
tag that represented some generic markup. We could potentially render it in a page like this:
<html> ... <template /> ... </html>
If our generic markup was <div class="big bold">Hello Mum!</div>
then when the page was rendered it would look like:
<html> ... <div class="big bold">Hello Mum!</div> ... </html>
But what if we wanted Hello Dad!
to be big and bold, or some other text? Then ideally we would write:
<html> ... <template> Hello Dad! </template> ... </html>
Which would get rendered as:
<html> ... <div class="big bold"> Hello Dad! </div> ... </html>
That Hello Dad!
text is in effect the body of the template
tag. And that body could be anything - even more tags and / or templates! If the body
is passed into template
, then the template
could choose where in it's markup to render it.
And that is how nested efan templates work. If our fictional template was an actual Template.efan
file it would look like this:
<div class="big bold"> <%= renderBody() %> </div>
renderBody()
is a special method on an EfanTemplate
that does just that.
And the page, if it too were a .efan
file, then it would look like:
<html> ... <%= ctx.template.render(...) { %> Hello Dad! <% } %> ... </html>
See how the body
is actually passed into the render()
method as an it-block
function.
Layout Component
Going back to rendering headers and footers, we are going to create an efan nested component (called LayoutComponent
) that encompasses the header and footer of our pages. Using a Layout component is a common pattern.
We'll dive straight in a write what we want our LayoutComponent.efan
to look like:
<!DOCTYPE html> <html> <head> <title><%= ctx %></title> </head> <body> <h1>Bed Nap Tutorial</h1> <%= renderBody() %> </body> </html>
It's a skeleton of a HTML page. Note that the ctx
is a Str
representing the page title.
We would use the Layout component in IndexPage
like this:
<%= ctx.layout.render("Bed Nap Index Page") { %> <h2>Summary Page</h2> <table> <tr> <th>Name</th> <th>Date</th> <th>Rating</th> <th></th> </tr> <% ctx.visits.each { %> <tr> <td class="t-name"><%= it.name %></td> <td class="t-date"><%= it.date %></td> <td class="t-rate"><%= it.rating %></td> <td><a href="/view/<%= it.id%>" class="t-view">view</a></td> </tr> <% } %> </table> <% } %>
To make this work we need to pass in a ctx
object with a layout
and visits
slot. So let's make a mini data class called IndexPageCtx
for the job:
using afIoc::Inject using afBedSheet::Text using afEfan::Efan using afEfan::EfanTemplateMeta const class IndexPage { @Inject private const VisitService visitService private const EfanTemplateMeta template private const EfanTemplateMeta layout new make(Efan efan, |This|in) { in(this) templateFile := Pod.of(this).file(`/fan/pages/IndexPage.efan`) template = efan.compileFromFile(templateFile, IndexPageCtx#) layoutFile := Pod.of(this).file(`/fan/pages/Layout.efan`) layout = efan.compileFromFile(layoutFile, Str#) } Text render() { ctx := IndexPageCtx() { it.layout = this.layout; it.visits = visitService.all } html := template.render(ctx) return Text.fromHtml(html) } } class IndexPageCtx { EfanTemplateMeta layout Visit[] visits new make(|This|in) { in(this) } }
All tests pass - great! Let's do the same for ViewPage
. First ViewPage.efan
:
<%= ctx.layout.render("Bed Nap View Page") { %> <h2>Visit View Page</h2> <div class="t-name"><%= ctx.visit.name %> said:</div> <div class="t-comment"><%= ctx.visit.comment %></div> <div class="t-date">on <%= ctx.visit.date %></div> <div class="t-rate"><%= ctx.visit.rating %> / 5 stars</div> <div><a href="/">< Back</a></div> <% } %>
And now ViewPage.fan
:
using afIoc::Inject using afBedSheet::Text using afEfan::Efan using afEfan::EfanTemplateMeta const class ViewPage { private const EfanTemplateMeta template private const EfanTemplateMeta layout new make(Efan efan, |This|in) { in(this) templateFile := Pod.of(this).file(`/fan/pages/ViewPage.efan`) template = efan.compileFromFile(templateFile, ViewPageCtx#) layoutFile := Pod.of(this).file(`/fan/pages/Layout.efan`) layout = efan.compileFromFile(layoutFile, Str#) } Text render(Visit visit) { ctx := ViewPageCtx() { it.layout = this.layout; it.visit = visit } html := template.render(ctx) return Text.fromHtml(html) } } class ViewPageCtx { EfanTemplateMeta layout Visit visit new make(|This|in) { in(this) } }
Being able to nest templates in this manner is an important and powerful concept.
Make your Ruby friends envious!
Ruby's ERB templating also has the notion of embedded templates. But only the top level template can render a body, hence they call it nested layout. efan takes the concept further and allows any template to render its body.
While nested components are powerful, there's still some work involved with parsing templates and setting up the ctx
variables. That's where efanXtra comes in - efanXtra makes component templating much, much easier! See the next tutorial chapter to learn about efanXtra.
Source Code
All the source code for this tutorial is available on the Bed Nap Tutorial Bitbucket Repository.
Code for this particular article is available on the 06-efan-Templates branch.
Use the following commands to check the code out locally:
C:\> hg clone https://bitbucket.org/fantomfactory/bed-nap-tutorial C:\> cd bed-nap-tutorial C:\> hg update 06-efan-Templates
Don't forget, you can trial the finished tutorial application at Bed Nap.
Have fun!
Edits
- 6 Aug 2016 - Updated tutorial to use BedSheet 1.5 & IoC 3.0.
- 6 Aug 2015 - Updated tutorial to use BedSheet 1.4.
- 2 Sep 2014 - Original article.