Bare efan templates can quickly accumulate a lot of boilerplate code to facilitate their rendering. In this article we convert our Bed Nap templates into efanXtra components and delete a whole lot of boilerplate code along the way.

Refactor Layout Component

To give an understanding as to how efanXtra components work we are first going to refactor the Layout component.

Lets move Layout.efan into new /fan/components/ directory, and give it an accompanying Layout.fan source file. Don't forget to update both srcDirs and resDirs to include the new folder in build.fan.

We're making Layout.fan because we want to render the Layout template in the same manner as the pages, by calling a render() method. But rather than creating and passing in a dedicated Ctx object, we're going to pass in the Layout class itself.

Let's also make the render() method return a simple Str so it's more generic. And lets stipulate that render() should never take any parameters. This forces us to have some sort of initRender() method that passes in / sets our title and makes it available to the template.

As we will see, this will split our template code up into two sections:

  • Boilerplate code that could be reused for every template.
  • Layout specific code, as required by the template.

Layout.fan should look like:

select all
using afIoc::Inject
using afBedSheet::Text
using afEfan::Efan
using afEfan::EfanTemplate

class Layout {
    // ---- Boilerplate Code --------------------

    private EfanTemplate    template

    new make(Efan efan) {
        templateFile := Pod.of(this).file(`/fan/components/Layout.efan`)
        template = efan.compileFromFile(templateFile, Layout#)
    }

    Text render() {
        html := template.render(this)
        return Text.fromHtml(html)
    }



    // ---- Component Specific Code -------------

    Str? title

    Void initRender(Str title) {
        this.title = title
    }
}

And Layout.efan:

select all
<!DOCTYPE html>
<html>
<head>
    <title><%= ctx.title %></title>
</head>
<body>
    <h1>Bed Nap Tutorial</h1>

    <%= renderBody %>
</body>
</html>

To test that the above works, you can use this little method to render it:

static Void main() {
    layout := Layout(Efan())

    layout.initRender("Gold Fish")

    html := layout.render()

    echo(html)
}

If we were to remove the boilerplate code, then this is pretty close to what an efanXtra component looks like!

Convert Layout to an efanXtra Component

Next we're going to convert Layout into an efanXtra component. So download efanXtra and add it as a dependency to build.fan.

fanr install -r http://eggbox.fantomfactory.org/fanr "afEfanXtra 1.2.0 - 1.2"

efanXtra components always pair up a Fantom source file and a template file, just like what we've done with Layout.

Differences in the source code are:

  • There is no boilerplate code. All that is taken care of.
  • Components are const mixins not classes.
  • Initialise render methods need to be annotated with @InitRender.
  • All fields must be abstract because they're in a mixin.

This means our Layout.fan is reduced to:

select all
using afEfanXtra::EfanComponent
using afEfanXtra::InitRender

const mixin Layout : EfanComponent{
    abstract Str? title

    @InitRender
    Void initRender(Str title) {
        this.title = title
    }
}

As for the template, it is rendered as if it is a method inside the Layout.fan meaning there is no ctx variable, it can access the title field directly:

select all
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1>Bed Nap Tutorial</h1>

