Search code examples
node.jstypescripthandlebars.js

Using custom helpers within an else statement in handlebars leads to undefined values


I'm currently trying to implement a few handlebars custom helpers in TypeScript. Here's my code for the eq (equality) operator :

const handleOptions = (
  handlebarsInstance: typeof handlebars,
  result: boolean | string,
  options: HandlebarsBlockOptions<typeof handlebars>,
): boolean | string => {
  if (options.fn && options.inverse) {
    // The helper is being used as a block helper
    return result
      ? options.fn(handlebarsInstance)
      : options.inverse(handlebarsInstance)
  }

  // The helper is being used in a sub-expression
  return result
}


function equality(
  this: typeof handlebars,
  arg1: unknown,
  arg2: unknown,
  options: HandlebarsBlockOptions<typeof handlebars>,
): boolean | string {
  logger.info('"equality" helpers triggered', {
    arg1,
    arg2,
    options,
  })

  return handleOptions(this, arg1 == arg2, options)
}

export const registerEqualityHelper = (
  handlebarsInstance: typeof handlebars,
) => {
  handlebarsInstance.registerHelper('eq', equality.bind(handlebarsInstance))
}
const handlebarInstance = handlebars.create()

registerEqualityHelper(handlebarInstance)

It seems to be working pretty well, except when trying to use eq and else in the same statement. Here's an example:

{{#eq var1 "foo"}}Text 1{{else eq var1 "bar"}}Text 2{{else}}Text 3{{/eq}}

Assuming var1 is equal to bar, the result should be Text 2, however it's Text 3. I did investigate, and this is caused by the arg1 attribute in the equality function, which is undefined, so the comparaison is undefined == "bar", which is obviously false. So far, this only happens when the helper is called from the else statement, the initial eq statement is valid.

Any idea how to make that template work as expected?

Here is more information, with what works and what doesn't...

My first assumption was that handlebars could not find the variable value because it is within an block, so the context is different. I attempt to address this with those templates, unfortunately they still have the same problem:

{{#eq var1 "foo"}}Text 1{{else eq ../var1 "bar"}}Text 2{{else}}Text 3{{/eq}}
{{#eq var1 "foo"}}Text 1{{else eq @root.var1 "bar"}}Text 2{{else}}Text 3{{/eq}}

EDIT: Wow, it's actually working! I have no idea why I didn't see that at first, maybe I put a typo somewhere at my first attempt. My apologies. I still find this not ideal though. As you can see below, if does not require that workaround, so there is still a disparity.

I've also tried to put eq between parenthesis, but this is causing an handlebars parsing issue.

A working solution would be to use if statements instead:

{{#if (eq var1 "foo")}}Text 1{{else if (eq var1 "bar")}}Text 2{{else}}Text 3{{/if}}

However, I'm not satisfied with this solution, as this would require some users to rewrite their templates. I'm also unsure why if works but not my eq implementation. I've tried mixing the two, but had the same issue:

{{#eq var1 "foo"}}Text 1{{else if (eq var1 "bar")}}Text 2{{else}}Text 3{{/eq}}

I did some research, but the only thing close to my problem I've found is this post: Handlebars custom if helper else is undefined , and unfortunately the problem is different.

Some close sourced handlebars implementation seems to include what I'm trying to achieve, such as this one: https://docs.airship.com/guides/messaging/user-guide/personalization/handlebars/logic-helpers/#ifelse so I suppose I'm not trying to do something impossible in handlebars.


Solution

  • I find that the way you are registering your helper is very complicated and difficult to follow.

    However, I believe your issue is due to what context the helper is called with. For Handlebars Helpers, the this is very important. By calling equality.bind(handlebarsInstance), you are preventing the helper from being called with the correct context this. I would suggest simply removing the .bind call, as in:

    handlebarsInstance.registerHelper('eq', equality).

    Your helper should then work when called as:

    {{#eq var1 "foo"}}Text 1{{else eq var1 "bar"}}Text 2{{else}}Text 3{{/eq}}