Hey there! The documentation for this module is still being rewritten.
This is an implementation of Handlebars that processes templates into JSON Logic, which is then used by JSON Logic Engine to optimize and evaluate the templates.
This implementation of Handlebars appears to be ~25x faster than the original implementation, and allows for more variety in execution strategy.
I will admit upfront that there are some differences in this implementation compared to the original Handlebars. I will try to document these differences as I go along.
Some of the obvious differences include:
- Asynchronous Execution is fully supported; this means you can add async helpers natively.
Iteration is strictly done via block expressions like#each
, implicit iteration is not supported. (Use#each kids
instead of#kids
)- There are significantly more built-in helpers, which I may remove or document as I publish this module.
The whitespace control is currently not supported in the grammar. (I may add it later) Ex.{{~foo}}
is not supported.- Whitespace control is supported, but is handled by a preprocessor and not the grammar. This nuance probably won't affect you, but it's worth mentioning. (Essentially, the whitespace is stripped before the template is parsed)
- Inline Partials are supported, but slightly different currently.
To avoid additional syntax,as
is not supported in block expressions, I chose to use hash arguments inwith
instead.-
as
is supported in block expressions, however it will incur a performance cost / disable inline optimizations as it will perform a recursive lookup. In spite of the de-optimization, it should still perform quite reasonably. -
with
supports hash arguments, which allows for more flexibility in how the block is executed. - Supports an (optimized) Interpreted Mode for both synchronous and asynchronous execution; this means you execute templates in browser contexts that disallow
new Function
oreval
. - If you need to access the index / private context of an above iterator,
../@index
is used instead of@../index
. I will likely change this to match the original handlebars. This is kind of a niche edge case, but it's worth mentioning.
I believe these differences are relatively minor and do not impact most use cases, and should be easy to work aorund, but I might iterate on this in the future.
To install:
bun install handlebars-jle
Conditionals are done via the if
block helper.
There is also support for if/else if chains
Iteration is done via block expressions like #each
, and the block helper is used to define the iteration context.
Data:
[
{ "name": "John", "age": 5 },
{ "name": "Jane", "age": 7 }
]
And over objects,
Data:
{
"John": 5,
"Jane": 7
}
Variable Traversal Syntax is supported with ../
and @key
and @index
are supported.
The with
block helper has been adjusted a bit from the original Handlebars. It now supports hash arguments, which allows for more flexibility in how the block is executed.
You can add helpers by adding methods to the JSON Logic Engine.
import { Handlebars } from 'handlebars-jle';
const hbs = new Handlebars();
hbs.engine.addMethod('addOne', ([a]) => a + 1, { sync: true, deterministic: true });
const template = hbs.compile('{{addOne age}}');
template({ age: 5 }); // 6
template({ age: 10 }); // 11
If your method is synchronous and deterministic (same input always produces the same output, and it will never return a promise), you should specify that in the options. This will allow the engine to optimize the method; and if synchronous, allow it to be used by compile
Here is a more interesting example, using async support:
import { AsyncHandlebars } from 'handlebars-jle';
const hbs = new AsyncHandlebars();
hbs.engine.addMethod('fetch', async ([url]) => {
const response = await fetch(url)
return response.json()
})
const template = hbs.compile(`{{#each (fetch 'https://jsonplaceholder.typicode.com/users')}}
@{{username}} - {{name}}
{{/each}}`)
template().then(console.log)
Would produce:
@Bret - Leanne Graham
@Antonette - Ervin Howell
@Samantha - Clementine Bauch
@Karianne - Patricia Lebsack
@Kamren - Chelsey Dietrich
@Leopoldo_Corkery - Mrs. Dennis Schulist
@Elwyn.Skiles - Kurtis Weissnat
@Maxime_Nienow - Nicholas Runolfsdottir V
@Delphine - Glenna Reichert
@Moriah.Stanton - Clementina DuBuque
This module supports both a compiled mode and an "interpreted" mode.
The compiled mode is faster, but the interpreted mode is more flexible and can be used in environments that disallow eval
or new Function
.
For example:
const { Handlebars, AsyncHandlebars } = require('handlebars-jle');
const hbs = new Handlebars();
const hbsAsync = new AsyncHandlebars();
const hbsInterpreted = new Handlebars({ interpreted: true });
const hbsAsyncInterpreted = new AsyncHandlebars({ interpreted: true });
const hello = hbs.compile('Hello, {{name}}!')
const helloAsync = hbsAsync.compile('Hello, {{name}}!')
const helloInterpreted = hbsInterpreted.compile('Hello, {{name}}!')
const helloAsyncInterpreted = hbsAsyncInterpreted.compile('Hello, {{name}}!)
Any of these can now be run with:
hello('Jesse') // Hello, Jesse!
helloAsync('Bob') // Promise<'Hello, Bob!'>
helloInterpreted('Steve') // Hello, Steve!
helloAsyncInterpreted('Tara') // Promise<Hello, Tara!>
If you have no async helpers to add to your templates, it's strongly recommended you use the synchronous class.
While the compiler / optimizer will make the execution fully synchronous if everything in the template is not async, there is still some overhead in JavaScript engines that slow it down a bit when packing it into a promise, so it should only be used if you've added async helpers to your engine you'd like to use.
You can add partials by using register
,
import { Handlebars } from 'handlebars-jle';
const hbs = new Handlebars();
hbs.register('greeting', 'Hello, {{name}}!')
const template = hbs.compile('{{>greeting name="Jesse"}}');
template(); // Hello, Jesse!
Of some note, partials that can be fully evaluated and inlined will be! In the above case, since greeting
receives all of the information it needs as constants, it will fully evaluate the partial and inline it into the template.
import { Handlebars } from 'handlebars-jle';
const hbs = new Handlebars({ interpreted: true });
hbs.register('greeting', 'Hello, {{name}}!')
const template = hbs.compile('{{>greeting name="Jesse"}}');
template(); // Hello, Jesse!
Works in the exact same way, but it will not use eval
or new Function
to compile the partial, which is useful in environments that disallow it. While this is not "compiled", this too will inline the partial if it can be fully evaluated.
Inline Partials are supported but have a slightly different syntax as they do not use the deprecated decorators syntax.
This will produce the same output as the previous example.
The reason they are flagged as experimental is because they currently do not scope themselves exclusively to the template that registered them. This means that if you register an inline partial in one template, it will be available in all templates. This is not ideal, and I will be working on a solution to this in the future.
This needs fleshed out documentation. This needs some explanation because you're able to deeply optimize the block helper in some powerful ways.