    <%= renderBody %>
</body>
</html>

And that's your first efanXtra component! It may be rendered via the EfanXtra service. But first we're going to convert the page templates into components.

Convert Pages to efanXtra Components

If you look at IndexPage.fan, once you remove the boilerplate ctor and the render method, all the efan template needs is the VisitSerice so it can retrieve all the visits:

using afIoc::Inject
using afEfanXtra::EfanComponent

const mixin IndexPage : EfanComponent {
    @Inject abstract VisitService visitService
}

(Pretty small, huh!?)

Which leaves us with the tricky question of; How do we render the Layout component?

efanXtra was created specifically for rendering components, and that includes nested components too. efanXtra will scan all the classes in a given pod looking for components. It keeps these components in a class called a library. Each library has methods for rendering its components. Our library will have methods called:

renderLayout(...)
renderIndexPage()
renderViewPage(...)

To add the components from our bednap pod into a library, contribute to the EfanLibraries service in our AppModule:

@Contribute { serviceType=EfanLibraries# }
static Void contributeEfanLibs(Configuration config) {
    config["app"] = Pod.find("bednap")
}

Note that we've contributed our pod under the name app. This name is very important. EfanXtra injects all the library classes into every component template. Each library is accessed via it's name. Given our library is called app we can call the Layout render method with:

<% app.renderLayout(...) %>

Okay, sounds easy... but how do we call Layout.initRender() to pass the title in? Well, the render method has the exact method signature as initRender(), and indeed, does call initRender() with the same arguments.

Knowing this we can update IndexPage.efan to:

select all
<%= app.renderLayout("Bed Nap Index Page") { %>
    <h2>Summary Page</h2>

    <table>
        <tr>
            <th>Name</th>
            <th>Date</th>
            <th>Rating</th>
            <th></th>
        </tr>
        <% visitService.all.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>
<% } %>

Note we don't have to tell efanXtra where to find our template files. That is because by default efanXtra looks in the component's pod for a template file with the same name as the component but with a .efan extension. If the template existed elsewhere or under a different name, we could use the @TemplateLocation facet to tell efanExtra exactly where it is.

Now let's update ViewPage in the same way.

ViewPage.fan:

select all
using afEfanXtra::InitRender
using afEfanXtra::EfanComponent

const mixin ViewPage : EfanComponent {
    abstract Visit visit

    @InitRender
    Void initRender(Visit visit) {
        this.visit = visit
    }
}

ViewPage.efan:

<%= app.renderLayout("Bed Nap View Page") { %>
    <h2>Visit View Page</h2>

    <div class="t-name"><%= visit.name %> said:</div>
    <div class="t-comment"><%= visit.comment %></div>
    <div class="t-date">on <%= visit.date %></div>
    <div class="t-rate"><%= visit.rating %> / 5 stars</div>
    <div><a href="/">&lt; Back</a></div>
<% } %>

Update BedSheet Routes

Because we've converted IndexPage and ViewPage to efanXtra components, the methods to render the BedSheet Routes no longer exist. So instead we'll create a couple of simple render methods in a separate PageRoutes class and direct the Routes to those:

select all
using afIoc::Inject
using afBedSheet::Text
using afEfanXtra::EfanXtra

const class PageRoutes {
    @Inject private const EfanXtra efanXtra

    new make(|This|in) { in(this) }

    Text renderIndexPage() {
        html := efanXtra.component(IndexPage#).render
        return Text.fromHtml(html)
    }

    Text renderViewPage(Visit visit) {
        html := efanXtra.component(ViewPage#).render([visit])
        return Text.fromHtml(html)
    }
}

And the new Route contributions in AppModule:

@Contribute { serviceType=Routes# }
static Void contributeRoutes(Configuration config) {
    config.add(Route(`/`,        PageRoutes#renderIndexPage))
    config.add(Route(`/view/**`, PageRoutes#renderViewPage))
}

By now we should not be referencing any code from afEfan directly, so we can remove the service definition from AppModule and remove it as a dependency in build.fan.

Run It

We can test that all our hard work still works by running all our tests:

Bed Nap Test All

Sweet!

Also note the extra information that efanXtra prints out on startup:

efan Library: 'app' has 3 components:

      Layout : app.renderLayout(Str title)
  Index Page : app.renderIndexPage()
   View Page : app.renderViewPage(bednap::Visit visit)

This handy little cheat sheet tells you exactly what render methods are available in each library!

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 07-Components-with-efanXtra 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 07-Components-with-efanXtra

Don't forget, you can trial the finished tutorial application at Bed Nap.

Have fun!

Edits

  • 7 Aug 2016 - Updated tutorial to use BedSheet 1.5 & IoC 3.0.
  • 7 Aug 2015 - Updated tutorial to use BedSheet 1.4.
  • 3 Sep 2014 - Original article.


Discuss