Can I shrink this code with an array or loop? - javascript

Forgive me, I hope this question isn't too obvious, I'm a javascript noob.
I have javascript code that takes numbers from an xml sheet and displays them in td elements on an html page. It works but I think it could be condensed into an array or a loop to be more efficient.
Is there a better way to write this code?
window.onload=function displayPrices()
{
twentyFourK=(x[i].getElementsByTagName("twentyFourK")[0].childNodes[0].nodeValue);
document.getElementById("twentyFourK").innerHTML=toCurrency(twentyFourK);
oneOzGold=(x[i].getElementsByTagName("oneOzGold")[0].childNodes[0].nodeValue);
document.getElementById("oneOzGold").innerHTML=toCurrency(oneOzGold);
fiveOzGold=(x[i].getElementsByTagName("fiveOzGold")[0].childNodes[0].nodeValue);
document.getElementById("fiveOzGold").innerHTML=toCurrency(fiveOzGold);
tenOzGold=(x[i].getElementsByTagName("tenOzGold")[0].childNodes[0].nodeValue);
document.getElementById("tenOzGold").innerHTML=toCurrency(tenOzGold);
oneKiloGold=(x[i].getElementsByTagName("oneKiloGold")[0].childNodes[0].nodeValue);
document.getElementById("oneKiloGold").innerHTML=toCurrency(oneKiloGold);
//etc.
}

Yes, a function could make things much easier for you:
window.onload = function() {
function loadCurrency(name) {
document.getElementById(name).innerHTML = toCurrency(x[i].getElementsByTagName(name)[0].firstChild.nodeValue);
}
loadCurrency('twentyFourK');
loadCurrency('oneOzGold');
loadCurrency('fiveOzGold');
loadCurrency('tenOzGold');
loadCurrency('oneKiloGold');
};
Also, if you have many items to load:
window.onload = function() {
function loadCurrency(name) {
document.getElementById(name).innerHTML = toCurrency(x[i].getElementsByTagName(name)[0].firstChild.nodeValue);
}
var items = ['twentyFourK', 'oneOzGold', 'fiveOzGold', 'tenOzGold', 'oneKiloGold'];
items.forEach(loadCurrency);
};
That requires Array.forEach, which is only available in ECMAScript 5, so here's a fallback:
Array.prototype.forEach = function(action, thisArg) {
for(var i = 0, l = this.length; i < l; i++) {
if(i in this) {
action.call(thisArg, this[i], i, this);
}
}
};

I would place the currency setting into its own method. This will be cleaner visually and will also allow for implementation changes in the future:
window.onload = function displayPrices() {
SetCurrency("twentyFourK");
SetCurrency("oneOzGold");
//etc.
}
function SetCurrency(name) {
var elements = x[i].getElementsByTagName(name);
if ((elements != null) && (elements.length != 0)) {
elements[0].innerHTML = toCurrency(elements[0].childNodes[0].nodeValue);
}
}

You could create a function with a list of element in parameter, and you just have to create a loop going through your list of elements (twentyFourK, oneKiloGold and so on)

Related

DRY up htmlCollection to Array calls

