@temelj/handlebars
Handlebars template engine with an extensive collection of built-in helpers and type-safe helper registration using Zod.
Installation
npm install @temelj/handlebars
Overview
The @temelj/handlebars package provides:
Registry-based Handlebars instance management
40+ built-in helpers for arrays, strings, objects, and values
Type-safe helper creation with Zod validation
Switch/case statement helpers
Partial and template rendering utilities
Quick start
import { Registry } from "@temelj/handlebars" ;
const registry = new Registry ();
registry . includeAllHelpers ();
const result = registry . render (
"Hello {{upperCase name}}!" ,
{ name: "world" }
);
console . log ( result ); // "Hello WORLD!"
Registry
The Registry class manages a Handlebars instance with helpers and partials:
class Registry {
constructor ();
includeAllHelpers () : Registry ;
compile ( source : string , options ?: CompileOptions ) : TemplateDelegate ;
render ( source : string , data ?: unknown , options ?: CompileOptions ) : string ;
registerHelper ( name : string , helper : HelperDelegate ) : void ;
registerHelpers ( helpers : HelperDeclareSpec ) : void ;
registerPartial ( name : string , template : Template ) : void ;
get partials () : PartialSpec ;
}
Basic usage
import { Registry } from "@temelj/handlebars" ;
const registry = new Registry ();
registry . includeAllHelpers ();
const output = registry . render (
"{{name}} is {{age}} years old" ,
{ name: "Alice" , age: 30 }
);
console . log ( output ); // "Alice is 30 years old"
import { Registry } from "@temelj/handlebars" ;
const registry = new Registry ();
registry . includeAllHelpers ();
const template = registry . compile ( "Hello {{name}}!" );
console . log ( template ({ name: "World" })); // "Hello World!"
console . log ( template ({ name: "Alice" })); // "Hello Alice!"
Built-in helpers
String helpers
Transform and manipulate strings:
Case conversion
String splitting
String joining
{{ camelCase "hello world" }} {{!-- helloWorld --}}
{{ snakeCase "hello world" }} {{!-- hello_world --}}
{{ pascalCase "hello world" }} {{!-- HelloWorld --}}
{{ titleCase "hello world" }} {{!-- Hello World --}}
{{ kebabCase "hello world" }} {{!-- hello-world --}}
{{ capitalize "hello" }} {{!-- Hello --}}
{{ upperCase "hello" }} {{!-- HELLO --}}
{{ lowerCase "HELLO" }} {{!-- hello --}}
String helper API
camelCase ( str : string ): string
snakeCase ( str : string ): string
pascalCase ( str : string ): string
titleCase ( str : string ): string
kebabCase ( str : string ): string
capitalize ( str : string ): string
upperCase ( str : string ): string
lowerCase ( str : string ): string
split ( str : string , separator ?: string ): string []
splitPart ( str : string , index : number , separator ?: string ): string
splitPartSegment ( str : string , from : number , to : number , separator ?: string ): string
join ( ... values : unknown []): string
Array helpers
Work with arrays and collections:
Array creation
Array access
Array operations
Array filtering
{{ #each ( array "a" "b" "c" ) }}
{{ this }}
{{ /each }}
Array helper API
array ( ... items : unknown []): unknown []
arrayItemAt ( array : unknown [], index : number ): unknown
arrayContains ( array : unknown [], item : unknown ): boolean
arrayJoin ( array : unknown [], separator : string ): string
arrayFilter ( array : unknown [], predicate : string ): unknown []
arrayFilter takes a Handlebars template string as the predicate. The template is compiled and applied to each item.
Value helpers
Comparisons, logic, and value operations:
Comparisons
Logic
Default values
JSON serialization
Empty checks
JavaScript values
{{ #if ( eq status "active" ) }}
Active
{{ /if }}
{{ #if ( ne count 0 ) }}
Has items
{{ /if }}
{{ #if ( gt age 18 ) }}
Adult
{{ /if }}
{{ #if ( lte score 100 ) }}
Valid score
{{ /if }}
Value helper API
eq ( a : PrimitiveValue , b : PrimitiveValue ): boolean
ne ( a : PrimitiveValue , b : PrimitiveValue ): boolean
lt ( a : number , b : number ): boolean
gt ( a : number , b : number ): boolean
lte ( a : number , b : number ): boolean
gte ( a : number , b : number ): boolean
and ( ... args : unknown []): boolean
or ( ... args : unknown []): boolean
not ( value : boolean | number ): boolean
orElse ( value : unknown , defaultValue : unknown ): unknown
json ( value : unknown , pretty ?: boolean ): string
isEmpty ( obj : unknown ): boolean
jsValue ( value : unknown ): SafeString
Object helpers
Create and manipulate objects:
Create objects
Pick properties
{{ #with ( object name = "Alice" age = 30 active = true ) }}
{{ name }} - {{ age }} - {{ active }}
{{ /with }}
Object helper API
object ( ** hash : Record < string , unknown > ): Record < string , unknown >
objectPick ( obj : unknown , ... keys : string []): Record < string , unknown >
Core helpers
Template composition and variable management:
Set variables
Set root variables
Render partials
Inline rendering
{{ set myVar = "value" count = 42 }}
{{ @myVar }} {{!-- value --}}
{{ @count }} {{!-- 42 --}}
Core helper API
set ( ** hash : Record < string , unknown > ): void
setRoot ( ** hash : Record < string , unknown > ): void
partial ( path : string , ** hash : unknown ): string
render ( template : string , ** hash : unknown ): string
Use set and setRoot to create reusable variables within templates.
Switch/case helpers
Implement switch-case logic in templates:
{{ #switch type }}
{{ #case "article" }}
< article > {{ content }} </ article >
{{ /case }}
{{ #case "video" }}
< video src= " {{ url }} " ></ video >
{{ /case }}
{{ #default }}
< div > {{ content }} </ div >
{{ /default }}
{{ /switch }}
Multiple cases
{{ #switch status }}
{{ #case "draft" "pending" }}
Not published
{{ /case }}
{{ #case "published" }}
Live
{{ /case }}
{{ #default }}
Unknown status
{{ /default }}
{{ /switch }}
Custom helpers
Simple helpers
Register custom helpers directly:
import { Registry } from "@temelj/handlebars" ;
const registry = new Registry ();
registry . registerHelper ( "shout" , ( text : string ) => {
return text . toUpperCase () + "!!!" ;
});
const result = registry . render (
"{{shout message}}" ,
{ message: "hello" }
);
console . log ( result ); // "HELLO!!!"
Type-safe helpers with Zod
Create helpers with input validation:
import { Registry , createHelperZod } from "@temelj/handlebars" ;
import { z } from "zod" ;
const registry = new Registry ();
// Helper with validated parameters
registry . registerHelper (
"formatCurrency" ,
createHelperZod ()
. params ( z . number (), z . string (). optional (). default ( "USD" ))
. handle (([ amount , currency ]) => {
return new Intl . NumberFormat ( "en-US" , {
style: "currency" ,
currency ,
}). format ( amount );
})
);
const result = registry . render (
"{{formatCurrency price 'EUR'}}" ,
{ price: 42.5 }
);
console . log ( result ); // "€42.50"
If parameters don’t match the Zod schema, a ZodError will be thrown with detailed validation errors.
Helpers with hash arguments
import { Registry , createHelperZod } from "@temelj/handlebars" ;
import { z } from "zod" ;
const registry = new Registry ();
registry . registerHelper (
"link" ,
createHelperZod ()
. hash ( z . object ({
href: z . string (),
text: z . string (),
target: z . string (). optional (),
}))
. handle (( hash ) => {
const target = hash . target ? ` target=" ${ hash . target } "` : "" ;
return `<a href=" ${ hash . href } " ${ target } > ${ hash . text } </a>` ;
})
);
const result = registry . render (
`{{link href="https://example.com" text="Visit" target="_blank"}}`
);
console . log ( result );
// '<a href="https://example.com" target="_blank">Visit</a>'
Helpers with params and hash
import { Registry , createHelperZod } from "@temelj/handlebars" ;
import { z } from "zod" ;
const registry = new Registry ();
registry . registerHelper (
"repeat" ,
createHelperZod ()
. params ( z . string ())
. hash ( z . object ({
times: z . number (). default ( 1 ),
separator: z . string (). default ( "" ),
}))
. handle (([ text ], hash ) => {
return Array ( hash . times ). fill ( text ). join ( hash . separator );
})
);
const result = registry . render (
`{{repeat "Hello" times=3 separator=", "}}`
);
console . log ( result ); // "Hello, Hello, Hello"
Partials
Register and use template partials:
import { Registry } from "@temelj/handlebars" ;
const registry = new Registry ();
registry . includeAllHelpers ();
// Register partial
registry . registerPartial ( "header" , `
<header>
<h1>{{title}}</h1>
<p>{{subtitle}}</p>
</header>
` );
// Use partial
const result = registry . render ( `
{{> header}}
<main>Content here</main>
` , {
title: "My Site" ,
subtitle: "Welcome!"
});
Dynamic partials with helper
const registry = new Registry ();
registry . includeAllHelpers ();
registry . registerPartial ( "user-card" , `
<div class="card">
<h3>{{name}}</h3>
<p>{{email}}</p>
</div>
` );
const result = registry . render ( `
{{partial "user-card" name="Alice" email="[email protected] "}}
` );
Complete example
A comprehensive example showing multiple features:
import { Registry , createHelperZod } from "@temelj/handlebars" ;
import { z } from "zod" ;
// Create registry with all helpers
const registry = new Registry ();
registry . includeAllHelpers ();
// Register custom helper
registry . registerHelper (
"badge" ,
createHelperZod ()
. params ( z . string ())
. hash ( z . object ({
color: z . enum ([ "red" , "green" , "blue" ]). default ( "blue" ),
}))
. handle (([ text ], hash ) => {
return `<span class="badge badge- ${ hash . color } "> ${ text } </span>` ;
})
);
// Register partial
registry . registerPartial ( "product-card" , `
<div class="product">
<h3>{{titleCase name}}</h3>
<p class="price">{{formatCurrency price}}</p>
{{#if inStock}}
{{badge "In Stock" color="green"}}
{{else}}
{{badge "Out of Stock" color="red"}}
{{/if}}
<div class="tags">
{{arrayJoin tags ", "}}
</div>
</div>
` );
// Register currency helper
registry . registerHelper (
"formatCurrency" ,
createHelperZod ()
. params ( z . number ())
. handle (([ amount ]) => {
return `$ ${ amount . toFixed ( 2 ) } ` ;
})
);
// Render template
const template = `
<h1>{{upperCase title}}</h1>
{{#each products}}
{{> product-card}}
{{/each}}
{{#if (isEmpty products)}}
<p>No products available</p>
{{/if}}
` ;
const result = registry . render ( template , {
title: "Our Products" ,
products: [
{
name: "laptop computer" ,
price: 999.99 ,
inStock: true ,
tags: [ "electronics" , "computers" ],
},
{
name: "wireless mouse" ,
price: 29.99 ,
inStock: false ,
tags: [ "electronics" , "accessories" ],
},
],
});
console . log ( result );
Type exports
export class Registry {
constructor ();
includeAllHelpers () : Registry ;
compile ( source : string , options ?: CompileOptions ) : TemplateDelegate ;
render ( source : string , data ?: unknown , options ?: CompileOptions ) : string ;
registerHelper ( name : string , helper : HelperDelegate ) : void ;
registerHelpers ( helpers : HelperDeclareSpec ) : void ;
registerPartial ( name : string , template : Template ) : void ;
get partials () : PartialSpec ;
}
export function createHelperZod () : HelperZodBuilder ;
export class SafeString extends hbs . SafeString {}
export type HelperDelegate = ( ... args : any []) => any ;
export type HelperDeclareSpec = Record < string , HelperDelegate >;
export type Template = string | TemplateDelegate ;
export type PartialSpec = Record < string , TemplateDelegate >;
export interface CompileOptions {
data ?: boolean ;
compat ?: boolean ;
knownHelpers ?: KnownHelpers ;
knownHelpersOnly ?: boolean ;
noEscape ?: boolean ;
strict ?: boolean ;
assumeObjects ?: boolean ;
preventIndent ?: boolean ;
ignoreStandalone ?: boolean ;
explicitPartialContext ?: boolean ;
}
Helper builder API
interface HelperZodBuilder {
params < TParams >(
... schemas : TParams
) : HelperZodBuilderWithParams < TParams >;
hash < THash extends z . ZodSchema >(
schema : THash
) : HelperZodBuilderWithHash < THash >;
handle (
handler : ( context : HelperContext ) => HelperResult
) : HelperDelegate ;
}
The Zod helper builder provides runtime type validation, automatic error messages, and TypeScript type inference for helper parameters. This makes helpers safer and easier to debug.
Performance considerations
Helpers created with createHelperZod have minimal runtime overhead. Zod schemas are validated once per helper invocation, and the validation is fast for simple types.