I have this function:
function worker(iS) {
var office = docXML.getElementsByTagName("office")[iS];
var counter = office.getElementsByTagName("worker").length;
for (var i = 0; i < counter; i++) {
return office.getElementsByTagName("worker")[i].childNodes[0].nodeValue;
}
}
Note: on my docXML I have various "office"
<office>
<workers>
<worker>PersonA</worker>
<worker>PersonB</worker>
</workers>
</office>
<office>
...
</office>
...
In this case, counter=2.
I am testing via alert(worker(1)) and shows me only PersonA. What can I do to appear on the alert PersonA and PersonB?
Thanks!
The return inside the for loop breaks the loop. Move it outside of the for loop and you are good to go.
Related
I have got 16 divs of the same class name in html document in the following fashion
<div class="game-selection-tab">
<h2>30m + 15s</h2>
</div>
<div class="game-selection-tab">
<h2>60m + 0s</h2>
</div>
<div class="game-selection-tab">
<h2>Custom</h2>
</div>
I want to create onclick method that returns h2 text content from particular div. I tried to solve this by using following javascript code.
var selectionTabs = document.getElementsByClassName("game-selection-tab");
for(var i = 0; i < selectionTabs.length; i++){
var tab = selectionTabs[i];
var content = tab.getElementsByTagName("h2");
tab.onclick = function(){
console.log(content[0].textContent);
}
}
The problem is: no matter which div i click, program always returns h2 text content from the last div(in this example "custom").
Try this
var selectionTabs = document.getElementsByClassName("game-selection-tab");
for(var i = 0; i < selectionTabs.length; i++){
(function (index) {
var tab = selectionTabs[index];
var content = tab.getElementsByTagName("h2");
tab.onclick = function(){
console.log(content[0].textContent);
}
})(i);
}
The thing is by the time your event attaches to the actual DOM element the for loop execution is complete and the value of i is the max value that it can reach. Hence, isolating the same in a function like this works. The function stores the value of i or in this case index as the original value that you expect.
Replace
var i = 0
by
let i = 0
and you're done.
A detailed explanation is here.
I'll quote my answer for your understanding below.
Cause of the problem: lack of understanding scope
Check this example to understand the problem:
var creates function scope
var funcs = []
for (var i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}
funcs.forEach(function(func) {
func()
})
While you might expect this forEach loop to result in number 0 to 9 being printed, instead you get ten times 10. The cause of this is the variable i being declared using var keyword, which creates a function scope that leads to each function in funcs holding a reference to the same i variable. At the time the forEach loop is executed, the previous for-loop has ended and i holds 10 (9++ from the last iteration).
Compare how ES6's let, which creates block scope instead of function scope, behaves in this regard:
let (ES6 or officially ES2015) creates block scope:
var funcs = []
for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}
funcs.forEach(function(func) {
func()
})
Because let creates block scope, each iteration of the for loop has its "own" variable i.
ES5 solution using an IIFE wrapper
If you need an ES5 solution, an IIFE (immediately invoked function expression) wrapper would be the way to go:
var funcs = []
for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value)
}
}(i)))
}
funcs.forEach(function(func) {
func()
})
Here, i is passed as a parameter to each function which stores its own copy value.
The same is true for for..in loops:
var funcs = [],
obj = {
first: "first",
last: "last",
always: "always"
}
for (var key in obj) {
funcs.push(function() {
console.log(key)
})
}
funcs.forEach(function(func) { // outputs: "always", "always", "always"
func()
})
Again, all functions in funcs hold the reference to the same key because var key creates a function scope that lives outside of the for..in loop. And again, let produces the result you'd probably rather expect:
var funcs = [],
obj = {
first: "first",
last: "last",
always: "always"
}
for (let key in obj) {
funcs.push(function() {
console.log(key)
})
}
funcs.forEach(function(func) {
func()
})
Also compare the excellent (!) book
Nicholas C. Zakas: "Understanding ES6", no starch press, p. 8-9.
from which the examples were taken.
It's showing always the same value because you are setting content outside of the onclick function. After the for loop, content points to the last h2.
Move the content definition inside the onclick function.
tab.onclick = function(){
var content = this.getElementsByTagName("h2");
console.log(content[0].textContent);
}
Working fiddle
Can you try the solution below.
var selectionTabs = document.getElementsByClassName("game-selection-tab");
Object.keys(selectionTabs).forEach((data, index) => {
var context = selectionTabs[data].getElementsByTagName("h2")[0].textContent;
selectionTabs[data].onclick = function () {
console.log(context)
}
})
Try this simple solution:
var els = document.getElementsByClassName('game-selection-tab');
var index = 0;
function getText() {
alert(this.innerText || this.textContent);
}
for (; index < els.length; index++) {
els[index].onclick = getText;
}
<div class="game-selection-tab">
<h2>30m + 15s</h2>
</div>
<div class="game-selection-tab">
<h2>60m + 0s</h2>
</div>
<div class="game-selection-tab">
<h2>Custom</h2>
</div>
Assuming that you don't wanna change your HTML to include an "onclick" event on each h2, this code might help you:
document.addEventListener('click', function(e) {
e = e || window.event;
var target = e.target || e.srcElement,
text = target.textContent || text.innerText;
console.log(text);
}, false);
EDIT
If you want to be more specific and get the content from only your h2's, you could use this:
h2s = document.getElementsByTagName('h2');
for (var i = 0; i < h2s.length; i++) {
h2s[i].addEventListener('click', redirect, false);
}
function redirect(e) {
var target = e.target || e.srcElement;
var text = target.textContent || text.innerText;
console.log(text);
}
I have a md-tabs setup, I have it binded to $scope.selectedIndex so that I can change it by code when I need to.
I use a button, call my function that updates data, I then change $scope.selectedIndex to the tab number needed (In this case 3) that will then change the selected tab.
All of that is fine, except it's calling $scope.selectedIndex before .forEach() call is finished, which results in a ng-repeat not working as it exits silently with no errors since its not defined.
Button Calls:
ng-click="initTVShow(show)"
Function:
$scope.initTVShow = function(show) {
var rng = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 10; i++ )
rng += possible.charAt(Math.floor(Math.random() * possible.length));
$scope.show = show;
$scope.show_name = show.title.split(' ').join('') + "-" + rng;
$scope.poster = show.images.poster;
$scope.backdrop = show.images.poster;
$scope.seasons = [{},{}];
getShow(show.imdb_id, function (err, show) {
for (let i = 0; i < 10; i++) {
$scope.seasons[i] = show.episodes.filter((episode) => episode.season === i);
}
});
$scope.selectedIndex=3;
};
As you can see, $scope.selectedIndex=3; basically changes to Tab #4 (0-based).
Then the following ng-repeat does not seem to work and most likely because .forEach hasn't finished yet. I tested with $scope.seasons with 2 empty index's to test and that works.
<span ng-repeat="season in seasons">{{season.number}}</span>
I think that this is because the function getShow retrieves data asynchronously. Show it to us.
Try this:
getShow(show.imdb_id, function (err, show) {
show.episodes.forEach(function(array){
$scope.seasons[array.season] = array;
});
$scope.selectedIndex=3;
});
It's because the data not ready and the DOM is.
you can solve it easily by adding ng-if on the element and check if the data ready.
Like:<span ng-if="season.number" ng-repeat="season in seasons">{{season.number}}</span>
I found the solution, it was actually just caused by the for() loop (Which pre-edit, was a .forEach), I simple added a callback to it, which changed the tab and it just WORKED!
Changed:
for (let i = 0; i < 11; i++) { }
To:
for (let i = 0; i < 11 || function(){ $scope.selectedIndex=3; }(); i++) { }
I got the following code which works perfectly. What it does is: in a table it highlights the corresponding table header cell and table first column cell when you hover over any table cell.
// Row & Column Highlight
(function() {
var gridCellRow = null,
gridCellCol = null,
tableElement = document.getElementsByClassName('inner_table');
for (var i = 0, len_i = tableElement.length; i < len_i; i++) {
if (tableElement[i].getElementsByClassName('row_label_cell').length > 0) {
var gridCell = tableElement[i].getElementsByClassName('input_cell');
for (var j = 0, len_j = gridCell.length; j < len_j; j++) {
function gridCellParents(currentCell) {
return gridCellRow = currentCell.parentNode.firstElementChild,
gridCellCol = currentCell.parentNode.parentNode.rows[0].cells[currentCell.cellIndex];
}
gridCell[j].addEventListener('mouseover', (function() {
gridCellParents(this);
gridCellRow.classList.add('highlight');
gridCellCol.classList.add('highlight');
}));
gridCell[j].addEventListener('mouseout', (function() {
gridCellRow.classList.remove('highlight');
gridCellCol.classList.remove('highlight');
}));
}
}
}
}());
However, JSHint tells me, that
for (var j = 0, len_j = gridCell.length; j < len_j; j++) {
function gridCellParents(currentCell) {
return gridCellRow = currentCell.parentNode.firstElementChild,
gridCellCol = currentCell.parentNode.parentNode.rows[0].cells[currentCell.cellIndex];
}
is not best practice "Function declarations should not be placed in blocks. Use a function expression or move the statement to the top of the outer function."
as well as
gridCell[j].addEventListener('mouseover', (function() {
gridCellParents(this);
gridCellRow.classList.add('highlight');
gridCellCol.classList.add('highlight');
}));
gridCell[j].addEventListener('mouseout', (function() {
gridCellRow.classList.remove('highlight');
gridCellCol.classList.remove('highlight');
}));
}
is not best practice "Don't make functions within a loop."
So how am I correctly and according to best practice building this whole function?
Function deceleration shouldn't be within loops because it makes no
sense to re-create the same function over and over again, in a
"continuous flow" (unlike other situation where the same function
might be created again, in a more complex code). The main reason is
because of hoisting and it strongly goes against javascript
principles to write functions declarations inside loops.
A good starting point, with a more ordered code:
// Row & Column Highlight
(function() {
var gridCellRow,
gridCellCol,
gridCell,
tableElement = document.getElementsByClassName('inner_table');
function gridCellParents(currentCell) {
gridCellRow = currentCell.parentNode.firstElementChild,
gridCellCol = currentCell.parentNode.parentNode.rows[0].cells[currentCell.cellIndex];
}
function onMouseEnter() {
gridCellParents(this);
gridCellRow.classList.add('highlight');
gridCellCol.classList.add('highlight');
}
function onMuoseLeave() {
gridCellRow.classList.remove('highlight');
gridCellCol.classList.remove('highlight');
}
for (var i = 0, len_i = tableElement.length; i < len_i; i++) {
if (tableElement[i].getElementsByClassName('row_label_cell').length > 0) {
gridCell = tableElement[i].getElementsByClassName('input_cell');
for (var j = 0, len_j = gridCell.length; j < len_j; j++) {
gridCell[j].addEventListener('mouseenter', onMouseEnter);
gridCell[j].addEventListener('mouseleave', onMuoseLeave);
}
}
}}());
As you can see, I've modified your events to mousenter and mouseleave which might better suit your needs and be better for overall performance.
Update - delegated version:
// Row & Column Highlight
(function() {
var gridCell,
tableElement = document.querySelectorAll('.inner_table');
function getCellParents(cell){
return {
row : cell.parentNode.firstElementChild, // row
col : cell.parentNode.parentNode.rows[0].cells[cell.cellIndex] // col
};
}
function updateGridCellParents(cell, state) {
state = state ? 'add' : 'remove';
var parents = getCellParents(cell);
parents.row.classList[state]('highlight');
parents.col.classList[state]('highlight');
}
funciton checkTarget(target){
// make sure the element is what we expected it to be
return target.className.indexOf('input_cell') != 0;
}
function onMouseEvents(e){
checkTarget(e.target) && updateGridCellParents(e.target, e.type == "mouseover");
}
document.body.addEventListener('mouseover', onMouseEvents);
document.body.addEventListener('mouseout', onMouseEvents);
})();
In addition to the previous answer, I think it is important to also state why it is a bad practice.
The issue when creating functions inside loops is that they often use values that depends on the loop's iteration. Let's have an example.
// Create three function, that writes their number
var funcs = [];
for(var i=0; i<3; i++){
funcs.push(function(){
document.write(i);
});
}
// Call them.
funcs.forEach(function(f){
f();
});
One may expect the above code to write 1 then 2 then 3. However, because variables in JS are not block-scoped but function-scoped (except for the new let and const), the closure of all three of these functions will actually use the exact same i: 3, the last value it had been given (and thus the value it still has).
Because of this behaviour, this is very easy to make mistakes. Hence, it is not recommended.
If you need to create a function that depends of the value of a loop, you can use a factory.
// Create a factory function that returns a
// function that writes the argument.
function writerFactory(msg){
return function(){
document.write(msg);
}
}
// Create three functions, that write their number.
var funcs = [];
for(var i=0; i<3; i++){
funcs.push(writerFactory(i));
}
// Call them.
funcs.forEach(function(f){
f();
});
This time, each function has a different closure: the one that is created by each call of the factory. They all have access to a different msg.
I have a menu class loading data from a received json file.
in the constructor I build the menu, so I have a for loop with this (extracted part) js:
for (var i = 0; i<data.length; i++)
{
var btn = $('<div>'+data[i].label+'</div>').appendTo(object.container);
btn.click(function()
{
if($('.blockingFrame').length == 0)//pas de blocking
{
data[i].action()
}
});
}
Now, obviously this doesn't work because on runtime data[i] doesn't exist anymore...
data[i].action contains a valid js function.
This works, but doesn't contains the condition..:
for (var i = 0; i<data.length; i++)
{
var btn = $('<div>'+data[i].label+'</div>').appendTo(object.container);
btn.click(data[i].action);
}
So I thought I could store this action inside the jquery object and call it like this, but it doesn't work:
for (var i = 0; i<data.length; i++)
{
var btn = $('<div>'+data[i].label+'</div>').appendTo(object.container);
btn.action = data[i].action;
btn.click(function()
{
if($('.blockingFrame').length == 0)//pas de blocking
{
$(this).action();
}
});
}
A partial solution I came u with, was to store the action in another event, like dblclick, and trigger a dblclick inside the condition, but this seams ugly.
Any idea how to do this?
for loops don't work properly with closures. Consider using iterator methods instead:
$.each(data, function(index, elem) {
var btn = $('<div>'+elem.label+'</div>').appendTo(object.container);
btn.click(function()
{
if($('.blockingFrame').length == 0)//pas de blocking
{
elem.action()
}
});
}
Iterators are usually more elegant and compact than for loops, especially when you have them nested.
The reason why your last snippet doesn't work if that btn = $(...) is a temporary jquery object and disappears once you leave the scope, with everything you have assigned to it. When later a click handler is being invoked, you create a new jquery object via $(this), which doesn't carry your changes from the previous step. If you want to keep any data attached permanently to an element, use the data method - but in this case there's no need for that.
Use an immediately-executing function to create a closure that holds i.
for (var i = 0; i<data.length; i++) {
var btn = $('<div>'+data[i].label+'</div>').appendTo(object.container);
btn.click(function(i) {
return function() {
if($('.blockingFrame').length == 0)//pas de blocking {
data[i].action();
}
}(i));
}
I would like to have some variables that my for loop uses inside a function scope (not global).
I tried to wrap the for loop inside a function like this but it results in console error:
function() {
var data = livingroomTableData;
for(var i = data[0]; i < data[1]; i++) {
var elemvalue = data[2] + format(i) + ".png";
livingroomTableArray[i] = elemvalue;
}
}
I would like the data variable to have the values of livingroomTableData only inside this for loop (not globally). In other loops I will input a different variable into the data variable.
Oh yes, and as you can probably tell, I'm a total newbie. :S
There is only function scope in javascript, block scope does not exist, so you can't let the variable only inside the for loop. What you could do is to create a function scope.
Code example:
(function(livingroomTableData) {
var data = livingroomTableData;
//... the rest code
})(livingroomTableData);
The big problem is this line:
for(var i = data[0]; i < data[1]; i++) {
That means, starting with i as the first element of the array, do the code in the loop, incrementing i by one at the end of each run until i is not less than the second element of data.
I'd rewrite it to show you a working version, but its not clear what you actually want to do.
function() {
for(var i = 0; i < livingroomTableData.length; i++) {
var data = livingroomTableData[i];
//your code here...
}
}