I have a function that is currently using the .getElementBy... DOM calls in JavaScript.
var $ = function (selector) {
var elements = [];
var lastSelector = selector.substring(selector.search(/[^#.]+$/), selector.length);
if(selector.includes('#') !== true || selector.includes('.') !== true) {
elements.push(document.getElementsByTagName(lastSelector));
elements = Array.prototype.slice.call(elements[0]);
}
return elements;
};
There are a number of other if statements in the function using the code:
elements.push(document.getElementsByTagName(lastSelector));
elements = Array.prototype.slice.call(elements[0]);
or
elements.push(document.getElementsByClassName(lastSelector));
elements = Array.prototype.slice.call(elements[0]);
Ideally i'd like to DRY up the repeated:
elements = Array.prototype.slice.call(elements[0]);
but I cannot define it before the if statements because elements has not yet been populated. It therefore tries to run the code on an empty array and errors.
Any suggestions?
Instead of using a home-brew limited function for selecting elements by a selector, you could just use the standard querySelectorAll() available in all browsers including IE8+.
As for converting an array-like object (e. g. a DOM collection) to a real Array (what Array.prototype.slice.call() is used for in your code), I use the following function:
var arrayFrom = function(arrayLike) {
if (Array.from) {
return Array.from(arrayLike);
}
var items;
try {
items = Array.prototype.slice.call(arrayLike, 0);
}
catch(e) {
items = [];
var count = arrayLike.length;
for (var i = 0; i < count; i++) {
items.push(arrayLike[i]);
}
}
return items;
};
or its following simplified version if browsers not supporting passing a non-Array argument to Array.prototype.slice.call() (IE8- if I recall correctly) don’t matter:
var arrayFrom = function(arrayLike) {
return Array.from
? Array.from(arrayLike);
: Array.prototype.slice.call(arrayLike, 0);
};
Certainly consider #marat-tanalin answer. In the case where using querySelectorAll() is not an option, the following worked for me, thanks #master565 for the help:
To start, wrapping the lines:
elements.push(document.getElementsByTagName(lastSelector));
elements = Array.prototype.slice.call(elements[0]);
in a function:
function pushByTag(selector) {
elements.push(document.getElementsByTagName(selector));
elements = Array.prototype.slice.call(elements[0]);
}
Dried things up considerably. Then setting a variable for the if argument helped a lot:
if(selector.includes('#') !== true || selector.includes('.') !== true)
became:
var noClassOrId = selector.includes('#') !== true || selector.includes('.') !== true;
Both these refactors allowed me to single line my if statement in to something I'd argue was fairly readable:
if (noClassOrId) pushByTag(lastSelector);

Javascript: move objects from one array to another: Best approach?

I have two arrays, called 'objects' and 'appliedObjects'. I'm trying to come up with an elegant way in Javascript and/or Angular to move objects from one array to another.
Initially I did something like this:
$scope.remove = function () {
angular.forEach($scope.appliedObjects, function (element, index) {
if (element.selected) {
element.selected = false;
$scope.objects.push(element);
$scope.appliedObjects.splice(index, 1);
}
});
}
$scope.add= function () {
angular.forEach($scope.objects, function (element, index) {
if (element.selected) {
element.selected = false;
$scope.appliedObjects.push(element);
$scope.objects.splice(index, 1);
}
});
}
But then I realized that when the value was removed from the looping array, and it would not add or remove every other item, since it went by index.
Then I tried using a temporary array to hold the list of items to be added or removed, and I started getting strange referential issues.
I'm starting to spin a bit on what the best solution to this problem would be...any help and/or guidance would much appreciated.
function moveElements(source, target, moveCheck) {
for (var i = 0; i < source.length; i++) {
var element = source[i];
if (moveCheck(element)) {
source.splice(i, 1);
target.push(element);
i--;
}
}
}
function selectionMoveCheck(element) {
if (element.selected) {
element.selected = false;
return true;
}
}
$scope.remove = function () {
moveElements($scope.appliedObjects, $scope.objects, selectionMoveCheck);
}
$scope.add = function () {
moveElements($scope.objects, $scope.appliedObjects, selectionMoveCheck);
}
When a construct does too much automatically (like forEach, or even a for-loop, in this case), use a more primitive construct that allows you to say what should happen clearly, without need to work around the construct. Using a while loop, you can express what needs to happen without resorting to backing up or otherwise applying workarounds:
function moveSelected(src, dest) {
var i = 0;
while ( i < src.length ) {
var item = src[i];
if (item.selected) {
src.splice(i,1);
dest.push(item);
}
else i++;
}
}
You are altering the array while iterating on it, you will always miss some elements.
One way of doing it would be to use a third array to store the references of the objects that need to be removed from the array:
// "$scope.add" case
var objectsToRemove = [];
$scope.objects.forEach(function (value) {
if (value.selected) {
value.selected = false;
$scope.appliedObjects.push(value);
objectsToRemove.push(value);
}
});
objectsToRemove.forEach(function (value) {
$scope.objects.splice($scope.objects.indexOf(value), 1);
});
If you wish to move simply whole array you could do:
appliedObjects = objects;
objects = []
Of course it won't work if they were parameters of a function!
Otherwise I cannot see other way than copying in the loop, e.g.
while (objects.length) {
appliedObjects.push(objects[0]);
objects.splice(0,1);
}
or if you like short code :) :
while (objects.length) appliedObjects.push(objects.splice(0,1));
check fiddle http://jsfiddle.net/060ywajm/
Now this maybe is not a fair answer, but if you notice you are doing alot of complicated object/array manipulations, you should really check out lodash or underscore library. then you could solve this with on liner:
//lodash remove function
appliedObjects.push.apply( appliedObjects, _.remove(objects, { 'selected': true}));
//or if you want to insert in the beginning of the list:
appliedObjects.splice(0, 0, _.remove(objects, { 'selected': true}));
This is a first pass at what I think will work for you. I'm in the process of making a test page so that I can test the accuracy of the work and will update the tweaked result, which hopefully there will not be.
EDIT: I ran it and it seems to do what you are wanting if I understand the problem correctly. There were a couple of syntax errors that I edited out.
Here's the plunk with the condensed, cleaned code http://plnkr.co/edit/K7XuMu?p=preview
HTML
<button ng-click="transferArrays(objects, appliedObjects)">Add</button>
<button ng-click="transferArrays(appliedObjects, objects)">Remove</button>
JS
$scope.transferArrays = function (arrayFrom, arrayTo) {
var selectedElements;
selectedElements = [];
angular.forEach(arrayFrom, function(element) {
if (element.isSelected) {
element.isSelected = false;
selectedElements.push(element);
}
});
angular.forEach(selectedElements, function(element) {
arrayTo.push(arrayFrom.splice(
arrayFrom.map(function(x) {
return x.uniqueId;
})
.indexOf(element.uniqueId), 1));
});
};
Old code
$scope.remove = function () {
var selectedElements;
selectedElements = [];
angular.forEach($scope.appliedObjects, function (element) {
if (element.isSelected) {
element.isSelected = false;
selectedElements.push(element);
}
});
angular.forEach(selectedElements, function (element) {
$scope.objects.push($scope.appliedObjects.splice(
$scope.appliedObjects.map(function (x) { return x.uniqueId; })
.indexOf(element.uniqueId), 1));
});
};
$scope.add = function () {
var selectedElements;
selectedElements = [];
angular.forEach($scope.objects, function (element) {
if (element.isSelected) {
element.isSelected = false;
selectedElements.push(element);
}
});
angular.forEach(selectedElements, function (element) {
$scope.appliedObjects.push($scope.objects.splice(
$scope.objects.map(function (x) { return x.uniqueId; })
.indexOf(element.uniqueId), 1));
});
};
You can use this oneliner as many times as many items you need to move from arr1 to arr2 just prepare check func
arr2.push(arr1.splice(arr1.findIndex(arr1El => check(arr1El)),1)[0])
You can use this to concat 2 arrays:
let array3 = [...array1, ...array2];

How do I parse the results of a querySelectorAll selector engine & allow method chaining?

SIMPLIFIED EXAMPLE CODE:
var $ = function(selector, node) { // Selector engine
var selector = selector.trim(), node = node || document.body;
if (selector != null) {
return Array.prototype.slice.call(node.querySelectorAll(selector), 0); }
}
}
I want to use it like this...:
$("div").innerHTML='It works!';
...not like this...:
$("div")[0].innerHTML='It works only on the specified index!';
...or this:
for(i=0;i<$('div');i++) {
$("div")[i].innerHTML='It works great but it's ugly!';
}
This is as close as I got. I would like chaining to work and for it to be compatible with native methods:
if(!Array.prototype.innerHTML) {
Array.prototype.innerHTML = function(html) {
for (var i = 0; i < this.length; i++) {
this[i].innerHTML = html;
}
}
}
$("div").innerHTML('It works, but it ruins method chaining!');
I decided to build this engine to better learn JavaScript; It's working but I am hoping I can learn some more from the kind members of Stack Overflow. Any help would be much appreciated!
I want to use it like this...:
$("div").innerHTML='It works!';
...not like this...:
$("div")[0].innerHTML='It works only on the specified index!';
It sounds like you want to have assigning to innerHTML on your set of results assign to the innerHTML of all of the results.
To do that, you'll have to use a function, either directly or indirectly.
Directly:
var $ = function(selector, node) { // Selector engine
var selector = selector.trim(),
node = node || document.body,
rv;
if (selector != null) {
rv = Array.prototype.slice.call(node.querySelectorAll(selector), 0); }
rv.setInnerHTML = setInnerHTML;
}
return rv;
}
function setInnerHTML(html) {
var index;
for (index = 0; index < this.length; ++index) {
this[index].innerHTML = html;
}
}
// Usage
$("div").setInnerHTML("The new HTML");
There, we define a function, and we assign it to the array you're returning as a property. You can then call that function on the array. (You might want to use Object.defineProperty if it's available to set the setInnerHTML property, so you can make it non-enumerable.)
Indirectly (requires an ES5-enabled JavaScript engine):
var $ = function(selector, node) { // Selector engine
var selector = selector.trim(),
node = node || document.body,
rv;
if (selector != null) {
rv = Array.prototype.slice.call(node.querySelectorAll(selector), 0); }
Object.defineProperty(rv, "innerHTML", {
set: setInnerHTML
});
}
return rv;
}
function setInnerHTML(html) {
var index;
for (index = 0; index < this.length; ++index) {
this[index].innerHTML = html;
}
}
// Usage
$("div").innerHTML = "The new HTML";
There, we use Object.defineProperty to define a setter for the property.
In the comments below you say
I have a few prototypes that work when individually attached to the $ function. Example: $('div').makeClass('this'); They do not work when they are chained together. Example: $('div').makeClass('this').takeClass('that');
To make chaining work, you do return this; from each of the functions (so the end of makeClass would do return this;). That's because when you're chaining, such as obj.foo().bar(), you're calling bar on the return value of foo. So to make chaining work, you make sure foo returns this (the object on which foo was called).
This is what works; it's a slightly different syntax then I gave in my prior example, but the end result is the same. I had some great help from other Stack Exchange members, thanks again everyone.
var $ = function(selector, node) { // Selector engine
var selector = selector.trim(), node = node || document.body;
if (selector != null) {
return Array.prototype.slice.call(node.querySelectorAll(selector), 0); }
}
}
if(!Array.prototype.html) {
Array.prototype.html = function(html) {
for (var i = 0; i < this.length; i++) {
this[i].innerHTML = html;
}
return this; //<---- Silly me, my original code was missing this.
}
}
When I run it, everything (including chaining) works as desired:
$("div").html('hello world');
OUTPUT:
<div>hello world</div>
Cheers!

