Skip to content

An opinionated way of building templates, leveraging handlebars and typescript

Notifications You must be signed in to change notification settings

lifegadget/typed-template

Repository files navigation

typed-template

travis coveralls codecov license twitter

This library is meant to be a relatively simple, yet opinionated, library that will help transform structured information into various outputs based on expected output channels (e.g., email, sms, etc.)

Getting started

First add it as a dependency to your project:

# using npm
npm install --save typed-template
# using yarn
yarn add typed-template

Now you'll want to create some directories in which you'll leverage this solution. Create the following directories off the root of your project:

  • templates/templates
  • templates/layouts

Template Structure

Layouts

Now let's create the most basic layout you can create:

templates/layouts/default.hbs

{{template}}

Bear in mind a "layout" is a template that surrounds or encapsulates a template (more on templates in a moment). In the above example we are basically doing nothing other than displaying the underlying template. Not terribly exciting so let's take a more sophisticated example ... let's use "email" as an example.

typed-template layouts are organized into "channels" (and sub-channels). So in the case of email that is considered a "channel" which contains two sub-channels:

  • html
  • text

So just focusing on the html sub-channel we might create the following file:

templates/layouts/email-html/default.hbs

<h1>Welcome Earthling</h1>
<div class="body">
  {{template}}
</div>

whereas the text sub-channel might be:

templates/layouts/email-text/default.hbs

Welome Earthling

{{template}}

So now -- by default -- every email template will be wrapped by our default layouts.

Templates

Where layouts are templates "at a high level", the "official" templates which reside in the templates/templates directory represent the core contents of your message content. Unlike the layouts which are organized by "channel/sub-channel", the templates are organized by a template name. This makes sense because templates are likely to be structured on around topics and are less generic than layouts. That said, templates also have support for channel/sub-channel distinctions but the structure is inverted (aka, directories are created for topics, filenames distinguish the channel).

Let's look at another example. Let's imagine we have a website and we are sending "welcome" emails when customers sign-up and then two weeks later we send a follow-up email to keep engagement strong and offer some ideas on how to get more value from the website. We might then create two topics: "welcome" and "engagement". Further, because our customers are very "real-time" we have agreed to send and SMS and email every time there is an "outage".

This might look like so:

templates / templates
               \_ welcome
               \_ engagement
               \_ outage

In the welcome and engagement topics we are only expecting email so we'd likely have two handlebars files email-html.hbs and email-text.hbs. In the outage folder, however, we are also expecting SMS so we'd add sms.hbs too. Of course, if we wanted to keep things dead simple we could just put a default.hbs into any of the topic folders and ALL channels would use that template to build their messages.

Handlebars

Whether we're talking about templates or layouts the grammer they are being written in is a popular "curly-based" templating language called Handlebars. We will not cover the specifics of it here but please refer to their docs to understand the full scope of what you can do with your templates/layouts.

API Surface

Up to now we've talked about adding templating structure but not how you would use this in your code. Let's change gears. Here's an example modelled losely off of our "engagement" email example above ...

const data = {
  name: "Bob Barker",
  tier: "gold",
  memberSince: "23 July, 2018"
};
const template = await TypedTemplate.create()
  .topic("engagement")
  .channels("email")
  .substitute(data)
  .generate();

which would result in template being a data structure of:

interface ITypedTemplate {
  emailText: string;
  emailHtml: string;
}

and where the template.emailHtml would be composed in a web-browser or email client like so:

┌─────────────────────────┐
│  Layout / email-html /  │
│       default.hbs       │
│                         │
│   ┌─────────────────┐   │
│   │                 │   │
│   │   template /    │   │
│   │  engagement /   │   │
│   │ email-html.hbs  │   │
│   │                 │   │
│   └─────────────────┘   │
│                         │
└─────────────────────────┘

List Datasets

Above was a simple example where we were sending to a single user but more often there is a need to send to multiple users. The most typical way of achieving this is to pass in an array into the .substitute() method. This signals that there are multiple templates and the resulting .generate() call will result in:

const template: ITypedTemplate[] = ...

However, there is another way -- which may be more efficient in many cases -- where instead of the .generate() call you instead call .iterator():

const data = [ {},{}, ... ]
const template: ITypedTemplateIterable = TypedTemplate.create()
  .topic("engagement")
  .channels("email")
  .substitute(data)
  .iterator();

while(!template.next().done) {
  //...
}

Pre-compiling Templates

While not always necessary, it is not entirely uncommon to have templates written in Handlebars pre-compiled to a Javascript function. Then at runtime the compilation step is no longer needed. To aid in this, typed-template provides a static class method which will precompile all HBS files in your templates directory:

await TypedTemplate.precompile();

This can then be incorporated into your build tooling to ensure that precompiled templates are ready for use. In cases where you are taking this optimisation step you also need to add a call to .usePrecompiled() in the fluent interface so at runtime it knows it can skip compilation w/o needing to check using file IO.

About

An opinionated way of building templates, leveraging handlebars and typescript

Resources

Stars

Watchers

Forks

Packages

No packages published