Skip to content

Instantly share code, notes, and snippets.

@akhoury
Last active February 17, 2024 13:25
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 24 You must be signed in to fork a gist
  • Save akhoury/9118682 to your computer and use it in GitHub Desktop.
Save akhoury/9118682 to your computer and use it in GitHub Desktop.
Handlebars random JavaScript expression execution, with an IF helper with whatever logical operands and whatever arguments, and few more goodies.
// for detailed comments and demo, see my SO answer here http://stackoverflow.com/questions/8853396/logical-operator-in-a-handlebars-js-if-conditional/21915381#21915381
/* a helper to execute an IF statement with any expression
USAGE:
-- Yes you NEED to properly escape the string literals, or just alternate single and double quotes
-- to access any global function or property you should use window.functionName() instead of just functionName()
-- this example assumes you passed this context to your handlebars template( {name: 'Sam', age: '20' } ), notice age is a string, just for so I can demo parseInt later
<p>
{{#xif " name == 'Sam' && age === '12' " }}
BOOM
{{else}}
BAMM
{{/xif}}
</p>
*/
Handlebars.registerHelper("xif", function (expression, options) {
return Handlebars.helpers["x"].apply(this, [expression, options]) ? options.fn(this) : options.inverse(this);
});
/* a helper to execute javascript expressions
USAGE:
-- Yes you NEED to properly escape the string literals or just alternate single and double quotes
-- to access any global function or property you should use window.functionName() instead of just functionName(), notice how I had to use window.parseInt() instead of parseInt()
-- this example assumes you passed this context to your handlebars template( {name: 'Sam', age: '20' } )
<p>Url: {{x " \"hi\" + name + \", \" + window.location.href + \" <---- this is your href,\" + " your Age is:" + window.parseInt(this.age, 10) "}}</p>
OUTPUT:
<p>Url: hi Sam, http://example.com <---- this is your href, your Age is: 20</p>
*/
Handlebars.registerHelper("x", function(expression, options) {
var result;
// you can change the context, or merge it with options.data, options.hash
var context = this;
// switched to Function construction instead of with+eval thanks to @Sayene and @GoldraK
try {
// create function returning expression;
var func = new Function(...Object.keys(context), 'return ' + expression);
// call function passing context values
result = func(...Object.values(context));
} catch(e) {
console.warn('•Expression: {{x \'' + expression + '\'}}\n•JS-Error: ', e, '\n•Context: ', context);
}
// No more `with`, everyone uses `strict mode` these days
// Commenting this out, I will leave it in here for those of you who live on the edge.
// // yup, i use 'with' here to expose the context's properties as block variables
// // you don't need to do {{x 'this.age + 2'}}
// // but you can also do {{x 'age + 2'}}
// // HOWEVER including an UNINITIALIZED var in a expression will return undefined as the result.
// with(context) {
// result = (function() {
// try {
// return eval(expression);
// } catch (e) {
// console.warn('•Expression: {{x \'' + expression + '\'}}\n•JS-Error: ', e, '\n•Context: ', context);
// }
// }).call(context); // to make eval's lexical this=context
// }
return result;
});
/*
if you want access upper level scope, this one is slightly different
the expression is the JOIN of all arguments
usage: say context data looks like this:
// data
{name: 'Sam', age: '20', address: { city: 'yomomaz' } }
// in template
// notice how the expression wrap all the string with quotes, and even the variables
// as they will become strings by the time they hit the helper
// play with it, you will immediately see the errored expressions and figure it out
{{#with address}}
{{z '"hi " + "' ../this.name '" + " you live with " + "' city '"' }}
{{/with}}
*/
Handlebars.registerHelper("z", function () {
var options = arguments[arguments.length - 1]
delete arguments[arguments.length - 1];
return Handlebars.helpers["x"].apply(this, [Array.prototype.slice.call(arguments, 0).join(''), options]);
});
Handlebars.registerHelper("zif", function () {
var options = arguments[arguments.length - 1]
delete arguments[arguments.length - 1];
return Handlebars.helpers["x"].apply(this, [Array.prototype.slice.call(arguments, 0).join(''), options]) ? options.fn(this) : options.inverse(this);
});
/*
More goodies since you're reading this gist.
*/
// say you have some utility object with helpful functions which you want to use inside of your handlebars templates
util = {
// a helper to safely access object properties, think ot as a lite xpath accessor
// usage:
// var greeting = util.prop( { a: { b: { c: { d: 'hi'} } } }, 'a.b.c.d');
// greeting -> 'hi'
// [IMPORTANT] THIS .prop function is REQUIRED if you want to use the handlebars helpers below,
// if you decide to move it somewhere else, update the helpers below accordingly
prop: function() {
if (typeof props == 'string') {
props = props.split('.');
}
if (!props || !props.length) {
return obj;
}
if (!obj || !Object.prototype.hasOwnProperty.call(obj, props[0])) {
return null;
} else {
var newObj = obj[props[0]];
props.shift();
return util.prop(newObj, props);
}
},
// some more helpers .. just examples, none are required
isNumber: function(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
},
daysInMonth: function (m, y) {
y = y || (new Date).getFullYear();
return /8|3|5|10/.test(m) ? 30 : m == 1 ? (!(y % 4) && y % 100) || !(y % 400) ? 29 : 28 : 31;
},
uppercaseFirstLetter: function (str) {
str || (str = '');
return str.charAt(0).toUpperCase() + str.slice(1);
},
hasNumber: function (n) {
return !isNaN(parseFloat(n));
},
truncate: function (str, len) {
if (typeof str != 'string') return str;
len = util.isNumber(len) ? len : 20;
return str.length <= len ? str : str.substr(0, len - 3) + '...';
}
};
// a helper to execute any util functions and get its return
// usage: {{u 'truncate' this.title 30}} to truncate the title
Handlebars.registerHelper('u', function() {
var key = '';
var args = Array.prototype.slice.call(arguments, 0);
if (args.length) {
key = args[0];
// delete the util[functionName] as the first element in the array
args.shift();
// delete the options arguments passed by handlebars, which is the last argument
args.pop();
}
if (util.hasOwnProperty(key)) {
// notice the reference to util here
return typeof util[key] == 'function' ?
util[key].apply(util, args) :
util[key];
} else {
log.error('util.' + key + ' is not a function nor a property');
}
});
// a helper to execute any util function as an if helper,
// that util function should have a boolean return if you want to use this properly
// usage: {{uif 'isNumber' this.age}} {{this.age}} {{else}} this.dob {{/uif}}
Handlebars.registerHelper('uif', function() {
var options = arguments[arguments.length - 1];
return Handlebars.helpers['u'].apply(this, arguments) ? options.fn(this) : options.inverse(this);
});
// a helper to execute any global function or get global.property
// say you have some globally accessible metadata i.e
// window.meta = {account: {state: 'MA', foo: function() { .. }, isBar: function() {...} } }
// usage:
// {{g 'meta.account.state'}} to print the state
// or will execute a function
// {{g 'meta.account.foo'}} to print whatever foo returns
Handlebars.registerHelper('g', function() {
var path, value;
if (arguments.length) {
path = arguments[0];
delete arguments[0];
// delete the options arguments passed by handlebars
delete arguments[arguments.length - 1];
}
// notice the util.prop is required here
value = util.prop(window, path);
if (typeof value != 'undefined' && value !== null) {
return typeof value == 'function' ?
value.apply({}, arguments) :
value;
} else {
log.warn('window.' + path + ' is not a function nor a property');
}
});
// global if
// usage:
// {{gif 'meta.account.isBar'}} // to execute isBar() and behave based on its truthy or not return
// or just check if a property is truthy or not
// {{gif 'meta.account.state'}} State is valid ! {{/gif}}
Handlebars.registerHelper('gif', function() {
var options = arguments[arguments.length - 1];
return Handlebars.helpers['g'].apply(this, arguments) ? options.fn(this) : options.inverse(this);
});
// just an {{#each}} warpper to iterate over a global array,
// usage say you have: window.meta = { data: { countries: [ {name: 'US', code: 1}, {name: 'UK', code: '44'} ... ] } }
// {{geach 'meta.data.countries'}} {{this.code}} {{/geach}}
Handlebars.registerHelper('geach', function(path, options) {
var value = util.prop(window, arguments[0]);
if (!_.isArray(value))
value = [];
return Handlebars.helpers['each'].apply(this, [value, options]);
});
@lgh06
Copy link

