Templating met Handlebars.js

June 2nd, 2013 | 13 min read | Dojo, Handlebars.js, JavaScript, Web

In een aantal van mijn vorige tutorials heb ik gebruik gemaakt van Meteor. Één van de concepten die Meteor voorziet is templating. De templating engine die Meteor gebruikt is Handlebars.js. In deze tutorial ga ik dieper in over Handlebars.js, maar dan zonder Meteor erbij te betrekken. Meteor is immers een platform dat allerlei JavaScript frameworks en technologieën bundelt tot één platform.
Ik ga de principes achter Handlebars.js illustreren met een RSS reader die ik ga opbouwen puur uit JavaScript (Dojo + Handlebars.js).

Project opzetten

Maak een nieuw web project (ik gebruik hiervoor Aptana Studio) en voorzie een bestand index.html en een map assets. In deze map maak je een map aan met de naam js en daarin het bestand additions.js. Omdat we in dit project een RSS reader gaan maken, moet je ook een RSS feed downloaden (bijvoorbeeld https://dimitr.im/feed/) en deze in de root map van je project plaatsen.
Omdat je via JavaScript geen cross-domain requests kunnen uitvoeren moeten we de RSS feed op hetzelfde domein beschikbaar maken, vandaar deze aanpak. Dit is uiteraard niet de manier waarop je dit in de praktijk zou doen, de échte oplossing zou zijn om zelf een XML/RSS proxy te schrijven die wel op je domein draait. Maar omdat deze tutorial daar niet over gaat ga ik dat niet doen.

Je project zou er nu moeten uitzien als in onderstaande screenshot.

project-structure

Index pagina

De eerste stap is dat we een HTML5 template gaan gebruiken om het werk wat eenvoudiger te maken. De HTML template die ik ga gebruiken is de volgende:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />

        <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
        Remove this if you use the .htaccess -->
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

        <title>Handlebars.js - RSS Reader</title>
        <meta name="description" content="" />
        <meta name="author" content="g00glen00b" />

        <meta name="viewport" content="width=device-width; initial-scale=1.0" />
    </head>

    <body>

    </body>
</html>

Hier staat nog niet veel interessants in, behalve dat we de

<head>

aangevuld hebben met enkele extra’s. De volgende stap is dat we de nodige resources gaan toevoegen. In deze tutorial ga ik gebruik maken van:

  • Dojo Toolkit
  • Handlebars.js
  • Twitter Bootstrap

Dit gaan we doen door volgende resources te importeren  (CDN) onderaan de

<head>

:

<script src="//ajax.googleapis.com/ajax/libs/dojo/1.9.0/dojo/dojo.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0-rc.4/handlebars.min.js" type="text/javascript"></script>
<script src="assets/js/additions.js" type="text/javascript"></script>

<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" />

Vlak daarboven gaan we de Dojo configuratie vastleggen:

<!-- Dojo config -->
<script type="text/javascript">
    dojoConfig = {
        parseOnLoad : true,
        async : true
    }
</script>

Daarna gaan we beginnen aan de content zelf. Ik ga een eenvoudige form voorzien met daarin een veld om een URL in te voeren. Daarnaast ga ik ook een lege div aanmaken die we later gaan gebruiken om op te vullen met de resultaten zelf.
De content voor de

<body>

is:

<div class="container">
    <header>
        <h1>Handlebars.js - RSS Reader</h1>
    </header>
    <form id="go-form" class="form-horizontal">
        <div class="controls-row">
            <input class="span10" id="feed" placeholder="e.g. http://localhost/handlebars-rss-reader-example/feed" type="text" />
            <button class="span2 btn" type="submit">
                Go!
            </button>
        </div>
    </form>
    <div id="results"></div>
</div>

Zoals je kan zien gebruiken we hier een aantal keer het id attribuut, dit maakt het makkelijker om nadien via JavaScript iets te doen met de DOM node. De class names die ik aan de controls gegeven heb zijn voor Twitter Bootstrap om de lay-out te voorzien.

Handlebars.js templates

Nu we de basis user interface vastgelegd hebben is het tijd om aan de slag te gaan met de Handlebars.js templates. We gaan twee templates voorzien, één voor foutmeldingen en een andere voor de resultaten. De template voor de foutmeldingen is het meest eenvoudige, vandaar gaan we daar eerst mee aan de slag.

De eenvoudigste manier om een Handlebars.js template te maken is om een

<script>

tag te voorzien met een ID en daarin de HTML template, bijvoorbeeld:

<script id="error-template" type="text/x-handlebars-template">
    ...
</script>

Voor de error template gaan we een Twitter Bootstrap alert voorzien waarin het bericht geplaatst kan worden. De template zal er uiteindelijk zo uit zien:

<script id="error-template" type="text/x-handlebars-template">
    <div class="alert alert-error">
        <strong>Oh snap!</strong> {{message}}
    </div>
</script>

Het meeste hierin is gewoonweg HTML met als enige uitzondering de placeholder

{{message}}

die we gebruiken. Dit wilt eigenlijk zeggen dat wanneer we een context toekennen aan de template en we een object met een property message meegeven, deze zal gebruikt worden in de HTML template, bijvoorbeeld:

{
    message: "Dit is een foutmelding"
}

Hoe we deze context gaan toekennen aan een template zal ik zometeen bespreken.

De volgende template is de entries template waarmee we de RSS feed gaan omtoveren tot een HTML pagina. Deze template is iets uitgebreider omdat we hier over een collectie moeten gaan en een lus moeten gebruiken. De code hiervoor is:

<script id="rss-entries-template" type="text/x-handlebars-template">
    {{#each entries}}
        <article>
            <header>
                <h3>{{this.title}} <small>{{this.pubDate}}</small></h3>
            </header>
            {{{this.description}}}
            <footer>
                <a href="{{this.link}}">Lees meer &raquo;</a>
            </footer>
        </article>
        {{#unless $last}}
            <hr />
        {{/unless}}
    {{/each}}
</script>

Wat we hier hebben is een lus die we aanroepen met

{{#each entries}}

. Alles wat tussen de

{{#each}}

en

{{/each}}

staat wordt daardoor uitgevoerd per object in de

entries

array.
Om het huidige object op te halen gebruiken we

{{this}}

. We kunnen uiteraard ook properties ophalen via

{{this.title}}

voor bijvoorbeeld de titel. Handlebars escaped automatisch alle HTML in de placeholders. Als je dit niet wenst dan moet je een extra paar brackets

{}

rond de placeholder plaatsen, bijvoorbeeld:

{{{this.description}}}

.

Uiteindelijk willen we ook nog dat er een seperator (

<hr />

) tussen twee artikels komt te staan. Uiteraard moet dit niet gebeuren bij het laatste artikel, vandaar plaatsen we een “if not” conditie met een

{{#unless}}

.

Plaats deze twee templates vlak boven de dojo configuratie zodat je index.html er zo uit ziet:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />

        <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
        Remove this if you use the .htaccess -->
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

        <title>Handlebars.js - RSS Reader</title>
        <meta name="description" content="" />
        <meta name="author" content="g00glen00b" />

        <meta name="viewport" content="width=device-width; initial-scale=1.0" />

        <!-- Handlebars.js templates -->
        <script id="rss-entries-template" type="text/x-handlebars-template">
            {{#each entries}}
                <article>
                    <header>
                        <h3>{{this.title}} <small>{{this.pubDate}}</small></h3>
                    </header>
                    {{{this.description}}}
                    <footer>
                        <a href="{{this.link}}">Lees meer &raquo;</a>
                    </footer>
                </article>
                {{#unless $last}}
                    <hr />
                {{/unless}}
            {{/each}}
        </script>
        <script id="error-template" type="text/x-handlebars-template">
            <div class="alert alert-error">
                <strong>Oh snap!</strong> {{message}}
            </div>
        </script>

        <!-- Dojo config -->
        <script type="text/javascript">
            dojoConfig = {
                parseOnLoad : true,
                async : true
            }
        </script>
        <script src="//ajax.googleapis.com/ajax/libs/dojo/1.9.0/dojo/dojo.js" type="text/javascript"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0-rc.4/handlebars.min.js" type="text/javascript"></script>
        <script src="assets/js/additions.js" type="text/javascript"></script>

        <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" />
    </head>

    <body>
        <div class="container">
            <header>
                <h1>Handlebars.js - RSS Reader</h1>
            </header>
            <form id="go-form" class="form-horizontal">
                <div class="controls-row">
                    <input class="span10" id="feed" placeholder="e.g. http://localhost/handlebars-rss-reader-example/feed" type="text" />
                    <button class="span2 btn" type="submit">
                        Go!
                    </button>
                </div>
            </form>
            <div id="results"></div>
        </div>
    </body>
</html>

JavaScript code

De volgende stap is dat we de JavaScript code gaan voorzien in assets/js/additions.js. We beginnen door met Dojo alle modules in te laden met:

/**
 * @author g00glen00b
 */
require([
    "dojo/on",
    "dojo/html",
    "dojo/query",
    "dojo/dom",
    "dojo/request/xhr",
    "dojo/_base/array",
    "dojo/date/locale",
    "dojo/domReady!"], function(on, html, query, dom, xhr, array, dateLocale) {

});
Module naam Doel
dojo/on Met deze module kan je event handlers definiëren. In deze tutorial ga ik dit gebruiken om het submit event van de form op te vangen.
dojo/html Met deze module kan je de HTML code van een DOM node aanpassen. In deze tutorial ga ik dit gebruiken om de HTML code voor het resultaat te voorzien.
dojo/query Met deze module kan je query’s (in de vorm van selectors) op DOM nodes uitvoeren. In deze tutorial ga ik dit voornamelijk gebruiken om query’s op het XML document (RSS feed) uit te voeren.
dojo/dom Met deze module vind je enkele DOM utilities terug. Ik ga deze module voornamelijk gebruiken voor de

byId()

functie

dojo/request/xhr Met deze module kan je XHR requests uitvoeren. In deze tutorial ga ik deze module gebruiken om de RSS feed op te halen.
dojo/_base/array Met deze module krijg je extra functies voor array’s tot je beschikking. Ik ga deze module gebruiken voor de

forEach()

functie om door een array te lopen.

dojo/date/locale Met deze module kan je datums formatteren naar wat je wenst. Ik ga deze module gebruiken om de pubDate om te zetten naar een “leesbaar” formaat.
dojo/domReady Met deze module kan je ervoor zorgen dat bepaalde JavaScript code pas uitgevoerd wordt indien de volledige DOM geladen is.

De volgende stap is dat ik nu de Handlebars templates ga parsen, dit kan met de volgende code:

var errorTemplate = Handlebars.compile(dom.byId("error-template").innerHTML);
var entriesTemplate = Handlebars.compile(dom.byId("rss-entries-template").innerHTML);

Standaard biedt Handlebars niet veel extra’s voor de

{{#each}}

helper. Daarom dat ik deze ga uitbreiden met functionaliteit om te bepalen of we bezig zijn met het eerste/laatste item. De code om een helper te creëeren is:

Handlebars.registerHelper("each",function(arr, options) {
    if(options.inverse && !arr.length)
        return options.inverse(this);

    return arr.map(function(item,index) {
        item.$index = index;
        item.$first = index === 0;
        item.$last  = index === arr.length-1;
        return options.fn(item);
    }).join('');
});

Daarna kunnen we beginnen met de échte Dojo code. Alles begint vanaf de form gesubmit wordt, dit kunnen we opvangen in Dojo door middel van volgende code:

on(dom.byId("go-form"), "submit", function(evt) {
    evt.preventDefault();
    ...
});

Wat we gaan doen in deze event handler is de waarde van het tekstveld ophalen en via een XHR request de data van die RSS feed ophalen. Zo’n XHR request schrijf je door middel van:

xhr(dom.byId("feed").value, {
    handleAs: "xml"
}).then(function(xmlDoc) {
    ...
}, function(err) {
    ...
});

Zoals je kan zien hebben we hier twee callbacks. De eerste wordt aangeroepen indien de request succesvol was terwijl de tweede aangeroepen wordt indien de request niet succesvol was. Deze laatste callback is eenvoudiger qua code, vandaar dat we daar eerst mee gaan beginnen.

Error callback

Het object

err

dat we terug krijgen in de callback bevat allerlei properties waarvan één het bericht vat (namelijk

err.message

). Dit maakt het zeer eenvoudig omdat we daardoor al direct voldoen aan de context om de error template aan te spreken.
Om de template te parsen met de juiste context, gebruiken we volgende code:

var output = errorTemplate(err);

Omdat we echter de output direct wensen te tonen aan de gebruiker, gebruiken we volgende code:

html.set(dom.byId("results"), errorTemplate(err));

Succes callback

Het andere en meer complexe scenario is de callback indien de request succesvol was. Hier moeten we echter zelf wel nog de context opbouwen met de volgende structuur zodat deze in de templates gebruikt kan worden:

{
    entries: [{
        title: "...",
        description: "...",
        pubDate: "...",
        link: "..."
    }, {
        title: "...",
        description: "...",
        pubDate: "...",
        link: "..."
    }, ... }]
}

Als eerste stap gaan we daarom al de basis van de context voorzien, namelijk:

var context = {
    entries: []
};

Daarna gaan we in een foreach alle items ophalen, dit doen we met:

array.forEach(query("item", xmlDoc), function(item, idx) {
    ...
});

Daarna moeten we het XML document verder uitlezen en alles in één object steken dat voldoet aan de context die ik daarjuist getoond heb. De code hiervoor is:

context.entries.push({
    title: query("title", item)[0].firstChild.data,
    link: query("link", item)[0].firstChild.data,
    description: query("description", item)[0].firstChild.data,
    pubDate: dateLocale.format(new Date(query("pubDate", item)[0].firstChild.data), {
        selector: "date",
        formatLength: "short"
    })
});

Zoals je kan zien gebruiken we ook hier de

query()

voor. De data in elke node halen we op met

firstChild.data

. Omdat de datums in ISO formaat getoond worden en we daar toch iets anders van willen maken, maken we gebruik van de dojo/date/locale module om het formaat van de datum te wijzigen.
Dit object voegen we toe aan

context.entries

door middel van de

push()

functie (hiermee wordt het huidige object achteraan de array toegevoegd).

Ten slotte moeten we enkel nog de entries template parsen met de juiste context en als resultaat tonen door middel van:

html.set(dom.byId("results"), entriesTemplate(context));

De JavaScript die je uiteindelijk zou moeten bekomen is:

/**
 * @author g00glen00b
 */
require([
    "dojo/on",
    "dojo/html",
    "dojo/query",
    "dojo/dom",
    "dojo/request/xhr",
    "dojo/_base/array",
    "dojo/date/locale",
    "dojo/domReady!"], function(on, html, query, dom, xhr, array, dateLocale) {
    var errorTemplate = Handlebars.compile(dom.byId("error-template").innerHTML);
    var entriesTemplate = Handlebars.compile(dom.byId("rss-entries-template").innerHTML);
    Handlebars.registerHelper("each",function(arr, options) {
        if(options.inverse && !arr.length)
            return options.inverse(this);

        return arr.map(function(item,index) {
            item.$index = index;
            item.$first = index === 0;
            item.$last  = index === arr.length-1;
            return options.fn(item);
        }).join('');
    });
    on(dom.byId("go-form"), "submit", function(evt) {
        evt.preventDefault();
        xhr(dom.byId("feed").value, {
            handleAs: "xml"
        }).then(function(xmlDoc) {
            var context = {
                entries: []
            };
            array.forEach(query("item", xmlDoc), function(item, idx) {
                context.entries.push({
                    title: query("title", item)[0].firstChild.data,
                    link: query("link", item)[0].firstChild.data,
                    description: query("description", item)[0].firstChild.data,
                    pubDate: dateLocale.format(new Date(query("pubDate", item)[0].firstChild.data), {
                        selector: "date",
                        formatLength: "short"
                    })
                });
            });
            html.set(dom.byId("results"), entriesTemplate(context));
        }, function(err) {
            html.set(dom.byId("results"), errorTemplate(err));
        });
    });
});

Testen

Dan rest er ons niets anders dan het project te testen. Host de code en ga naar je index pagina.

result-basic

Voer nu de locatie in van je RSS feed en klik op Go!, zoals je ziet worden alle items mooi getoond op je HTML pagina volgens de Handlebars.js template die we aangemaakt hebben.

result-success

Als we nu een foutieve URL invoeren krijgen we de andere template te zien:

result-error

Daarmee zijn we dan ook aan het einde gekomen van deze tutorial. Handlebars.js voorziet op een zeer nette manier templates die we kunnen gebruiken (en herbruiken) zonder te veel ondersteunende code te moeten schrijven. Dit is zeker handig als je web applicaties schrijft waarbij je vaak XHR requests gebruikt.
Voor web applicaties waarbij XHR requests zelden of nooit gebruikt worden is dit misschien minder nuttig omdat hier MVC frameworks vaak al helpen bij het omzetten van model-data naar views (JSTL bijvoorbeeld).

Download project

Download – handlebars-rss-reader-example.tar.gz (6 kB)
Demo

Back to tutorialsContact me on TwitterDiscuss on Twitter

Profile picture

Dimitri "g00glen00b" Mestdagh is a consultant at Cronos and tech lead at Aquafin. Usually you can find him trying out new libraries and technologies. Loves both Java and JavaScript.