Handlebars.js: Can I use multiple helpers in a single tag?

Created on 7 Sep 2012  ·  17Comments  ·  Source: handlebars-lang/handlebars.js

like {{ helper1 helper2 text }}, you know sometimes only one helper is not enough to do the work.

Most helpful comment

this appears to have native support using something like:

{{ helper1 (helper2 text) }}

All 17 comments

I'd think there would need to be a way to nest expressions, like this: {{headerText {{getTitle "my_page"}}}}.

That is not currently supported and I have no plans to support it.

However, you could theoretically create a helper that intentionally consumes and chains other helpers:

{{chain "helper1" "helper2" text}}

If someone is interested, I've created such a helper:

Handlebars.registerHelper('chain', function () {
    var helpers = [], value;
    $.each(arguments, function (i, arg) {
        if (Handlebars.helpers[arg]) {
            helpers.push(Handlebars.helpers[arg]);
        } else {
            value = arg;
            $.each(helpers, function (j, helper) {
                value = helper(value, arguments[i + 1]);
            });
            return false;
        }
    });
    return value;
});

Works like this:

{{chain "taxAdd" "formatPrice" this.product.price}}

@Znarkus that introduces a dependency to jQuery

@jrajan I only used jQuery.each, feel free to rewrite it with whatever your preferences are.

If anyone is interested in a jQuery agnostic version:

Handlebars.registerHelper('chain', function() {
  var helpers = [];
  var args = Array.prototype.slice.call(arguments);
  var argsLength = args.length;
  var index;
  var arg;

  for (index = 0, arg = args[index];
       index < argsLength;
       arg = args[++index]) {
    if (Handlebars.helpers[arg]) {
      helpers.push(Handlebars.helpers[arg]);
    } else {
      args = args.slice(index);
      break;
    }
  }

  while (helpers.length) {
    args = [helpers.pop().apply(Handlebars.helpers, args)];
  }

  return args.shift();
});

One problem with both implementations is worth noting: if any of the arguments are intended as values to be passed to a helper, but also happen to be (coercible to) strings that match the name of an existing helper, there will be unexpected results.

Here's two implementations. They both allow you to have multiple arguments sent to each helper, unlike the previous examples.
They're also written in coffee-script and depend on underscore or lodash.

The first one allows you to do something like this:
{{{chain 'join-strings' 'link-twitter-handles' '@' twitterUsername}}}
But something like this would produce unexpected results:
{{{chain 'join-strings' 'link-twitter-handles' '@' 'join-strings' twitterUsername}}}

Handlebars.registerHelper 'chain', ->
    # Get rid of the options hash
    args = Array.prototype.slice.call arguments, 0, -1
    helpers = []
    argsForHelpers = null
    value = undefined

    _.each args, (arg, i) ->
        if Handlebars.helpers[arg]
            helpers.push Handlebars.helpers[arg]
        else if not value # Only call the helpers once
            value = arg
            unless argsForHelpers
                argsForHelpers = args[i+1..-1]
                argsForHelpers.unshift value
            _.each helpers, (helper) ->
                argsForHelpers[0] = value
                value = helper.apply null, argsForHelpers

    value

This second example has a separator that lets chain split the helpers from the arguments.
The helper will assume that every argument before the separator is a helper and all others should be passed in as arguments to the helpers.

Handlebars.registerHelper 'chain', ->
    # Get rid of the options hash
    args = Array.prototype.slice.call arguments, 0, -1
    helpers = []

    for arg,i in args
        if arg is '--'
            argsForHelpers = args.slice i + 1
            value = argsForHelpers[0]
            break
        else
            helpers.push Handlebars.helpers[arg]

    _.each helpers, (helper) ->
        argsForHelpers[0] = value
        value = helper.apply null, argsForHelpers

    value

Given this template:
{{{chain 'join-strings' 'link-twitter-handles' '@' 'join-strings' twitterUsername}}}
and this object:
{twitterUsername: 'abc'}
We could expect a compiled template like this:
<a href="https://twitter.com/join-stringsabc">@join-stringsabc</a>

I've taken @cdata 's implementation and turned it into a version for block helpers. I'm also requiring that helper names are prefixed by '!!' to avoid the problem of arguments matching helper names.