lgh06 commented Jul 18, 2016

Excellent work.thx.

@bgashler1
Copy link

What's the license to use this? MIT? Thanks for sharing this!

@henrymilleruk
Copy link

This is wonderful. Thank you.

@akhoury
Copy link
Author

akhoury commented Oct 18, 2017

@bgashler1 MIT it is

@chrisross
Copy link

This is great. Is there anyway to replace the eval/with statements to avoid a Parser Error when using strict mode?
I've asked the question in SO: https://stackoverflow.com/questions/50827130/extended-if-handlebars-helper-without-with-statement

@joonseokhu
Copy link

This is the most legit handlebars helper from what i've seen.

@edoves
Copy link

edoves commented Sep 4, 2019

I believe this is working according to the comments below, however, I found it difficult to run it properly on my express application
I have this code:

const express = require("express");
var exphbs = require("express-handlebars");

const app = express();
//this is not working
const hbs = exphbs.create({
  helpers: {
    xif: (expression, options) => {
      return Handlebars.helpers["x"].apply(this, [expression, options])
        ? options.fn(this)
        : options.inverse(this);
    }
  }
});

can some one point me on the right direction please

@akhoury
Copy link
Author

akhoury commented Sep 4, 2019

@edoves xif requires another helper x
Also, it is not recommend to use shorthand function syntax () => {} because the functions will get immediately bound to the context at the time they get created, and this will remain that context, and we don't want that, so i use the long syntax function () {}

