Is there a way in handlebars JS to incorporate logical operators into the standard handlebars.js conditional operator? Something like this:
{{#if section1 || section2}}
.. content
{{/if}}
I know I could write my own helper, but first I'd like to make sure I'm not reinventing the wheel.
This is possible by 'cheating' with a block helper. This probably goes against the Ideology of the people who developed Handlebars.
Handlebars.registerHelper('ifCond', function(v1, v2, options) {
if(v1 === v2) {
return options.fn(this);
}
return options.inverse(this);
});
You can then call the helper in the template like this
{{#ifCond v1 v2}}
{{v1}} is equal to {{v2}}
{{else}}
{{v1}} is not equal to {{v2}}
{{/ifCond}}
Taking the solution one step further. This adds the compare operator.
Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
switch (operator) {
case '==':
return (v1 == v2) ? options.fn(this) : options.inverse(this);
case '===':
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case '!=':
return (v1 != v2) ? options.fn(this) : options.inverse(this);
case '!==':
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
case '<':
return (v1 < v2) ? options.fn(this) : options.inverse(this);
case '<=':
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
case '>':
return (v1 > v2) ? options.fn(this) : options.inverse(this);
case '>=':
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
case '&&':
return (v1 && v2) ? options.fn(this) : options.inverse(this);
case '||':
return (v1 || v2) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
});
Use it in a template like this:
{{#ifCond var1 '==' var2}}
Coffee Script version
Handlebars.registerHelper 'ifCond', (v1, operator, v2, options) ->
switch operator
when '==', '===', 'is'
return if v1 is v2 then options.fn this else options.inverse this
when '!=', '!=='
return if v1 != v2 then options.fn this else options.inverse this
when '<'
return if v1 < v2 then options.fn this else options.inverse this
when '<='
return if v1 <= v2 then options.fn this else options.inverse this
when '>'
return if v1 > v2 then options.fn this else options.inverse this
when '>='
return if v1 >= v2 then options.fn this else options.inverse this
when '&&', 'and'
return if v1 and v2 then options.fn this else options.inverse this
when '||', 'or'
return if v1 or v2 then options.fn this else options.inverse this
else
return options.inverse this
Handlebars supports nested operations. This provides a lot of flexibility (and cleaner code) if we write our logic a little differently.
{{#if (or section1 section2)}}
.. content
{{/if}}
In fact, we can add all sorts of logic:
{{#if (or
(eq section1 "foo")
(ne section2 "bar"))}}
.. content
{{/if}}
Just register these helpers:
Handlebars.registerHelper({
eq: (v1, v2) => v1 === v2,
ne: (v1, v2) => v1 !== v2,
lt: (v1, v2) => v1 < v2,
gt: (v1, v2) => v1 > v2,
lte: (v1, v2) => v1 <= v2,
gte: (v1, v2) => v1 >= v2,
and() {
return Array.prototype.every.call(arguments, Boolean);
},
or() {
return Array.prototype.slice.call(arguments, 0, -1).some(Boolean);
}
});
taking this one up a notch, for those of you who live on the edge.
gist: https://gist.github.com/akhoury/9118682
Demo: Code snippet below
Handlebars Helper: {{#xif EXPRESSION}} {{else}} {{/xif}}
a helper to execute an IF statement with any expression
EXPRESSION is a properly escaped String
Yes you NEED to properly escape the string literals or just alternate single and double quotes
you can access any global function or property i.e. encodeURIComponent(property)
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 in this post
Usage:
<p>
{{#xif " name == 'Sam' && age === '12' " }}
BOOM
{{else}}
BAMM
{{/xif}}
</p>
Output
<p>
BOOM
</p>
JavaScript: (it depends on another helper- keep reading)
Handlebars.registerHelper("xif", function (expression, options) {
return Handlebars.helpers["x"].apply(this, [expression, options]) ? options.fn(this) : options.inverse(this);
});
Handlebars Helper: {{x EXPRESSION}}
A helper to execute javascript expressions
EXPRESSION is a properly escaped String
Yes you NEED to properly escape the string literals or just alternate single and double quotes
you can access any global function or property i.e. parseInt(property)
this example assumes you passed this context to your handlebars template( {name: 'Sam', age: '20' } ), age is a string for demo purpose, it can be anything..
Usage:
<p>Url: {{x "'hi' + name + ', ' + window.location.href + ' <---- this is your href,' + ' your Age is:' + parseInt(this.age, 10)"}}</p>
Output:
<p>Url: hi Sam, http://example.com <---- this is your href, your Age is: 20</p>
JavaScript:
This looks a little large because I expanded syntax and commented over almost each line for clarity purposes
Handlebars.registerHelper("x", function(expression, options) {
var result;
// you can change the context, or merge it with options.data, options.hash
var context = this;
// 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;
});
Handlebars.registerHelper("xif", function(expression, options) {
return Handlebars.helpers["x"].apply(this, [expression, options]) ? options.fn(this) : options.inverse(this);
});
var data = [{
firstName: 'Joan',
age: '21',
email: 'joan#aaa.bbb'
}, {
firstName: 'Sam',
age: '18',
email: 'sam#aaa.bbb'
}, {
firstName: 'Perter',
lastName: 'Smith',
age: '25',
email: 'joseph#aaa.bbb'
}];
var source = $("#template").html();
var template = Handlebars.compile(source);
$("#main").html(template(data));
h1 {
font-size: large;
}
.content {
padding: 10px;
}
.person {
padding: 5px;
margin: 5px;
border: 1px solid grey;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0/handlebars.min.js"></script>
<script id="template" type="text/x-handlebars-template">
<div class="content">
{{#each this}}
<div class="person">
<h1>{{x "'Hi ' + firstName"}}, {{x 'lastName'}}</h1>
<div>{{x '"you were born in " + ((new Date()).getFullYear() - parseInt(this.age, 10)) '}}</div>
{{#xif 'parseInt(age) >= 21'}} login here:
<a href="http://foo.bar?email={{x 'encodeURIComponent(email)'}}">
http://foo.bar?email={{x 'encodeURIComponent(email)'}}
</a>
{{else}} Please go back when you grow up. {{/xif}}
</div>
{{/each}}
</div>
</script>
<div id="main"></div>
Moar
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}}
Javascript:
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);
});
There is a simple way of doing this without writing a helper function... It can be done within the template completely.
{{#if cond1}}
{{#if con2}}
<div> and condition completed</div>
{{/if}}
{{else}}
<div> both conditions weren't true</div>
{{/if}}
Edit: Conversely you can do or's by doing this:
{{#if cond1}}
<div> or condition completed</div>
{{else}}
{{#if cond2}}
<div> or condition completed</div>
{{else}}
<div> neither of the conditions were true</div>
{{/if}}
{{/if}}
Edit/Note: From the handlebar's website: handlebarsjs.com here are the falsy values:
You can use the if helper to conditionally render a block. If its
argument returns false, undefined, null, "" or [] (a "falsy" value),
Then any 'cond' (like cond1 or cond2) will not be counted as true.
One problem with all of the answers posted here is that they don't work with bound properties, i.e. the if condition is not re-evaluated when the properties involved change. Here's a slightly more advanced version of the helper supporting bindings. It uses the bind function from the Ember source, which is also used to implement the normal Ember #if helper.
This one is limited to a single bound property on the left-hand side, comparing to a constant on the right-hand side, which I think is good enough for most practical purposes. If you need something more advanced than a simple comparison, then perhaps it would be good to start declaring some computed properties and using the normal #if helper instead.
Ember.Handlebars.registerHelper('ifeq', function(a, b, options) {
return Ember.Handlebars.bind.call(options.contexts[0], a, options, true, function(result) {
return result === b;
});
});
You can use it like this:
{{#ifeq obj.some.property "something"}}
They are equal!
{{/ifeq}}
Improved solution that basically work with any binary operator (at least numbers, strings doesn't work well with eval, TAKE CARE OF POSSIBLE SCRIPT INJECTION IF USING A NON DEFINED OPERATOR WITH USER INPUTS):
Handlebars.registerHelper("ifCond",function(v1,operator,v2,options) {
switch (operator)
{
case "==":
return (v1==v2)?options.fn(this):options.inverse(this);
case "!=":
return (v1!=v2)?options.fn(this):options.inverse(this);
case "===":
return (v1===v2)?options.fn(this):options.inverse(this);
case "!==":
return (v1!==v2)?options.fn(this):options.inverse(this);
case "&&":
return (v1&&v2)?options.fn(this):options.inverse(this);
case "||":
return (v1||v2)?options.fn(this):options.inverse(this);
case "<":
return (v1<v2)?options.fn(this):options.inverse(this);
case "<=":
return (v1<=v2)?options.fn(this):options.inverse(this);
case ">":
return (v1>v2)?options.fn(this):options.inverse(this);
case ">=":
return (v1>=v2)?options.fn(this):options.inverse(this);
default:
return eval(""+v1+operator+v2)?options.fn(this):options.inverse(this);
}
});
Here's a solution if you want to check multiple conditions:
/* Handler to check multiple conditions
*/
Handlebars.registerHelper('checkIf', function (v1,o1,v2,mainOperator,v3,o2,v4,options) {
var operators = {
'==': function(a, b){ return a==b},
'===': function(a, b){ return a===b},
'!=': function(a, b){ return a!=b},
'!==': function(a, b){ return a!==b},
'<': function(a, b){ return a<b},
'<=': function(a, b){ return a<=b},
'>': function(a, b){ return a>b},
'>=': function(a, b){ return a>=b},
'&&': function(a, b){ return a&&b},
'||': function(a, b){ return a||b},
}
var a1 = operators[o1](v1,v2);
var a2 = operators[o2](v3,v4);
var isTrue = operators[mainOperator](a1, a2);
return isTrue ? options.fn(this) : options.inverse(this);
});
Usage:
/* if(list.length>0 && public){}*/
{{#checkIf list.length '>' 0 '&&' public '==' true}} <p>condition satisfied</p>{{/checkIf}}
Here's a link to the block helper I use: comparison block helper. It supports all the standard operators and lets you write code as shown below. It's really quite handy.
{{#compare Database.Tables.Count ">" 5}}
There are more than 5 tables
{{/compare}}
Install Ember Truth Helpers addon by running the below command
ember install ember-truth-helpers
you can start use most of the logical operators(eq,not-eq,not,and,or,gt,gte,lt,lte,xor).
{{#if (or section1 section2)}}
...content
{{/if}}
You can even include subexpression to go further,
{{#if (or (eq section1 "section1") (eq section2 "section2") ) }}
...content
{{/if}}
Yet another crooked solution for a ternary helper:
'?:' ( condition, first, second ) {
return condition ? first : second;
}
<span>{{?: fooExists 'found it' 'nope, sorry'}}</span>
Or a simple coalesce helper:
'??' ( first, second ) {
return first ? first : second;
}
<span>{{?? foo bar}}</span>
Since these characters don't have a special meaning in handlebars markup, you're free to use them for helper names.
Similar to Jim's answer but a using a bit of creativity we could also do something like this:
Handlebars.registerHelper( "compare", function( v1, op, v2, options ) {
var c = {
"eq": function( v1, v2 ) {
return v1 == v2;
},
"neq": function( v1, v2 ) {
return v1 != v2;
},
...
}
if( Object.prototype.hasOwnProperty.call( c, op ) ) {
return c[ op ].call( this, v1, v2 ) ? options.fn( this ) : options.inverse( this );
}
return options.inverse( this );
} );
Then to use it we get something like:
{{#compare numberone "eq" numbertwo}}
do something
{{else}}
do something else
{{/compare}}
I would suggest moving the object out of the function for better performance but otherwise you can add any compare function you want, including "and" and "or".
One other alternative is to use function name in #if. The #if will detect if the parameter is function and if it is then it will call it and use its return for truthyness check. Below myFunction gets current context as this.
{{#if myFunction}}
I'm Happy!
{{/if}}
Unfortunately none of these solutions solve the problem of "OR" operator "cond1 || cond2".
Check if first value is true
Use "^" (or) and check if otherwise cond2 is true
{{#if cond1}}
DO THE ACTION
{{^}}
{{#if cond2}}
DO THE ACTION
{{/if}}
{{/if}}
It breaks DRY rule. So why not use partial to make it less messy
{{#if cond1}}
{{> subTemplate}}
{{^}}
{{#if cond2}}
{{> subTemplate}}
{{/if}}
{{/if}}
I can understand why you would want to create a helper for situations where you have a large number of varied comparisons to perform within your template, but for a relatively small number of comparisons (or even one, which was what brought me to this page in the first place), it would probably just be easier to define a new handlebars variable in your view-rendering function call, like:
Pass to handlebars on render:
var context= {
'section1' : section1,
'section2' : section2,
'section1or2' : (section1)||(section2)
};
and then within your handlebars template:
{{#if section1or2}}
.. content
{{/if}}
I mention this for simplicity's sake, and also because it's an answer that may be quick and helpful while still complying with the logicless nature of Handlebars.
I have found a npm package made with CoffeeScript that has a lot of incredible useful helpers for Handlebars. Take a look of the documentation in the following URL:
https://npmjs.org/package/handlebars-helpers
You can do a wget http://registry.npmjs.org/handlebars-helpers/-/handlebars-helpers-0.2.6.tgz to download them and see the contents of the package.
You will be abled to do things like {{#is number 5}} or {{formatDate date "%m/%d/%Y"}}
Correct Solution for AND/OR
Handlebars.registerHelper('and', function () {
// Get function args and remove last one (function name)
return Array.prototype.slice.call(arguments, 0, arguments.length - 1).every(Boolean);
});
Handlebars.registerHelper('or', function () {
// Get function args and remove last one (function name)
return Array.prototype.slice.call(arguments, 0, arguments.length - 1).some(Boolean);
});
Then call as follows
{{#if (or (eq questionType 'STARTTIME') (eq questionType 'ENDTIME') (..) ) }}
BTW: Note that the solution given here is incorrect, he's not subtracting the last argument which is the function name.
https://stackoverflow.com/a/31632215/1005607
His original AND/OR was based on the full list of arguments
and: function () {
return Array.prototype.slice.call(arguments).every(Boolean);
},
or: function () {
return Array.prototype.slice.call(arguments).some(Boolean);
}
Can someone change that answer? I just wasted an hour trying to fix something in an answer recommended by 86 people. The fix is to filter out the last argument which is the function name. Array.prototype.slice.call(arguments, 0, arguments.length - 1)
if you just want to check if one or the other element are present you can use this custom helper
Handlebars.registerHelper('if_or', function(elem1, elem2, options) {
if (Handlebars.Utils.isEmpty(elem1) && Handlebars.Utils.isEmpty(elem2)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
like this
{{#if_or elem1 elem2}}
{{elem1}} or {{elem2}} are present
{{else}}
not present
{{/if_or}}
if you also need to be able to have an "or" to compare
function return values
I would rather add another property that returns the desired result.
The templates should be logicless after all!
For those having problems comparing object properties, inside the helper add this solution
Ember.js helper not properly recognizing a parameter
Just came to this post from a google search on how to check if a string equals another string.
I use HandlebarsJS in NodeJS server-side, but I also use the same template files on the front-end using the browser version of HandlebarsJS to parse it. This meant that if I wanted a custom helper, I'd have to define it in 2 separate places, or assign a function to the object in question - too much effort!!
What people forget is that certain objects have inherit functions that can be used in the moustache template. In the case of a string:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
An Array containing the entire match result and any parentheses-captured matched results; null if there were no matches.
We can use this method to return either an array of matches, or null if no matches were found. This is perfect, because looking at the HandlebarsJS documentation http://handlebarsjs.com/builtin_helpers.html
You can use the if helper to conditionally render a block. If its argument returns false, undefined, null, "", 0, or [], Handlebars will not render the block.
So...
{{#if your_string.match "what_youre_looking_for"}}
String found :)
{{else}}
No match found :(
{{/if}}
UPDATE:
After testing on all browsers, this doesn't work on Firefox. HandlebarsJS passes other arguments to a function call, meaning that when String.prototype.match is called, the second argument (i.e. the Regexp flags for the match function call as per above documentation) appears to be being passed. Firefox sees this as a deprecated use of String.prototype.match, and so breaks.
A workaround is to declare a new functional prototype for the String JS object, and use that instead:
if(typeof String.includes !== 'function') {
String.prototype.includes = function(str) {
if(!(str instanceof RegExp))
str = new RegExp((str+'').escapeRegExp(),'g');
return str.test(this);
}
}
Ensure this JS code is included before you run your Handlebars.compile() function, then in your template...
{{#your_string}}
{{#if (includes "what_youre_looking_for")}}
String found :)
{{else}}
No match found :(
{{/if}}
{{/your_string}}
You can do it simply by using the logical operator like this shown below:
{{#if (or(eq firstValue 'String_to_compare_value') (eq secondValue 'String_to_compare_value'))}}business logic goes here{{/if}}
{{#if (and(eq firstValue 'String_to_compare_value') (eq secondValue 'String_to_compare_value'))}}business logic goes here{{/if}}
Before closing if you can write your business logic
Here we have vanilla handlebars for multiple logical && and || (and or):
Handlebars.registerHelper("and",function() {
var args = Array.prototype.slice.call(arguments);
var options = args[args.length-1];
for(var i=0; i<args.length-1; i++){
if( !args[i] ){
return options.inverse(this);
}
}
return options.fn(this);
});
Handlebars.registerHelper("or",function() {
var args = Array.prototype.slice.call(arguments);
var options = args[args.length-1];
for(var i=0; i<args.length-1; i++){
if( args[i] ){
return options.fn(this);
}
}
return options.inverse(this);
}
// Results
// {{#and foo bar sally bob}} yup {{else}} nope {{/and}} // yup
// {{#or foo bar "" sally bob}} yup {{else}} nope {{/or}} // yup
// {{#and foo bar "" sally bob}} yup {{else}} nope {{/and}} // nope
// {{#or "" "" "" "" ""}} yup {{else}} nope {{/or}} // nope
Not so sure if it's "safe" to use "and" and "or"... maybe change to something like "op_and" and "op_or"?
Following these 2 guides a-way-to-let-users-define-custom-made-bound-if-statements and custom bound helpers I was able to adjust my shared views in this post on stackoverflow to use this instead of the standard #if statement. This should be more secure than just tossing an #if in there.
The custom bound helpers in that gist are outstanding.
<li>
<a href="{{unbound view.varProductSocialBlog}}">
{{#if-equal view.showDiv "true"}}<div>{{/if-equal}}<i class="fa fa-rss-square"></i>{{#if-equal view.showDiv "true"}}</div>{{/if-equal}}
{{#if-equal view.showTitle "true"}}Blog{{/if-equal}}
</a>
</li>
I am using the ember cli project to build my ember application.
Current setup at the time of this post:
DEBUG: -------------------------------
DEBUG: Ember : 1.5.1
DEBUG: Ember Data : 1.0.0-beta.7+canary.b45e23ba
DEBUG: Handlebars : 1.3.0
DEBUG: jQuery : 2.1.1
DEBUG: -------------------------------
In Ember.js you can use inline if helper in if block helper. It can replace || logical operator, for example:
{{#if (if firstCondition firstCondition secondCondition)}}
(firstCondition || (or) secondCondition) === true
{{/if}}
You can use the following code:
{{#if selection1}}
doSomething1
{{else}}
{{#if selection2}}
doSomething2
{{/if}}
{{/if}}
you cannot write your expressions inside handlebars template but all of your logic (expressions) in express.js
app.js
res.render("view.hbs", {expression: section1 || section2})
view.hbs
{{#if expression}}
<h1> My Expression Returned True </h1>
{{ else }}
<h2>My Expression Returned False</h2>
{{/if}} <!-- End IF -->
Here's an approach I'm using for ember 1.10 and ember-cli 2.0.
// app/helpers/js-x.js
export default Ember.HTMLBars.makeBoundHelper(function (params) {
var paramNames = params.slice(1).map(function(val, idx) { return "p" + idx; });
var func = Function.apply(this, paramNames.concat("return " + params[0] + ";"))
return func.apply(params[1] === undefined ? this : params[1], params.slice(1));
});
Then you can use it in your templates like this:
// used as sub-expression
{{#each item in model}}
{{#if (js-x "this.section1 || this.section2" item)}}
{{/if}}
{{/each}}
// used normally
{{js-x "p0 || p1" model.name model.offer.name}}
Where the arguments to the expression are passed in as p0,p1,p2 etc and p0 can also be referenced as this.
Related
I have a helper function (ifCond) in ember.js as below
export default () => {
Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
alert("hi");
if (params[3]) { //handle case insensitive conditions if 4 param is passed.
params[0] = params[0].toLowerCase();
params[2] = params[2].toLowerCase();
}
let v1 = params[0];
let operator = params[1];
let v2 = params[2];
switch (operator) {
case '==':
return (v1 == v2);
case '!=':
return (v1 != v2);
case '===':
return (v1 === v2);
case '<':
return (v1 < v2);
case '<=':
return (v1 <= v2);
case '>':
return (v1 > v2);
case '>=':
return (v1 >= v2);
case '&&':
return !!(v1 && v2);
case '||':
return !!(v1 || v2);
default:
return false;
}
});
}
when I am trying to access this function in my hbs file as below
{{#if (ifCond novv.ViolationId '==' noviv.ViolationId true)}}
{{log 'someVariable'}}
<br />
{{/if}}
I am getting error as below:
Any help please - thanks in advance
From your example it looks like you may be working with a very old version of Ember. The first place to start is with the Ember Guide on writing helpers. There is a version selector in the top right corner of each guide that will take you back to previous documentation. A guess form your example is that you're working with something around 1.11.
I would highly recommend that you update to a newer version of ember and use ember-cli as that will make the import and discovery process for helpers much smoother along with gaining access to newer features and security updates.
Did it my friend, its done.
Here it is, the previous developer put all the helper functions in one file, the confusion he created was, he put other files with those helper functions in different directories - it seems I need to clean that up.
And your suggestion to check the define helped me to look into the webpack.config.js (async helpers, async __parts), there he is combining all the files scripts and generating as one js file which is written on to app.js. The convention he (or Ember I am not sure) followed was camel casing. For example for one helper-function he written as below withing the same ifCond.js file, is converted as "replace", we can use it as replace in handlebar
IMS.ReplaceHelper = Ember.Helper.extend({
compute(args) {
return args[0].replace(new RegExp(args[1], 'ig'), args[2]);
}
})
If suppose, if there are two Words - then it follows the camel casing (you know it). Totally completed and I have written some helper functions of my own as needed - thanks a lot for everybody who jumped-in to help me - thanks a lot and I don't have words how to describe how happy I am - thank you. My own helper functions as below:
IMS.IsLastHelper = Ember.Helper.extend({
compute(args) {
var list = args[0];
var item = args[1];
if (Array.isArray(list)) {
var id = list.indexOf(item);
return id == list.length - 1;
}
return false;
}
})
IMS.IsFirstHelper = Ember.Helper.extend({
compute(args) {
var list = args[0];
var item = args[1];
if (Array.isArray(list)) {
var id = list.indexOf(item);
return id == 0;
}
return false;
}
})
And I called them with syntax as below:
{{#each model.novs as |nov index|}}
{{#if (isFirst model.novs nov)}}
({{nov.NOVNumber}}:
{{else}}
{{nov.NOVNumber}}:
{{/if}}
{{#each nov.Violations as |novv index|}}
{{#unless (isLast nov.Violations novv)}}
{{novv.ViolationNumber}},
{{else}}
{{#if (isLast model.novs nov)}}
{{novv.ViolationNumber}}
{{else}}
{{novv.ViolationNumber}};
{{/if}}
{{/unless}}
{{/each}}
{{#if (isLast model.novs nov)}}
)
{{/if}}
{{/each}}
Thanks again to everybody in this group. :pray:
I've been searching online and it seems that I cannot make sort work on my observable array. I am definitely wrong somewhere but not sure where, here is the code:
var availableProducts = [{"Id":"1","Description":"Product 1","Rate":2956.00},{"Id":"3","Description":"Product 2","Rate":1518.00},{"Id":"2","Description":"Product 3","Rate":750.00}];
function productViewModel() {
var self = this;
self.products = ko.observableArray();
}
var productDetails = new productViewModel();
$(document).ready(function () {
productDetails.products = (availableProducts);
ko.applyBindings(productDetails, document.getElementById("product-edit"));
}
And HTML looks like this:
<tbody data-bind="foreach: sortedProducts">
<tr>
<td><span data-bind="text: Description"></span></td>
<td><span data-bind="currency: Rate"></span></td>
</tr>
</tbody>
So as it can be seen IDs are in order 1, 3, 2 and I would like to sort them and show them in order, but I cannot seem to sort products. I tried putting following in my ViewModel
self.sortedProducts = function () {
return self.products().sort(function (a,b) {
return a.Id < b.Id ? -1 : a.Id > b.Id ? 1 : 0;
});
This did not work, I tried then adding same code to "$(document).ready(...)" with exception of replacing self with productDetails but did not work. I do not want to make button to be called to sort my data, I want them sorted before presenting them.
The sort function looks OK, although you should check - preferrably with unit tests - whether your supported browsers actually understand the compareFunction handed to Array.prototype.sort. It's just an issue I ran into a while ago.
1) You are overwriting the ko.observableArray() with the native array availableProducts- this should give you an error when accessing the property with the getter syntax self.products().
2) The foreach binding in your template shouldn't work because you're handing it a plain function rather than an observable array, I'd guess that it actually iterates over the properties of the function object itself (.length, .prototype, .hasOwnProperty etc.).
3) I'd recommend using the Knockout Projections library that adds efficient observable handling to arrays in case you plan on handling larger arrays. In my experience with over more than say 1000 items in a collection the UI is not as fluid as you'd like it to be without the projections feature.
My take on this would be:
var availableProducts = [{"Id":"1","Description":"Product 1","Rate":2956.00},{"Id":"3","Description":"Product 2","Rate":1518.00},{"Id":"2","Description":"Product 3","Rate":750.00}];
function productViewModel() {
var self = this;
// In case you know the products at this stage you could just
// specify it as the first argument
self.products = ko.observableArray(/* [{...}] */);
// The sorted products are computed from the original ones
self.sortedProducts = ko.computed(function () {
// I'd probably use explicit parens for better
// readability - I had to look twice to get the comparer :-)
return self.products().sort(function (a, b) {
return a.Id < b.Id ? -1 : (a.Id > b.Id ? 1 : 0);
});
//// With ko-projections this would become:
//// (note the missing parens on `products`)
//return self.products.sort(function (a, b) {
// return a.Id < b.Id ? -1 : a.Id > b.Id ? 1 : 0;
//});
});
}
var productDetails = new productViewModel();
$(document).ready(function () {
// Set the observable array value
productDetails.products(availableProducts);
ko.applyBindings(productDetails, document.getElementById("product-edit"));
});
How can i get a reversed array in angular?
i'm trying to use orderBy filter, but it needs a predicate(e.g. 'name') to sort:
<tr ng-repeat="friend in friends | orderBy:'name':true">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
<tr>
Is there a way to reverse original array, without sorting.
like that:
<tr ng-repeat="friend in friends | orderBy:'':true">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
<tr>
I would suggest using a custom filter such as this:
app.filter('reverse', function() {
return function(items) {
return items.slice().reverse();
};
});
Which can then be used like:
<div ng-repeat="friend in friends | reverse">{{friend.name}}</div>
See it working here: Plunker Demonstration
This filter can be customized to fit your needs as seen fit. I have provided other examples in the demonstration. Some options include checking that the variable is an array before performing the reverse, or making it more lenient to allow the reversal of more things such as strings.
This is what i used:
<alert ng-repeat="alert in alerts.slice().reverse()" type="alert.type" close="alerts.splice(index, 1)">{{$index + 1}}: {{alert.msg}}</alert>
Update:
My answer was OK for old version of Angular.
Now, you should be using
ng-repeat="friend in friends | orderBy:'-'"
or
ng-repeat="friend in friends | orderBy:'+':true"
from https://stackoverflow.com/a/26635708/1782470
Sorry for bringing this up after a year, but there is an new, easier solution, which works for Angular v1.3.0-rc.5 and later.
It is mentioned in the docs:
"If no property is provided, (e.g. '+') then the array element itself is used to compare where sorting". So, the solution will be:
ng-repeat="friend in friends | orderBy:'-'" or
ng-repeat="friend in friends | orderBy:'+':true"
This solution seems to be better because it does not modify an array and does not require additional computational resources (at least in our code). I've read all existing answers and still prefer this one to them.
Simple solution:- (no need to make any methods)
ng-repeat = "friend in friends | orderBy: reverse:true"
You can reverse by the $index parameter
<tr ng-repeat="friend in friends | orderBy:'$index':true">
You can just call a method on your scope to reverse it for you, like this:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
<script>
angular.module('myApp', []).controller('Ctrl', function($scope) {
$scope.items = [1, 2, 3, 4];
$scope.reverse = function(array) {
var copy = [].concat(array);
return copy.reverse();
}
});
</script>
</head>
<body ng-controller="Ctrl">
<ul>
<li ng-repeat="item in items">{{item}}</li>
</ul>
<ul>
<li ng-repeat="item in reverse(items)">{{item}}</li>
</ul>
</body>
</html>
Note that the $scope.reverse creates a copy of the array since Array.prototype.reverse modifies the original array.
if you are using 1.3.x, you can use the following
{{ orderBy_expression | orderBy : expression : reverse}}
Example List books by published date in descending order
<div ng-repeat="book in books|orderBy:'publishedDate':true">
source:https://docs.angularjs.org/api/ng/filter/orderBy
If you are using angularjs version 1.4.4 and above,an easy way to sort is using the "$index".
<ul>
<li ng-repeat="friend in friends|orderBy:$index:true">{{friend.name}}</li>
</ul>
view demo
When using MVC in .NET with Angular you can always use OrderByDecending() when doing your db query like this:
var reversedList = dbContext.GetAll().OrderByDecending(x => x.Id).ToList();
Then on the Angular side, it will already be reversed in some browsers (IE). When supporting Chrome and FF, you would then need to add orderBy:
<tr ng-repeat="item in items | orderBy:'-Id'">
In this example, you'd be sorting in descending order on the .Id property. If you're using paging, this gets more complicated because only the first page would be sorted. You'd need to handle this via a .js filter file for your controller, or in some other way.
You can also use .reverse(). It's a native array function
<div ng-repeat="friend in friends.reverse()">{{friend.name}}</div>
That's because you are using JSON Object. When you face such problems then change your JSON Object to JSON Array Object.
For Example,
{"India":"IN","America":"US","United Kingdon":"UK"} json object
[{"country":"India","countryId":"IN"},{"country":"America","countryId":"US"},{"country":"United Kingdon","countryId":"UK"}]
The orderBy filter performs a stable sorting as of Angular 1.4.5. (See the GitHub pull request https://github.com/angular/angular.js/pull/12408.)
So it is sufficient to use a constant predicate and reverse set to true:
<div ng-repeat="friend in friends | orderBy:0:true">{{friend.name}}</div>
I found something like this, but instead of array i use objects.
Here is my solution for objects:
Add custom filter:
app.filter('orderObjectBy', function() {
return function(items, field, reverse){
var strRef = function (object, reference) {
function arr_deref(o, ref, i) {
return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]);
}
function dot_deref(o, ref) {
return !ref ? o : ref.split('[').reduce(arr_deref, o);
}
return reference.split('.').reduce(dot_deref, object);
};
var filtered = [];
angular.forEach(items, function(item) {
filtered.push(item);
});
filtered.sort(function (a, b) {
return (strRef(a, field) > strRef(a, field) ? 1 : -1);
});
if(reverse) filtered.reverse();
return filtered;
};
});
Which can then be used like
<div ng-repeat="(key, value) in items | orderObjectBy:'field.any.deep':true">
If you need old browser support, you will need to define the reduce function (this is only available in ECMA-262 mozilla.org)
// Production steps of ECMA-262, Edition 5, 15.4.4.21
// Reference: http://es5.github.io/#x15.4.4.21
if (!Array.prototype.reduce) {
Array.prototype.reduce = function(callback /*, initialValue*/) {
'use strict';
if (this == null) {
throw new TypeError('Array.prototype.reduce called on null or undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
var t = Object(this), len = t.length >>> 0, k = 0, value;
if (arguments.length == 2) {
value = arguments[1];
} else {
while (k < len && !(k in t)) {
k++;
}
if (k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
value = t[k++];
}
for (; k < len; k++) {
if (k in t) {
value = callback(value, t[k], k, t);
}
}
return value;
};
}
I had gotten frustrated with this problem myself and so I modified the filter that was created by #Trevor Senior as I was running into an issue with my console saying that it could not use the reverse method. I also, wanted to keep the integrity of the object because this is what Angular is originally using in a ng-repeat directive. In this case I used the input of stupid (key) because the console will get upset saying there are duplicates and in my case I needed to track by $index.
Filter:
angular.module('main').filter('reverse', function() {
return function(stupid, items) {
var itemss = items.files;
itemss = itemss.reverse();
return items.files = itemss;
};
});
HTML:
<div ng-repeat="items in items track by $index | reverse: items">
Im adding one answer that no one mentioned. I would try to make the server do it if you have one. Clientside filtering can be dangerous if the server returns a lot of records. Because you might be forced to add paging. If you have paging from the server then the client filter on order, would be in the current page. Which would confuse the end user. So if you have a server, then send the orderby with the call and let the server return it.
Useful tip:
You can reverse you're array with vanilla Js: yourarray .reverse()
Caution: reverse is destructive, so it will change youre array, not only the variable.
I would sugest using array native reverse method is always better choice over creating filter or using $index.
<div ng-repeat="friend in friends.reverse()">{{friend.name}}</div>
Plnkr_demo.
Here's the template structure
{{#each loadedEvents}}
{{#if future}}
{{#if timezone="Europe/Warsaw"}}
{{> event}}
{{/if}}
{{/each}}
Is that possible to view only items with given value?
And the second question, how to combine this two statements:
{{#if future}} {{#if timezone="Europe/Warsaw"}}
You can create a dedicated helper to check if a timezone is equal to a certain value :
Template.loadedEvents.helpers({
timezoneIs: function(timezone){
return this.timezone == timezone;
}
});
If you want to combine two Spacebars {{#if}} block helpers, once again create a dedicated helper that performs the test in JS :
JS
Template.loadedEvents.helpers({
isFutureAndTimezoneIs: function(timezone){
return this.future && this.timezone == timezone;
}
});
HTML
{{#each loadedEvents}}
{{#if isFutureAndTimezoneIs "Europe/Warsaw"}}
{{> event}}
{{/if}}
{{/each}}
Use Template.registerHelper to create a global helper. For instance, to create a helper that compares two arbitrary variables:
Template.registerHelper('compare', function(v1, v2) {
if (typeof v1 === 'object' && typeof v2 === 'object') {
return _.isEqual(v1, v2); // do a object comparison
} else {
return v1 === v2;
}
});
Then use it using:
{{#if compare timezone "Europe/Warsaw"}}
// Do something
{{/if}}
With handlebars.js I want to display two blocks of html depending of a resulting json.
Let's say I want to thanks my user for ordering items at my shop.
I write my handlerbars.js template like this :
<p>{{name}}</p>
{{#if costIsZero}}
Can't find any order
{{else}}
You bought {{cost}} items in our shop, thanks.
{{/if}}
I'm coding a simple helper for costIsZero like this :
Handlebars.registerHelper('costIsZero', function(){
return this.cost == 0
});
When I mix it with the following json data :
var data = {
"name":"foo",
"cost": 9
};
Whatever the value of "cost" is the {{#if costIsZero}} seems always to be true.
If I comment out the helper itself, thus having nothing for costIsZero it returns always false.
All the code above is available as a JSFiddle there http://jsfiddle.net/gsSyt/
What I'm doing wrong ?
Maybe I'm hijacking the way handlebars.js work, but in that case, How should I implement my feature with handlebars.js ?
Helpers are not invoked when evaluating an expression such as costIsZero.
You could create a custom helper that works as an alternative to if:
Handlebars.registerHelper('ifCostIsZero', function(block) {
if (this.cost == 0) {
return block(this);
} else {
return block.inverse(this);
}
});
Which you would use like this:
{{#ifCostIsZero}}
Can't find any order
{{else}}
You bought {{cost}} items in our shop, thanks.
{{/ifCostIsZero}}
Alternatively, you can use the stock if (or unless) since your test is against zero :
{{#if cost}}
You bought {{cost}} items in our shop, thanks.
{{else}}
Can't find any order
{{/if}}
You can play with both options at http://jsfiddle.net/gsSyt/41/
Try this:
Handlebars.registerHelper('if', function(conditional, block) {
if(this.cost == 0) {
return block(this);
} else {
return block.inverse(this);
}
})
http://jsfiddle.net/mZbtk/2/