Alternatives to eval() for multiple nested objects - javascript

I'm trying to create a generic i18n solution for a HTML app I'm working in. I'm looking for alternatives to use eval() to call deeply nested Javascript objects:
Suppose the following HTML example:
<div id="page1">
<h1 data-i18n="html.pageOne.pageTitle"></h1>
</div>
and it's companion Javascript (using jQuery):
var i18n;
i18n = {
html: {
pageOne: {
pageTitle: 'Lorem Ipsum!'
}
}
};
$(document).ready(function () {
$('[data-18n]').each(function () {
var q;
q = eval('i18n.' + $(this).attr('data-i18n'));
if (q) {
$(this).text(q);
}
});
});
Any advices on how to access the "pageTitle" property inside the i18n object without using eval()? I need to keep the object's structure, so changing its layout to a "flat" solution is not feasible.
Thanks!!!

You can use bracket syntax, as others have hinted at. But, you'll need to split and iterate at .:
function lookup(obj, path) {
var keys = path.split('.'),
result = obj;
for (var i = 0, l = keys.length; i < l; i++) {
result = result[keys[i]];
// exit early if `null` or `undefined`
if (result == null)
return result;
}
return result;
}
Then:
q = lookup(i18n, $(this).attr('data-i18n'));
if (q) {
$(this).text(q);
}

The dot syntax (object.field) is really just syntactic sugar for object['field']. If you find yourself writing eval('object.'+field), you should simply write object['field'] instead. In your example above, you probably want: i18n[$(this).attr('data-i18n')].
Since you're encoding your attribute in a way that has dots in it, try splitting it by the dots, and iterating over the fields. For example (this can probably be improved):
var fields = $(this).attr('i18n').split('.');
fieldCount = fields.length;
fieldIdx = 0;
var cur = i18n;
while(cur != undefined && fieldIdx > fieldCount) {
cur = cur[fields[fieldIdx++]];
}
You'll want to do additional checking to make sure all of the fields were handled, nulls weren't encountered, etc.

You can split the string on the periods and traverse the object:
var q = i18n;
$.each($(this).attr('data-i18n').split('.'), function(index, key){
if (q) q = q[key];
});
Demo: http://jsfiddle.net/GsVsr/

Related

getElementByID loop returning null

Chrome developer tools says that the value function doesn't work on a null value and points to the line in the for loop. Why isn't getElementByID fetching my values? (this is a refactor, getElement work perfect with the actual values typed in).
locationStops = ["start","end"];
var stopNum = locationStops.length;
var stopAddresses = [];
for(val in locationStops) {
stopAddresses.push(document.getElementById(val).value);
}
You could avoid the for loop, and the potential for bugs that you ran into with, by using map:
stopAddresses = locationStops . map(function(id) {
return document.getElementById(id).value;
});
Depending on your stylistic preferences, you might find the following more readable:
function get_value_from_id(id) {
return document.getElementById(id).value;
}
stopAddresses = locationStops . map(get_value_from_id);
If you want to use a loop, you could use the new for...of construct:
for (let val of locationStops) {
^^
stopAddresses.push(document.getElementById(val).value);
}
If you have an environment that supports ES7 array comprehensions:
[ for (id of locationStops) document.getElementById(id).value ]
If you want to stick with your for...in loop, then as other answers have pointed out, the loop variable is the index, not the value, so you have to access the ID with locationStops[i], but you are better off using a regular for loop.
Do not use for in for arrays.
Use a simple for loop instead.
var a = ["start", "end"];
for(var i = 0; i < a.length; ++i)
{
console.log(document.getElementById(a[i]).value);
}
You can use for-in also but, it is not recommended as it results in unexpected behaviour sometimes.
val refers to 0,1 etc. So there must be elements with ID's 0,1.
for(var val in a)
{
console.log(document.getElementById(a[val]).value);
}
Your code is not working because your for loop syntax is incorrect
Try This
var locationStops = ["start","end"];
var stopNum = locationStops.length;
var stopAddresses = [];
for(i = 0; i < locationStops.length; i++) {
stopAddresses.push(document.getElementById(locationStops[i]).value);
}
Alternatively, you could use Array.prototype.map.
var locationStops = ["start","end"];
var stopAddresses = locationStops.map(function(val) {
return document.getElementById(val).value;
});
Honestly, though, looping over a two element array is kind of silly and if it was my code I would even prefer to simply assign each address directly.
var stopAddresses = [document.getElementById("start").value, document.getElementById("end").value];

Javascript efficient search array for value with jQuery