const express = require("express");
var exphbs = require("express-handlebars");

const helpers = {
    xif: function (expression, options) {
      return helpers["x"].apply(this, [expression, options]) 
        ? options.fn(this)
        : options.inverse(this);
    },
    x: function (expression, options) {
        var result;
        var context = this;
        with(context) {
            result = (function() {
                try {
                    return eval(expression);
                } catch (e) {
                    console.warn('•Expression: {{x \'' + expression + '\'}}\n•JS-Error: ', e, '\n•Context: ', context);
                }
            }).call(context); // to make eval's lexical this=context
        }
        return result;
    }
};

const app = express();
//this is not working
const hbs = exphbs.create({
    helpers  
});

@edoves
Copy link

edoves commented Sep 4, 2019

Thanks, man. I appreciate it. What I really want to achieve is how I'm going to put a comparison of my if statement with handlebars
Here is my template sample:
`

` What I want is to happen is => ` {{#if pageID == "home" }} jumbotron hidden-xs{{/if}}`

@akhoury
Copy link
Author

akhoury commented Sep 4, 2019

this should work

 {{#xif ' pageID == "home" ' }} jumbotron hidden-xs{{/xif}}

@akhoury
Copy link
Author

akhoury commented Sep 4, 2019

notice that the expression is a string, using singlequotes', but inside of it you can either use doublequotes, or single quotes with escaping.

@edoves
Copy link

edoves commented Sep 12, 2019

Hi @akhoury fantastic men thank a lot but, seem like the {{ else }} part is not working

@edoves
Copy link

edoves commented Sep 12, 2019

Here is my code
{{#xif ' pageID == "home" ' }} col-sm-8 {{else}} col-sm-4 text-center {{/xif}}">

@Sayene
Copy link

Sayene commented Sep 12, 2019

@chrisross to avoid using with() and keep strict mode you may try this approach:

 try {
        let func = new Function(...Object.keys(context), 'return ' + expression); // create function returning expression;
        result = func(...Object.values(context));     // call function passing context values
       } catch(e) {
        console.warn('•Expression: {{x \'' + expression + '\'}}\n•JS-Error: ', e, '\n•Context: ', context);
       }

@akhoury
Copy link
Author

akhoury commented Sep 12, 2019

@Sayene, not a bad idea at all.

@akhoury
Copy link
Author

akhoury commented Sep 12, 2019

@edoves can you create a snippet where the {{else}} isn't working? works fine when i test it, also which Handlebars version are you using, this is a very old helper, things might have changed since

@ErrorBot1122
Copy link

there is one big problom, i cant use @root as a part of an input var...

•Expression: {{x '@root.methodsExist == longname'}}
•JS-Error:  SyntaxError: Invalid or unexpected token
    at Object.<anonymous> (/workspace/Roblox-File-Handler/dont_doc/customBlockHelpers.hbs.js:18:21)
    at Object.<anonymous> (/workspace/Roblox-File-Handler/dont_doc/customBlockHelpers.hbs.js:22:8)
    at Object.<anonymous> (/workspace/Roblox-File-Handler/dont_doc/customBlockHelpers.hbs.js:36:30)
    at Object.wrapper (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/internal/wrapHelper.js:15:19)
    at eval (eval at createFunctionContext (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/compiler/javascript-compiler.js:262:23), <anonymous>:10:130)
    at Object.prog [as fn] (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/runtime.js:268:12)
    at Object.<anonymous> (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/helpers/if.js:29:22)
    at Object.wrapper (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/internal/wrapHelper.js:15:19)
    at eval (eval at createFunctionContext (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/compiler/javascript-compiler.js:262:23), <anonymous>:21:47)
    at prog (/workspace/Roblox-File-Handler/node_modules/handlebars/dist/cjs/handlebars/runtime.js:268:12) 
•Context:  {
  id: 'Instance',
  longname: 'Instance',
  name: 'Instance',
  kind: 'class',
  scope: 'global',
  description: 'Instance is the base class for all classes in the Roblox class hierarchy. Every other class that the Roblox engine defines inherits all of the members of Instance.',
  thisvalue: undefined,
  customTags: [ { tag: 'shortdescription', value: 'The main Baseclass' } ],
  meta: {
    lineno: 9,
    filename: 'Instance.js',
    path: '/workspace/Roblox-File-Handler/Classes'
  },
  order: 48
}

there are alot mor, but i dont thnk you need to see all of it

P.S: Tell me if you need to see code and hbs files...

@akhoury
Copy link
Author

akhoury commented Nov 19, 2021

can you create a code snippet maybe on https://codepen.io to show the error live?

@GoldraK
Copy link

GoldraK commented Feb 15, 2024

Hi,
I tried the code but received this error

Uncaught (in promise) SyntaxError: Strict mode code may not include a with statement

I show with is deprecated.

do you know other system to work?

Thanks for the time.

@akhoury
Copy link
Author

akhoury commented Feb 15, 2024

yea you have to disabled strict mode to have with working.

@GoldraK
Copy link

GoldraK commented Feb 16, 2024

I rewrite this functión to delete with and can use strict mode

Handlebars.registerHelper("x", function (expression, options) {
    let result;
    
    // Create a new object to hold variables from the context
    let contextVars = {};
    let context = this;

    // Copy relevant properties from the context to the new object
    for (var key in context) {
        if (context.hasOwnProperty(key) && !(key in options)) { // Avoid overriding options
            contextVars[key] = context[key];
        }
    }

    let contextValues = Object.values(contextVars);
    // Create a function with the context variables exposed as local variables
    var func = new Function(...Object.keys(contextVars), 'return ' + expression);    
    
    try {
        result = func(...contextValues);
    } catch (e) {
        console.warn('•Expression: {{x \'' + expression + '\'}}\n•JS-Error: ', e, '\n•Context: ', context);
    }

    return result;
});

@akhoury
Copy link
Author

akhoury commented Feb 16, 2024

That works too! good idea

@akhoury
Copy link
Author

akhoury commented Feb 16, 2024

@GoldraK it seems that @Sayene did almost the same thing a few years back and I totally forgot! thank you both! Updated the gist to use new Function() instead

@sujun000
Copy link

//i use your plugin and met some issues there is:
var data ={
test:{
firstName: 'Joan',
age: '21',
email: 'joan@aaa.bbb',
lastName:'check',
},
};
//and template is:

{{#each this}}

{{x "'Hi ' + firstName"}}, {{x 'lastName'}}

{{x '"you were born in " + parseInt(age, 10)'}}
{{@key}}
  {{#xif "@key=='test'"}}
 <span>good</span>
 {{else}}

not good
{{/xif}}
{{#xif 'parseInt(age) >= 21'}} login here:

http://foo.bar?email={{x 'encodeURIComponent(email)'}}

{{else}} Please go back when you grow up. {{/xif}}


{{/each}}

//my issue is i can not access @key inside of xif block, i am really needed to use this variable, {{@key}} this worked well but not in xif block,or i missed some way to do so, can you help me out, thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment