Search code examples
javascripttemplateshandlebars.jsdecorator

Handlebars decorator in loop


For few days, I've begun to use Handlebars and I'm currently trying Decorators. I've understood how it works when it remains simple : Ex: decorate the name alone : bill gaTes -> Mr Bill GATES

I then tried to use decorator in a "each" loop, to do exactly the same as before but with a list of people instead of just one. The problem is: when I am in the decorator function, I need to know which element (from the array) I am looking at.

So I would like to either give in argument the index of the "each" loop (then, as I have access to the data, I could retrieve the current element) or the current element.

I tried to use @index (which usually works well), but I get undefined when debugging. And I can't find a way to get the current element in my decorator.

Here is the code:

index.html

<!doctype html>
<html>
    <head>  
        <title>Test Handlebars Decorators 4</title>
    </head>
    <body>
        <div id = "main"> </div>

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

            {{#each this}}
                Decorated: {{formatGenderHelper this}}
                {{* activateFormatter @index}}
                <br />
            {{/each}}

        </script>

        <script type = "text/javascript" src = "js/lib/handlebars.min-latest.js"></script>
        <script type = "text/javascript" src = "js/lib/jquery-2.2.4.min.js"></script>
        <script type = "text/javascript" src = "js/data.js"></script>
        <script type = "text/javascript" src = "js/index.js"></script>
    </body>
</html>

data.js

var data = [{
    "firstname": "Bill",
    "lastname": "Gates",
    "gender": "MALE"
}, {
    "firstname": "Hillary",
    "lastname": "Clinton",
    "gender": "FEMALE"
}];

function formatMale(author) {
    return "M. " + author.firstname + " " + author.lastname.toUpperCase();
}
function formatFemale(author) {
    return "Mme " + author.firstname + " " + author.lastname.toUpperCase();
}

Handlebars.registerDecorator('activateFormatter', function(program, props, container, context) {
    var genderHelper;
    var gender = context.args[0] || context.data.root[0].gender;

    switch(gender) {
        case "MALE":
            genderHelper = formatMale;
            break;
        case "FEMALE":
            genderHelper = formatFemale;
            break;
        default:
            console.log('Gender format not set. Please set before rendering template.');
            genderHelper = function() {};
    }

    container.helpers = {
        formatGenderHelper: genderHelper
    };
});

index.js

// Get the template
var rawTemplate = $('#rawTemplate').html();

// Compile the template
var compiledTemplate = Handlebars.compile(rawTemplate);

// Apply the template on the data
var content = compiledTemplate(data);

// Finally, re-inject the rendered HTML into the DOM
$('#main').html(content);

If you need further information, please let me know.

Thanks you for helping :)


Solution

  • There are two issues that make your example fail. One is a slight issue with your code, and the other is the way decorators seem to work, which is essentially just "not in loops" (unless you're using partials, see the last section of this answer).


    First, you haven't told the decorator what to do with the @index that you're passing to it. You could change the decorator function, but since you have the decorator inside the #each block, the this is the same one that gets passed to formatGenderHelper, meaning that we can pass the decorator this.gender which resolves to just the gender string. This means that the writer of the template knows exactly what they're passing to the decorator, and the logic isn't trying to second-guess what the template is telling it.

    index.html

    ...
    {{#each this}}
        Decorated: {{formatGenderHelper this}}
        {{* activateFormatter this.gender}}
        <br />
    {{/each}}
    ...
    

    Also, I figure you are basing your example on Ryan Lewis's sitepoint demo. One problem/limitation his code has that isn't immediately obvious (because in the simple case it isn't even an issue), is that his decorator overwrites of all of the available helpers except for the formatting one it provides. To avoid "Error(s): TypeError: Cannot read property 'call' of undefined" errors when doing things that are a little bit more complex, I recommend using this in your decorator function.

    data.js

      Handlebars.registerDecorator('activateFormatter', function(program, props, container, context) {
            // leaving out the code here for brevity
            container.helpers = Object.assign({}, container.helpers, {
                formatGenderHelper: genderHelper
            });
        });
    

    The second issue is a general one with using decorators in loops, that is simply a result of the way Handlebars works with decorators. According to the decorators API doc,

    Decorators are executed when the block program is instantiated and are passed (program, props, container, context, data, blockParams, depths).

    This means (afaict) that when the template for the block is first instantiated, meaning before any of the other helpers or programs that will need to run on the block, it executes the decorator, and the decorator makes the modifications it needs, before executing the program that created the block (in this case the #each). This means that to get the decorator to differentiate each iteration of the #each differently, it needs to run in a grandchild block of the #each, so it is executed after the #each but before the formatGenderHelper, however, if you're doing this and redefining the decorated helper with each iteration based on the iterated-over context, you're better off just registering a helper that has the gender formatting logic baked in.


    However, that's sort of a non-answer. So, to answer your question, I've found that you can make handlebars do what you're trying to do, but it's sort of a hack. Since the trick is that you need the block with the decorator to be rendered from a sub-sub block of the #each we can render it in a partial. To do this we can stick it in another file and register that as a partial, but that's not really convenient, so we have two better options. 1) we can wrap the code in an undefined partial-block and let Handlebars render it as the fallback block when that partial fails, or (the slicker option) 2) we can use the provided builtin decorator #*inline to define an inline partial.

    1. Fail-over partial block method

      index.html

      ...
      {{#each this}}
         {{#>nothing}}
             Decorated: {{formatGenderHelper this}}
             {{* activateFormatter this.gender}}
             <br />
         {{/nothing}}
      {{/each}}
      ...
      
    2. #*inline method

      index.html

      ...
      {{#*inline "formattedAuthor"}}
          Decorated: {{formatGenderHelper this}}
          <br />
      {{/inline}}
      {{#each this}}
         {{#>formattedAuthor}}
             {{* activateFormatter this.gender}}
         {{/formattedAuthor}}
      {{/each}}
      ...
      

    The key here is that we use our *activeFormatter decorator to dynamically reconfigure the partial each time it gets called. The second, inline example demonstrates this more clearly. There may certainly be other good use-cases, but this is where I see decorators really shining, i.e. allowing us to dynamically reconfigure the logic or helpers of partials right from where we call them.

    However, there is a caveat: if our partial uses a helper that is only provided in a decorator that is called from outside of that partial's definition (like we have above in example 2) the partial will not be able to find the helper if that decorator is not called in the right place. This is also why it's better to use either of the two methods above for providing the partial: we keep the definition of the partial in the same file as the decorator call, so we can know that the helper is only provided dynamically and not registered globally.