There's a gap in my JavaScript knowledge here. I want to search an array of objects values for a particular value and return it.
For the year I have been writing JavaScript, I have been implementing it like this:
var itemClicked = (function(){
var retval;
//Note self.inventory.itemsArray is an array of JS objects
$(self.inventory.itemsArray).each(function(i){
if(parseInt(this.id) === parseInt(idOfItem)){
retval = this;
return false;
}
});
return retval;
})();
It works, but I'm sure as anything there is a more elegant way. Tell me please!
EDIT - Solution
Thanks to #gdoron with his answer below.
var myVar = $(self.owner.itemsArray).filter(function(){
return parseInt(this.id) == parseInt(recItemID);
}).get(0);
Note: .get(0) was added at the end because myVar is wrapped as a jQuery object.
The native jQuery function for this is filter:
$(data).filter(function(){
return this.id == "foo";
});
It's shorter than code you have and more important a lot more readable.
About efficiency, it will iterate all the elements in the set to find as much as possible matches, but I hardly believe it will be the bottle neck of your application, don't focus on micro-optimisations.
I suggest you read Eric Lipper blog about Which is faster.
You can also use grep as suggested by #Mattias Buelens:
$.grep(data, function(ele){
retun ele.id == "foo";
});
Just another option using jQuery's $.grep( ) function
var arr = $.grep( self.inventory.itemsArray, function ( n ) {
return n.id == idOfItem;
});
The above returns an array of matching array elements. If you just want the first it is easy enough to return arr[0] if it exists.
Although I'm unsure what the function is actually supposed to do (due to the external contexts' variables), the following should be more efficient cycle-wise
var itemClicked = (function(){
var i, array = self.inventory.itemsArray, length = array.length;
for( i=0; i < length; i++) {
if(parseInt(array[i].id) === parseInt(idOfItem)){
return array[i];
}
}
return undefined;
})();
It's an array of Javascript objects
Then do not use jQuery at all. At least, use $.each instead of building a wrapper object around the array. Still, a simple for-loop is much shorter and more performant:
var itemClicked = (function(idnum) {
var arr = self.inventory.itemsArray;
for (var i=0, l=arr.length; i<l; i++)
if (parseInt(arr[i].id, 10) === idnum)
return arr[i];
})( parseInt(idOfItem, 10) );
You might as well think of storing the id properties as numbers right away, so you don't need to convert it each time.

Custom for-loop helper for EmberJS/HandlebarsJS

