Efan XtraUser Guide

Overview

efanXtra creates managed libraries of reusable Embedded Fantom (efan) components. Influenced by Java's Tapestry 5, it pairs up Fantom classes and efan template files to encapsulate model / view behaviour.

efanXtra extends afEfan, is powered by afIoc and works great with Slim templates.

efanXtra excels in a web / BedSheet environment, where URLs are automatically mapped to efan components (see Pillow), but is presented here context free for maximum reuse. Think email, code generation, blog posts, etc...

Quick Start

Overdue.efan:

Dear <%= userName %>,

It appears the following rented DVDs are overdue:

    <%= dvds.join(", ") %>

Please return them at your convenience.

<% app.renderSignOff("The Management") %>

Overdue.fan:

using afIoc
using afEfanXtra

const mixin Overdue : EfanComponent {

  // use afIoc services!
  @Inject
  abstract DvdService? dvdService

  // mixin fields can be accessed by the template
  abstract Str? userName

  // use lifecycle methods to initialise your components
  @InitRender
  Void initRender(Str userName) {
    this.userName = userName
  }

  // mixin methods can be called from the template
  Str[] dvds() {
    dvdService.findByName(userName)
  }
}

AppModule.fan:

using afIoc

class AppModule {

  static Void bind(ServiceBinder binder) {
    binder.bindImpl(DvdService#)
  }

  @Contribute { serviceType=EfanLibraries# }
  static Void contributeEfanLibraries(MappedConfig config) {

    // contribute all components in the pod as a library named 'app'
    config["app"] = AppModule#.pod
  }
}

Then to render an efan component:

efanXtra.render(Overdue#, "Mr Smith")

Full example source code available on BitBucket.

Components

An efan component consists of a const mixin Fantom class and a corresponding efan template file.

efanXtra does away with efan's ctx variable and instead lets the template access fields and methods of the component class directly.

Mixins

Component mixins must be const and extend the EfanComponent mixin.

All fields and methods of the mixin are directly accessible in the template. Components are created by afIoc so feel free to @Inject services just as you would in a service class.

Any non-private method or field in your mixin is visible from your template code.

See Lifecycle Methods for how to initialise and pass data into your components.

Templates

By default, the template file has the same name as the component (with a .efan extension) and lives anywhere in a pod resource dir. For example, if you were to create a component called Layout, you may have the following files:

/fan/components/Layout.fan
/fan/components/Layout.efan

Be sure to add /fan/components/ as a resDir in your build.fan.

ALIEN-AID: Note resource directories in your build.fan are NOT nested. Adding res/ will NOT add /res/components/ - /res/components/ needs to be added separately. Example:

resDirs = [`doc/`, `res/`, `res/components/`]

If you wish the template to have a different name to the Fantom class, set an explicit Uri with the @EfanTemplate facet. Example:

@EfanTemplate { uri=`fan://acmePod/templates/Notice.efan` }
const mixin Overdue : EfanComponent {
  ...
}

Libraries

Components are organised by libraries. A library encompasses all components within a pod. To use your efan components, you need add your pod as a library. Do this in your AppModule:

using afIoc
using afEfanXtra

class AppModule {

  ...

  @Contribute { serviceType=EfanLibraries# }
  static Void contributeEfanLibraries(MappedConfig config) {

    // contribute all components in the same pod as AppModule, as a library called 'app'
    config["app"] = AppModule#.pod
  }
}

Library classes are automatically added as fields in your components. Library classes contain component render methods. In the example above, the library (in a field named app) would bave 2 render methods, available for use in your templates:

app.renderOverdue(Str userName)
app.renderSignOff(Str who)

This allows you to render components from within templates by calling <libName>.render<ComponentName>(...)

ALIEN-AID: Library render methods are logged at registry startup so you don't have to remember them!

Lifecycle Methods

Components can be thought of as having a lifecycle. Components can be made aware of the lifecycle by annotating callback methods with lifecycle facets. The lifecycle only exists for the duration of the render.

efanXtra Component Lifecycle

@InitRender

A method annotated with @InitRender will be called before any other. It allows you to initialise and prepare your component for rendering.

The @InitRender may take any parameters, the signature will be mimicked by the containing library's render method. Example, if your @InitRender looks like:

const mixin MyComponent : EfanComponent {

  @InitRender
  Obj? initRender(Int x, Str y) { ... }

  ...
}

then the library render method will look like:

Obj? renderMyComponent(Int x, Str y) { ... }

This lets you pass any arguments you want into your components.

@InitRender methods may return any object. If defined as Void or it returns null or true then rendering continues as normal. Any other object aborts the rendering and is passed back via the library render method.

@BeforeRender

A method annotated with @BeforeRender is invoked after @InitRender but before any template rendering. It may optionally take a StrBuf as a parameter, this will hold the contents of the current render buffer.

@InitRender methods may return Bool. If true, the template is rendered as usual. If false, template rendering is skipped.

@AfterRender

A method annotated with @AfterRender is invoked after the template has rendered. It may optionally take a StrBuf as a parameter, this will hold the contents of the current render buffer.

@AfterRender methods may return Bool. If true, the template is rendered as usual. If false, template rendering is skipped.

Render Variables

Efan components can store state! Components may be const and they may be mixins, but they can still store variables that can be accessed and used by the rendering template. Taking the Overdue component as an example:

Overdue.fan:

using afEfanXtra

const mixin Overdue : EfanComponent {

  abstract Str? userName

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

userName is a render variable used by the efan template. Before initRender() is called, all render variables are reset to null (hence they need to be nullable). They may then be initialised during initRender(), and used and / or reset at any other point in the rendering lifecycle.

Note that state is not preserved between different renderings of the component.

Use with Slim

efanXtra works great with Slim! Just add the following to your AppModule and efanXtra will automatically pick up component templates with the extension .slim:

using afIoc
using afSlim
using afEfanXtra

class AppModule {

  static Void bind(ServiceBinder binder) {
    binder.bindImpl(Slim#)
  }

  @Contribute { serviceType=EfanTemplateConverters# }
  internal static Void contributeSlimTemplates(MappedConfig config, Slim slim) {
    config["slim"] = |File file -> Str| { slim.parseFromFile(file) }
  }
}

Release Notes

v1.0.0

  • New: Introduced component lifecycle methods: @InitRender, @BeforeRender & AfterRender.
  • New: Added @Abstract facet to mark base components.
  • Chg: Template rendering is held in a single mutable StrBuf so it may be manipulated by lifecycle methods.
  • Chg: Components are now defined by extending EfanComponent, not by annotating with a @Component facet.
  • Chg: Renamed to efanXtra (from efanExtra).

v0.0.8

  • New: Added EfanTemplateDirectories service which scans external directories for efan templates.
  • New: Compilation err msgs are updated with code hints should a simple component rendering typo be spotted. (ALIEN-AID)

v0.0.6

  • New: Fields may be annotated with any facet, not just @Inject. Think @Config!!!
  • Chg: Updated to efan-1.3.2
  • Chg: Better Err msgs if component template not found.

v0.0.4

  • New: Component templates can be specified via the @Component.template field.
  • New: Added a contributable EfanTemplateFinders service.
  • New: Ability to suppress startup log messages.
  • New: Added compiler hooks (mainly for afPillow).
  • New: efanXtra.component() returns component instances.
  • Chg: @Component facet is inherited.

v0.0.2

  • New: Preview Release