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
- Convert Layout to an efanXtra Component
- Convert Pages to efanXtra Components
- Update BedSheet Routes
- Run It
- Source Code
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:
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
:
<!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
notclasses
. - Initialise render methods need to be annotated with
@InitRender
. - All fields must be
abstract
because they're in amixin
.
This means our Layout.fan
is reduced to:
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:
<!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:
<%= 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
:
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="/">< 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:
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:
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.