A small two hours ago I started: Nested HandlebarsJS #each helpers with EmberJS not working
Shortly after I figured an acceptable temporary solution myself, question is still unaswered. My problems didn't stop there though.
I am now trying to make a custom helper which will loop through an array of objects, but exclude the first index - pretty much: for(i = 1; i < length; i++) {}. I've read on websites you have to get the length of your context and pass it to options - considering your function looks like: forLoop(context, options).
However, context is a string rather than an actual object. When you do a .length, you will get the length of the string, rather than the size of the array. When I pass that to options, nothing happens - not too mention browser freezes.
I then first tried to do a getPath before passing it to options, this returns an empty string.
What am I supposed to do instead, I made the for-loop code before for just HandlebarsJS and that worked, but EmberJS doesn't seem to take it, why?
EDIT: I pretty much also followed: http://handlebarsjs.com/block_helpers.html -> Simple Iterators
I solved this myself after trying for a long time.
The HandlebarsJS method (as described on the site) is no longer valid for EmberJS, it's now as follows:
function forLoop(context, options) {
var object = Ember.getPath(options.contexts[0], context);
var startIndex = options.hash.start || 0;
for(i = startIndex; i < object.length; i++) {
options(object[i]);
}
}
Heck, you could even extend the for-loop to include an index-value!
function forLoop(context, options) {
var object = Ember.getPath(options.contexts[0], context);
var startIndex = options.hash.start || 0;
for(i = startIndex; i < object.length; i++) {
object[i].index = i;
options(object[i]);
}
}
This is a working for-loop with variable start index. You use it in your templates like so:
{{#for anArray start=1}}
<p>Item #{{unbound index}}</p>
{{/for}}
Here is how I did it (and it works !!!)
First,
i had in my model a 'preview' property/function, that just return the arrayController in an array :
objectToLoop = Ember.Object.extend({
...
arrayController: [],
preview: function() {
return this.get('arrayController').toArray();
}.property('arrayController.#each'),
...
});
Then, I add a new Handlebars helper :
Handlebars.registerHelper("for", function forLoop(arrayToLoop, options) {
var data = Ember.Handlebars.get(this, arrayToLoop, options.fn);
if (data.length == 0) {
return 'Chargement...';
}
filtered = data.slice(options.hash.start || 0, options.hash.end || data.length);
var ret = "";
for(var i=0; i< filtered.length; i++) {
ret = ret + options.fn(filtered[i]);
}
return ret;
});
And thanks to all this magic, I can then call it in my view :
<script type="text/x-handlebars">
<ul>
{{#bind objectToLoop.preview}}
{{#for this end=4}}
<li>{{{someProperty}}}</li>
{{/for}}
{{/bind}}
</ul>
</script>
And that's it.
I know it is not optimal, so whoever have an idea on how to improve it, PLEASE, make me know :)

Get URL array variables in javascript/jquery

I've got a bunch of parameters being passed to a page by URL variables. The URL looks sort of like:
file.aspx?category[]=1&category[]=7&category[]=3&id=8az
Using the jQuery getUrlParam extension I can get url variables very easily, but rather than returning category as an array (which is what I want) it gets returned as null.
Is there a way for me to read these into a javascript array?
I previously pointed to this question: Get QueryString values with jQuery - but as #Crescent Fresh pointed out, those examples don't deal with arrays in the query string (and besides, they're a bit slow I think.
So I cooked up my version of this function:
function getQueryString () {
var ret = {};
var parts = (document.location.toString().split('?')[1]).split('&');
for (var i = 0; i < parts.length; i++) {
var p = parts[i].split('=');
// so strings will be correctly parsed:
p[1] = decodeURIComponent(p[1].replace(/\+/g, " "));
if (p[0].search(/\[\]/) >= 0) { // then it's an array
p[0] = p[0].replace('[]','');
if (typeof ret[p[0]] != 'object') ret[p[0]] = [];
ret[p[0]].push(p[1]);
} else {
ret[p[0]] = p[1];
}
}
return ret;
}
But there are caveats. It will only work on a correctly formed query string - there's no error detection. Also, it does not work on numbered/indexed arrays.. that is when your array is defined in the query string as:
?category[3]=1&category[4]=7&category[20]=3&id=8az
It would be trivial to add to the .search() query a regex for finding that as well, but I'm not the best regex expert... anybody got ideas?
Shouldn't it be: file.aspx?category=1&category=7&category=3

Better way to see if an array contains an object?

I have an array of items (terms), which will be put as <option> tags in a <select>. If any of these items are in another array (termsAlreadyTaking), they should be removed first. Here is how I have done it:
// If the user has a term like "Fall 2010" already selected, we don't need that in the list of terms to add.
for (var i = 0; i < terms.length; i++)
{
for (var iAlreadyTaking = 0; iAlreadyTaking < termsAlreadyTaking.length; iAlreadyTaking++)
{
if (terms[i]['pk'] == termsAlreadyTaking[iAlreadyTaking]['pk'])
{
terms.splice(i, 1); // remove terms[i] without leaving a hole in the array
continue;
}
}
}
Is there a better way to do this? It feels a bit clumsy.
I'm using jQuery, if it makes a difference.
UPDATE Based on #Matthew Flaschen's answer:
// If the user has a term like "Fall 2010" already selected, we don't need that in the list of terms to add.
var options_for_selector = $.grep(all_possible_choices, function(elem)
{
var already_chosen = false;
$.each(response_chosen_items, function(index, chosen_elem)
{
if (chosen_elem['pk'] == elem['pk'])
{
already_chosen = true;
return;
}
});
return ! already_chosen;
});
The reason it gets a bit more verbose in the middle is that $.inArray() is returning false, because the duplicates I'm looking for don't strictly equal one another in the == sense. However, all their values are the same. Can I make this more concise?
var terms = $.grep(terms, function(el)
{
return $.inArray(el, termsAlreadyTaking) == -1;
});
This still has m * n performance (m and n are the lengths of the arrays), but it shouldn't be a big deal as long as they're relatively small. To get m + n, you could use a hashtable
Note that ECMAScript provides the similar Array.filter and Array.indexOf. However, they're not implemented in all browsers yet, so you would have to use the MDC implementations as a fallback. Since you're using jQuery, grep and inArray (which uses native indexOf when available) are easier.
EDIT:
You could do:
var response_chosen_pk = $.map(response_chosen_items, function(elem)
{
return elem.pk;
});
var options_for_selector = $.grep(all_possible_choices, function(elem)
{
return $.inArray(elem.pk, response_chosen_pk) == -1;
});
http://github.com/danstocker/jorder
Create a jOrder table on termsAlreadyTaking, and index it with pk.
var table = jOrder(termsAlreadyTaking)
.index('pk', ['pk']);
Then you can search a lot faster:
...
if ([] == table.where([{ pk: terms[i].pk }]))
{
...
}
...

Categories

Resources