Cannot access array elements properly within setTimeout

Here what I'm trying to do.
I'm having an array like the following
var my_array = ['1', '2', '3' ... ,'1000000000000000'];
What I want to do is create a bunch of HTML elements for every element of that array, and since the array can contain a huge number of elements I attempted to do the following so the browser won't freeze.
for(var i in my_array)
{
if(my_array.hasOwnProperty(i))
{
setTimeout(function(){
do_something_with_data(my_array[i]);
});
}
}
What happens though is that the my_array[i] within the setTimeout doesn't have the value it should.
To be more accurate, when I try to console.log(my_array[i]) what I get is something like this:
"getUnique" function (){
var u = {}, a = [];
for(var i = 0, l = this.length; i < l; ++i){
if(u.hasOwnProperty(this[i])) {
continue;
}
a.push(this[i]);
u[this[i]] = 1;
}
return a;
}
getUnique is a function I've added to the Array prototype just like this:
Array.prototype.getUnique = function(){
var u = {}, a = [];
for(var i = 0, l = this.length; i < l; ++i){
if(u.hasOwnProperty(this[i])) {
continue;
}
a.push(this[i]);
u[this[i]] = 1;
}
return a;
};
Can please somebody help me with this issue?
the setTimeout is executed after the loop is done, and i is the last key or some garbage value at that point. You can capture the i like so:
for (var i in my_array) {
if (my_array.hasOwnProperty(i)) {
(function(capturedI) {
setTimeout(function() {
do_something_with_data(my_array[capturedI]);
});
})(i);
}
}
You should also not use for..in loops for arrays because it's an order of magnitude slower (especially so with the .hasOwnProperty check) than a for loop and the iteration order is not defined
If you have jQuery or willing to add some extra code for older browsers, you can do:
my_array.forEach( function( item ) {
setTimeout( function() {
do_something_with_data( item );
}, 1000);
});
With jQuery:
$.each( my_array, function( index, item ) {
setTimeout( function() {
do_something_with_data( item );
}, 1000);
});
See docs for [].forEach
The problem is that the functions you're creating have a reference to the i variable, not a copy of its value, and so when they run they see i as it is at that point in time (past the end of the array, presumably). (More: Closures are not complicated)
I'd recommend a completely different approach (below), but first, let's look at how to make your existing approach work.
To do what you were trying to do, with the for loop, you have to have the functions close over something that won't change. The usual way to do that is to use a factory function that creates the timeout functions such that they close over the argument to the factory. Or actually, you can pass in the array element's value rather than the index variable.
for(var i in my_array)
{
if(my_array.hasOwnProperty(i))
{
setTimeout(makeFunction(my_array[i]));
}
}
function makeFunction(entry) {
return function(){
do_something_with_data(entry);
};
}
But, I would probably restructure the code so you're not creating masses and masses of function objects unnecessarily. Instead, use one function, and have it close over an index that it increments:
// Assumes `my_array` exists at this point, and that it
// has at least one entry
var i = 0;
setTimeout(tick, 0);
function tick() {
// Process this entry
if (my_array.hasOwnProperty(i)) {
do_something_with_data(my_array[i]);
}
// Move to next
++i;
// If there are any left, schedule the next tick
if (i < my_array.length) {
setTimeout(tick, 0);
}
}
Its just a guess. Try it like:
for(var i in my_array)
{
if(my_array.hasOwnProperty(i))
setTimeout("do_something_with_data('"+my_array[i]+"')", 500);
}

