Why use context helpers at all?

Let’s say that you had a monitoring app. If you were doing logical tests in your template, you might write this:

{! Don't do this !}
{@eq key="gearsTurning" value="true" type="boolean"}
  {@eq key="engineRunning" value="true" type="boolean"}
    {@eq key="onFire" value="false" type="boolean"}
      {@gt key="oilLevel" value="0.7"}
        Everything is awesome!
      {/gt}
    {/eq}
  {/eq}
{/eq}

But version 2.0 adds a new test, gyroscopeIsActive. You’ll have to add yet another nested conditional.

Version 3.0 features status messages for each error state. Your template is growing out of control!

This is where handlers, or context helpers (they’re the same thing by different names) can help. Your Dust context isn’t limited to containing data like strings, numbers, and arrays. You can also include functions directly in the context that provide new data or transform existing data. This means Dust contexts act like view models.

Using a handler, we could rewrite our template to something much simpler:

{#appStatusOK}
  Everything is awesome!
{/appStatusOK}

And move all the logic to a real Javascript function in your context:

{
  "appStatusOK": function() {
     return gearsTurning &&
            engineRunning &&
            !onFire &&
            oilLevel > 0.7;
  }
}

Now, your template doesn’t have to change, even if the conditions you’re testing change.

As a bonus, your handler can be much smarter than a template. Don’t worry about all the syntax here; we’ll go over it in more detail later.

{#appStatusOK} Everything is awesome! {:gearsError} Gears are stopped! Status code: {gears.error} {:engineError} Engine is not running! Engine temperature: {engine.temperature} {:oilLevelError} Oil level is too low! Current level: {engine.oilLevel} {/appStatusOK} { "gears": { "status": "OK", "error": false }, "engine": { "status": "OK", "error": false, "oilLevel": 0.5, "temperature": 80 }, "appStatusOK": function(chunk, context, bodies, params) { if(this.gears.error) { return chunk.render(bodies.gearsError, context); } else if(this.engine.error) { return chunk.render(bodies.engineError, context); } else if(this.engine.oilLevel < 0.7) { return chunk.render(bodies.oilLevelError, context); } return true; } }

Writing context helpers

Returning a value

The most basic context helpers simply return a value. The value will act just like a value contained in the context.

{#friends}{.} {/friends}{~n} {#friendsHelper}{.} {/friendsHelper}{~n} {?hasFriends}Yay friends!{/hasFriends}{~n} {?hasFriendsHelper}Yay friends!{/hasFriendsHelper} { "friends": ["Alice", "Bob", "Charlie"], "friendsHelper": function() { return ["Alice", "Bob", "Charlie"]; }, "hasFriends": true, "hasFriendsHelper": function() { return this.friends.length > 0; } }

Accessing the template

Context helpers are passed several parameters to provide information about their template and context. The full signature of a context helper is:

function(chunk, context, bodies, params) {}

Chunk

Context helpers can access the current section of their template to modify it. In Dust, this section is called a chunk. Returning the chunk instead of a value tells Dust that you have manually overridden the output of that variable or section.

{#status}No Status Available!{/status} { "hyperDriveStatus": "Warp Speed 9", "photonTorpedoCount": 800, "status": function(chunk) { return chunk.write("System Status\n".toUpperCase()) .write("Hyperdrive: " + this.hyperDriveStatus + "\n") .write("Photon Torpedoes: " + this.photonTorpedoCount); } }

Context

Context helpers can read values out of any level of the Dust context passed to the template, not just the current scope.

Remember that Dust contexts are “stacks” of objects, and that Dust can read upwards through multiple levels. For a refresher on contexts, see Contexts.

{#status} System Status: {#OK}OK!{:else}Horribly Wrong!{/OK} {/status} { "engine": { "temperature": 180, "rpm": 3100 }, "flywheel": { "active": true }, "status": { "OK": function(chunk, context) { var engineRPM = context.get(["engine", "rpm"]), flywheelActive = context.get("flywheel.active"); return (engineRPM < 9000) && flywheelActive; } } }

Bodies

As you’ve seen, Dust sections might have multiple bodies that output conditionally. The most common one is an {:else} body, which you might have used as part of an {?exists} block or an {@eq} helper.

Using a context helper, you can define your own bodies and have as many as you want. This lets you keep HTML and text in the template where it belongs, instead of conditionally outputting various strings as part of your Javascript.

{#login} Welcome! {:usernameError} Your username was not found. {:passwordError} Your password was incorrect. You have {.} attempts remaining. {:else} Please log in! {/login} (function() { function authorizeUser(username, password) { /* fake API - change the message and change the output! */ return { message: "InvalidPassword", loginAttemptsRemaining: 42 }; } return { "login": function(chunk, context, bodies) { var username = context.get("username"), password = context.get("password"), status = authorizeUser(username, password); switch(status.message) { case "OK": return true; case "InvalidUserName": return chunk.render(bodies.usernameError, context); case "InvalidPassword": return chunk.render(bodies.passwordError, context.push(status.loginAttemptsRemaining)); } return false; } }; })();

Params

Context helpers can be passed parameters just like regular Dust sections. They are accessed through the params argument.

{#price value=39.9 /} { "price": function(chunk, context, bodies, params) { return chunk.write("$" + Number(params.value).toFixed(2)); } }
Evaluating a parameter

If a parameter contains a Dust reference, you must evaluate the reference if you want to use it in your context helper. Reference evaluation is done using dust.helpers.tap() (provided as part of the dustjs-helpers addon).

{#say text="Hello {name}!"/} { "name": "lowercase person", "say": function(chunk, context, bodies, params) { var text = dust.helpers.tap(params.text, chunk, context); text = text.toUpperCase(); return chunk.write(text); } }

Changing helper context

You might want to invoke your helper with only a portion of your context. To do this, add a colon and the context key after your helper’s name, like {#helper:context}.

{#greet:friends/}{~n} {#greet:acquaintances/} { "friends": ["Alice", "Bob", "Charlie"], "acquaintances": ["Dusty", "Eggbert", "Fabrice"], "greet": function(chunk, context) { var people = context.current(); return chunk.write("Hello " + people.join(" and ") + "!"); } }

Asynchronous context helpers

Dust’s asynchronous nature is one of its defining features. Writing context helpers in an async way lets you make HTTP requests or call services without blocking the rendering of your template.

If you have a callback-based API, you can tell Dust to wait until the callback returns to render using chunk.map.

{#query}{data}{/query} (function() { function query(query, cb) { dust.nextTick(function() { cb(null, {data: "Dust"}); }); } return { "query": function(chunk, context, bodies, params) { return chunk.map(function(chunk) { query("SELECT name FROM USERS", function(err, data) { return chunk.render(bodies.block, context.push(data)) .end(); }); }); } }; }())

To start an asynchronous block, call chunk.map. Inside its callback function, you can perform any sync or async operations. The only difference is that when you’re done, you must call chunk.end to signal that the async operations have completed.

You can’t return a value from an asynchronous helper like you can a normal one. You must return a chunk that has been ended.

Promises (Dust 2.6.2)

Your helper can return Promises as of Dust 2.6.2. Dust will unwrap the Promise and push its data onto your context when the Promise resolves, so you don’t have to worry about manually mapping the chunk.

{#ip}Your IP address: {ip}{/ip} { "ip": function(chunk, context, bodies, params) { return $.get("//ip.jsontest.com/"); } }

Try it out

Try these exercises to help you further your understanding of context helpers!

The Bad API

Your JSON data is badly-formatted, as seen in the sample. Write a helper to reformat this data to make the template work.

{#friends} {name} - {birthday}{~s} {/friends} { "people": ["Alice", "Bob", "Charles"], "birthdays": { "Alice": "12/01/84", "Bob": "08/30/66", "Charles": "07/07/77" }, "friends": function(chunk, context, bodies, params) { } } Alice - 12/01/84 Bob - 08/30/66 Charles - 07/07/77

Solution

"friends": function(chunk, context, bodies, params) {
  var friends = [],
      people = context.get("people"),
      birthdays = context.get("birthdays");

  people.forEach(function(person) {
    chunk.render(bodies.block, context.push({
      "name": person,
      "birthday": birthdays[person]
    }));
  });
}

Temperature Converter

Our weather API reports temperatures in Fahrenheit, but we need to display in Celsius.

The formula to convert is:

(F - 32) * 5/9
{#convertToCelsius temperatureF="68" /} { "convertToCelsius": function(chunk, context, bodies, params) { } } 20

Solution

"convertToCelsius": function(chunk, context, bodies, params) {
  var f = params.temperatureF,
      c = (f - 32) * 5/9;
  return chunk.write(c);
}

Extra Credit

Turn your helper into a temperatureConverter that takes either a c or f parameter, and outputs the other one.

Race Winners

We have a list of racers and their times, but we want to show them in order so we know who won.

{#orderedRacers} {name} - {time} minutes{~s} {/orderedRacers} { "racers": [ { "name": "Mario", time: 5.8 }, { "name": "Bowser", time: 4.9 }, { "name": "Peach", time: 5.1 }, { "name": "Daisy", time: 7 }, { "name": "Toad", time: 5.2 } ], "orderedRacers": function(chunk, context, bodies, params) { } } Bowser - 4.9 minutes Peach - 5.1 minutes Toad - 5.2 minutes Mario - 5.8 minutes Daisy - 7 minutes

Solution

"orderedRacers": function(chunk, context, bodies, params) {
  var racers = context.get("racers");
  racers.sort(function(a, b) {
    return a.time - b.time;
  });
  return racers;
}

Extra Credit

Write a second helper that takes the {time} in minutes from the context and formats it into minutes and seconds, and incorporate that into your list.

"minutesSeconds": function(chunk, context, bodies, params) {
  var time = context.get("time"),
      minutes = Math.floor(time),
      seconds = Math.round((time - minutes) * 60);

  if(minutes) { chunk.write(minutes + "m"); }
  if(seconds) { chunk.write(seconds + "s"); }
  return chunk;
}

Fork me on GitHub