/**
 * Takes an arbitrary number of arguments, the first of which is the operation type 'AND' or 'OR'.
 * Following that will be a list of block helper names prefixed by '!!'.
 * Calls each block helper with the remaining arguments.
 *
 * @returns {string}  returns options.fn(this) or options.inverse(this) depending on result of each helper and operation type
 */
Handlebars.registerHelper('chainBlockHelpers', function() {
    var index, arg, helperResult, pass,
        helpers = [],
        args = Array.prototype.slice.call(arguments),
        argsLength = args.length,
        options = args[argsLength-1],
        operation = args.shift(),
        passVal = options.fn(this),
        failVal = options.inverse(this);

    if (operation !== 'AND' && operation !== 'OR')
        throw new Error ('chainBlockHelpers only supports "AND" or "OR" operations.')

    for (index = 0, arg = args[index]; index < argsLength; arg = args[++index]) {
        if (typeof arg == 'string' && arg.startsWith('!!') && Handlebars.helpers[arg.substr(2)]) {
            helpers.push(Handlebars.helpers[arg.substr(2)]);
        } else {
            args = args.slice(index);
            break;
        }
    }

    if (operation === 'AND') {
        pass = true;
        while (helpers.length) {
            helperResult = helpers.pop().apply(Handlebars.helpers, args);
            if (helperResult == failVal) {
                pass = false;
                break;
            }
        }
    } else {
        pass = false;
        while (helpers.length) {
            helperResult = helpers.pop().apply(Handlebars.helpers, args);
            if (helperResult == passVal) {
                pass = true;
                break;
            }
        }
    }

    return pass ? passVal : failVal;
});

this appears to have native support using something like:

{{ helper1 (helper2 text) }}

In some cases, you could make your helpers work like:

{{#helper1}}{{helper2}}content{{/helper2}}{{/helper1}}

@Znarkus Using your method, when I pass in two helpers that have safe strings applied my code breaks. The first time I pass data through it comes in as a string, but the second filter the value being passed appears to be an object? In my debugger it shows as 'n a line break and then string:'my value'; Not sure what SafeString does to the value, but passing it through it twice doesn't seem to work out to well.

    Handlebars.registerHelper('shortNumber', function (value) {
        //return new Handlebars.SafeString(iSpot.number.shortNumber(value)); // Breaks
        return iSpot.number.shortNumber(value);  // Works
    });
    Handlebars.registerHelper('asDollars', function (value) {
        //return new Handlebars.SafeString(iSpot.number.asDollars(value)); // Breaks
        return iSpot.number.asDollars(value); // Works
    });

+1 @amwmedia approach - {{pluralize (titleize (humanize schema.name))}}

+1 that looks like a nice clean syntax.

+1 @amwmedia Works perfectly thanks

@breandr / @amwmedia approach works like a charme, but just w/o Handlebars.SafeString() as @cssagogo mentioned above.

So you can do smth. like:

{{> partial text=(concat value (default extension 'PDF')) }}

thanks

I ran into this issue myself. I know I'm really late on this, but I have a solution that worked for me. Please forgive me if this was already mentioned (I didn't take the time to check). This is not the coolest solution and it is technically not a direct answer to your question but it is a solution to the problem (running multiple functions on one value).

I keep all of my helper functions in a separate file. Because they are all in the same place, I can pass the value to one helper and then just call whatever other functions I have in my helper function's file. So:

{{ function1 value }}

helper file

function2 (value) {
   // code
}

function1 (value) {
   // code
   function2(value)
   // code
   return value;
}

Of course, this is a simple way of being able to use as many functions as you want. You could even designate a base function that acts as your "chainer".

{{ chainer value }}

helper file

function1 (value) {
   // code
}
function2 (value) {
   // code
}
function3 (value) {
   // code
}
function4 (value) {
   // code
}

function chainer(value) {
   function1(value)
   function2(value)
   function3(value)
   function4(value)
   return value;
}

I haven't done this myself but I don't see why it wouldn't work.

Note You only have to register the functions that you want to use in your html as handlebars helpers. I personally have all of mine registered because I use them independently of each other, but you don't have to if you don't foresee yourself needing to use it directly.

A throwback to procedural programming :)

Was this page helpful?
0 / 5 - 0 ratings