Pure JavaScript equivalent of jQuery click()?

I am building a small app which captures mouse clicks. I wrote the prototype in jQuery but, since it is a small app focusing on speed, embedding jQuery to use just one function would be an overkill.
I tried to adapt this example from JavaScriptKit:
document.getElementById("alphanumeric").onkeypress=function(e){
//blah..blah..blah..
}
but it didn't work when I tried this:
document.getElementsByTagName("x").onclick
What am I doing wrong?
Say you have a list of p tags you would like to capture the click for the <p> tag:
var p = document.getElementsByTagName("p");
for (var i = 0; i < p.length; i++) {
p[i].onclick = function() {
alert("p is clicked and the id is " + this.id);
}
}
Check out an example here for more clarity:
http://jsbin.com/onaci/
In your example you are using getElementsByTagName() method, which returns you an array of DOM elements. You could iterate that array and assign the onclick handler to each element, for example:
var clickHandler = function() {
alert('clicked!');
}
var elements = document.getElementsByTagName('div'); // All divs
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = clickHandler;
}
it looks a little bit like you miss more than just the click function of jQuery. You also miss jquery's selector engine, chaining, and automatic iteration across collections of objects. With a bit more effort you can minimally reproduce some of those things as well.
var myClickCapture = function (selector) {
var method, name,iterator;
if(selector.substr(0,1) === "#") {
method = "getElementById";
name = selector.substr(1);
iterator = function(fn) { fn(document[method](name)); };
} else {
method = "getElementsByTagName";
name = selector;
iterator = function(fn) {
var i,c = document[method](name);
for(i=0;i<c.length;i++){
fn(c[i]);
};
};
myClickCapture.click = function (fn){
iterator(function(e){
e.onclick=fn;
})
}
return myClickCapture;
}
I haven't tested the code, but in theory, it gets you something like this:
myClickCapture("x").click(function(e){ alert("element clicked") });
Hopefully this gives you a sense of the sorts of things jquery is doing under the covers.
document.getElementsByTagName("x")
returns an array of elements having the tagname 'x'.
You have to right event for each element in the returned array.

Categories

Resources