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

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.

select all
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:

select all
<!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:

select all
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:

select all
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:

select all
<!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="/">&lt; Back</a></div>
</body>
</html>

And update ViewPage.fan:

select all
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:

select all
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:

select all
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:

select all
<!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:

select all
<%= 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:

select all
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="/">&lt; Back</a></div>
<% } %>

And now ViewPage.fan:

select all
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.


Discuss