Inspired by this nice Blog Post I used functional templates in one of our projects and immediately saw the following benefits:
- short and clear components and source files
- good separation of concerns: one component has only one task to do
- little effort to reuse functions and code
I talked about functional templates during a 5 minute lightning talk at this year's Inspiring Conference. Here are the slides.
What are Functional Templates?
Functional Templates are Handlebars Helpers or EmberJs Components which do not render any output. They perform simple and independent side-tasks, e.g. they change the variables of the current scope of your template. At first that does not sound useful at all, but consider simple constructs like if-then-else or for-each – widely accepted and known to be very useful indeed.
Example in EmberJS: Sorting a list
Consider you have a few items to render in a bullet list – sorted of course. Sounds easy at first, but it is worth its own component for the following reasons:
- Sorting might be by value or by property
- If the value is a String, you want sorting by lower-cased value
- Values might be null or undefined
The following examples shows the usage and implementation of functional templates in handlebars.
{{x-sort-by list=friends property="name" as |sorted|}}
{{#each sorted as |friend|}}
…
{{/each}}
{{/x-sort-by}}
import Ember from 'ember';
/**
* use "{{yield sorted}}" as handlebars template
*/
export default Ember.Component.extend({
list: undefined,
property: undefined,
sorted: function () {
const list = this.get("list");
if (!list) {
return [];
}
return list.sort((a, b) => {
const x = this._getValue(a);
const y = this._getValue(b);
return (x < y) ? -1 : (x > y) ? 1 : 0;
});
}.property("list", "property"),
_getValue(x) {
const property = this.get("property");
const value = property === null ? x : x[property];
if ((typeof value) === "string") {
return value.toLocaleLowerCase();
} else {
return value;
}
}
});
Handlebars.java: Filtering a list
Depending on the library the Handlebar Helpers are implemented differently. The following example runs with the Handlebars.java port of Handlebars.
{{findAll persons field="isFriend" assignTo="friends"}}
{{#sort friends sortBy="name" assignTo="sorted"}}
{{#each sorted}}
…
{{/each}}
{{/sort}}
{{/findAll}}
import com.github.jknack.handlebars.Context
import com.github.jknack.handlebars.Helper
import com.github.jknack.handlebars.Options
/**
* filters a list and adds a variable to the context
*
* options:
* - assignTo: name of the variable to add to the context
* - field: (optional) filter by "trueness" of the given property (this is default)
* may start with an '!' to indicate inversion of trueness
*
* {{#findAll list field="name" assignTo="filteredList"}}
* {{#each filteredList}}
* ...
* {{/each}}
* {{/findAll}}
*
*
* {{#findAll list field="!this" assignTo="filteredList"}}
* {{#each filteredList}}.
* ...
* {{/each}}
* {{/findAll}}
*/
class FindAllHelper implements Helper<Collection<Object>> {
public static final String id = "findAll"
@Override
public CharSequence apply(Collection<Object> context, Options options) throws IOException {
def assignTo = (String) options.hash["assignTo"]
if (!assignTo) {
throw new IllegalArgumentException("assignTo not set")
}
def field = (String) options.hash("field", "this")
def negate = field.startsWith("!")
if (negate) {
field = field.substring(1)
}
def filtered = (context ?: []).findAll { isTrue(it, field, negate) }
return options.fn.apply(
Context.newBuilder(options.context, [(assignTo): filtered]).build()
)
}
private static boolean isTrue(Object value, String field, boolean negate) {
if (field == "this") {
field = null
}
def result = getField(value, field)
if (negate) {
return !result
} else {
return result
}
}
protected static Object getField(Object value, String field) {
if (field) {
return ((Map) value)[field]
} else {
return value
}
}
}