Sliding accordion with pure JS and for loop - javascript

I've written a for loop, which should run through all accordions and their children, but I can't figure out why it's only working on the first object.
Fiddle Example
for (
var i = 0,
accordion = document.getElementsByClassName('accordion');
i < accordion.length;
i++
) {
var accordion_section = accordion[i].children[i],
accordion_key = accordion[i].children[i].children[0],
accordion_bellow = accordion[i].children[i].children[1];
function accordion_bellow_MarginTop( value ) {
accordion_bellow.style.marginTop = value + 'px';
}
accordion_bellow_MarginTop( -accordion_bellow.offsetHeight );
accordion_key.onclick = function() {
if ( accordion_section.getAttribute('class' ) == 'active' ) {
accordion_section.setAttribute('class', '');
accordion_bellow_MarginTop( -accordion_bellow.offsetHeight );
}
else {
accordion_section.setAttribute('class', 'active');
accordion_bellow_MarginTop( 0 );
}
return false;
}
}

There are a couple of issues at play here. As previous commenters noted, you are not properly looping over each of the sections within your accordion. After fixing that, you will also need to address the fact that your onClick handler will not work correctly.
The problem with looping over each section is that you are using improper variable scoping. What happens is only the last element you loop over will be affected by the click handler. This is because the variables "accordion_section" and "accordion_bellow" will always reference the last set of elements in your main for loop.
This is contrary to the expectation that they will be the unique element assigned during the loop. The reason for this is because the variables "accordion_section" and "accordion_bellow" are defined outside the scope of the onClick function. In order for your onClick to work, those variables need to be defined within a separate scope during each iteration of your for loop.
In order to do this, you can use an anonymous function like this:
for (var i = 0; i < sections.length; i++)
{
(function() {
var section = sections.item(i),
anchor = sections.item(i).children[0],
below = sections.item(i).children[1];
closeBelow(below, -below.offsetHeight);
anchor.onclick = function () {
if (section.getAttribute('class' ) == 'active' ) {
section.setAttribute('class', '');
closeBelow(below);
}
else {
section.setAttribute('class', 'active');
openBelow(below);
}
}
})();
}
In this solution, the variables "section", "anchor", and "below" are always specific to the elements you are looping over. By using a self-executing function, you ensure that each click handler only works with locally scoped variables.
Solution: http://jsfiddle.net/b0u916p4/4/

you need to make another loop inside first
this way you get all accordion and all sections of each
try this:
for (i = 0,
accordion = document.getElementsByClassName('accordion');
i < accordion.length;
i++) {
for (j = 0;
j <= accordion[i].children.length;
j++) {
//your code here
}
}

Related

JavaScript addEventListener with Event and Binding a variable

I'm busy trying to dynamically assign functions to certain buttons and I've run into a strange problem that I'm absolutely stumped with.
I have the following simple HTML for demonstration purposes
<div id="butts">
<button>BUTT 01</button>
<button>BUTT 02</button>
</div>
Now I am assigning functions to these buttons using JavaScript with the following loop (including the event)
var butts = $("#butts").find("button");
for(var cnt = 0; cnt < butts.length; cnt ++) {
// get button description just for testing
var buttonDesc = $(butts[cnt]).text();
// EVENT
butts[cnt].addEventListener (
"click", function(event) {
funcEvent(event)
}
);
}
Calling a very simple test function to verify that it is working
function funcEvent(event) {
console.log("funcEvent");
console.log(event);
}
This is working fine but I also need to pass a variable to the function which I would normally do as follows
var butts = $("#butts").find("button");
for(var cnt = 0; cnt < butts.length; cnt ++) {
// get button description just for testing
var buttonDesc = $(butts[cnt]).text();
// BIND
butts[cnt].addEventListener (
"click", funcBind.bind(this, buttonDesc)
);
}
Another very simple test function
function funcBind(buttonDesc) {
console.log("funcBind");
console.log(buttonDesc);
}
Separately they both work just fine but I am struggling to pass the event argument in the bind function
I am trying to combine the two so that I can call a single function that can receive both the event and the argument
UPDATE
This seems to be a possible fix although I do not understand how to be honest
With the same loop
var butts = $("#butts").find("button");
for(var cnt = 0; cnt < butts.length; cnt ++) {
// get button description just for testing
// using var did not work (always last element of array)
// var buttonDesc = $(butts[cnt]).text();
let buttonDesc = $(butts[cnt]).text();
// EVENT
butts[cnt].addEventListener (
"click", function(event) {
funcEventBind(event, buttonDesc);
}
);
}
Calling a very simple test function to verify that it is working
function funcBindEvent(event, buttonDesc) {
console.log("funcEvent");
console.log(event);
console.log(buttonDesc);
}
You need to create a closure so that the even handler callback contain the context. You can do that by using forEach like this
butts.forEach(function(bt) {
var buttonDesc = $(bt).text();
// BIND
bt.addEventListener (
"click", function(event){
funcBind(event, buttonDesc)
}
);
})

EventListener doesn't work on dynamic elements added

I have checked another questions like this one on stackoverflow, but it doesn't solved my problem.
My problem is that whenever I add events to dynamic added elements. but it doesn't work in case I click on that element. It means that it doesn't work properly.
Here is what I have implemented:
function init() {
let promptMessage = 'Enter the number of rows and columns'
let promptDefault = '1 1';
let prompt = this.prompt(promptMessage, promptDefault);
if (prompt) {
prompt = prompt.split(" ")
console.log(prompt);
prompt[0] = Number(prompt[0]);
prompt[1] = Number(prompt[1]);
return prompt;
}
return init()
}
function selected(i, j) {
return console.log(`You clicked on row ${i} and column ${j}`)
}
var gameSize = init(),
rows = gameSize[0],
cols = gameSize[1],
boardGame = document.querySelector('.board');
for (var i = 0; i < rows; i++) {
for (var j = 0; j < cols; j++) {
let div = document.createElement('div');
div.className = `block cell-${i}-${j}`;
div.addEventListener("click", selected(i, j));
boardGame.appendChild(div);
}
}
Problem: I expect that after entering numbers in prompt whenever I inspect the document see onclick="selected(i, j)" for each of elements. But it doesn't work so. Since the browser render the html file, it console.log all the elements, in case I didn't click on them. Where is the problem?
From what I see, div.addEventListener("click", selected(i, j)); is the problematic line.
You're invoking the function selected here (this is why you see the logging on load) and assigning its value as the handler (which in this case is undefined). You're actually intending to assign the function.
Change
function selected(i, j) {
return console.log(`You clicked on row ${i} and column ${j}`)
}
to
function selected(i, j) {
return function(){
console.log(`You clicked on row ${i} and column ${j}`);
}
}
This use of higher order functions lets you close over i and j to build a function that, once invoked, logs the appropriate values
You did bind the function wrong, it is executed the instantly, and the return value (null) is now your event listener. You need to bind it like a function.
ES5
div.addEventListener("click", function(){ selected(i, j) });
ES6
div.addEventListener("click", _ => selected(i, j));
You have another bug as well if you use the variables in the bind function as I showed, when you use var in your loop, the last value will the executed value when the event listener is executed, that bug can be solved if let is used instead
As sunrize920 and Benjaco wrote, you have you eventListener wrong. What you probably need is a closure, like this:
div.addEventListener("click", function() {
return selected(i, j)
});

Simple count function does not work

I thought making a simple function where if you click on a button a number will show up inside of a paragraph. And if you continue to click on the button the number inside the paragraph tag will increase. However, I'm getting an error message saying that getElementsByTagName is not a function. Here is the code on jsfiddle, I know there is something simple that I'm doing wrong but I don't know what it is.
HTML
<div class="resist" id="ex1"><h2>Sleep</h2><p></p><button>Resist</button></div>
<div class="resist" id="ex2"><h2>Eat</h2><p></p><button>Resist</button></div>
Javascript
var count = 0;
var resist = document.getElementsByClassName('resist') ;
for(var i = 0; i < resist.length; i++)
{ var a = resist[i];
a.querySelector('button').addEventListener('click', function(a){
count +=1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
}
You are overwriting a variable with event object passed into event handler. Change the name to e maybe, or remove it altogether as you are not using it anyway:
a.querySelector('button').addEventListener('click', function(e /* <--- this guy */) {
count += 1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
Another problem you are going to have is classical closure-in-loop issue. One of the solutions would be to use Array.prototype.forEach instead of for loop:
var count = 0;
var resist = Array.prototype.slice.call(document.getElementsByClassName('resist'));
// ES6: var resist = Array.from(document.getElementsByClassName('resist'));
resist.forEach(function(a) {
a.querySelector('button').addEventListener('click', function(e) {
count += 1;
a.getElementsByTagName('p')[0].innerHTML = count;
});
});
vars in Javascript are function scoped, so you must wrap your event listener binding in a closure function to ensure the variable you're trying to update is correctly set.
(Note: I've renamed a to div in the outer function and removed the arg from the inner click function).
var count = 0;
var resist = document.getElementsByClassName('resist') ;
var div;
for(var i = 0; i < resist.length; i++)
{
div = resist[i];
(function(div){
div.querySelector('button').addEventListener('click', function(){
count +=1;
div.getElementsByTagName('p')[0].innerHTML = count;
});
})(div);
}

java script last iteration set the value for all iterations

var myElements = document.getElementsByName('bb1');
for (var i = 0; i < myElements.length; i++) {
var curValue = myElements[i].getAttribute('innerId')
myElements[i].addEventListener('mouseover', function () {
alert('Hello i am : ' + curValue);
}, false);
}
when mouse over, every element, instead of showing a different value for curValue, a constant value (the last iteration value) is displayed.
what am i doing wrong here?
There is no different scope inside blocks like for in JavaScript, so when your mouseover event is triggered, it will alert the current variable value which was set in the last iteration.
You can just use this inside your callback function to get the attribute of the object which the event was triggered.
var myElements = document.getElementsByName('bb1');
for (var i = 0; i < myElements.length; i++) {
myElements[i].addEventListener('mouseover', function () {
alert('Hello i am : ' + this.getAttribute('innerId'));
}, false);
}
The general issue here is the closure in Javascript. This happens when using variable (in this case curValue) not defined within the callback function.
I recommend reading answers about JS closures here.

Scope troubles in Javascript when passing an anonymous function to a named function with a local variable

Sorry about the title - I couldn't figure out a way to phrase it.
Here's the scenario:
I have a function that builds a element:
buildSelect(id,cbFunc,...)
Inside buildSelect it does this:
select.attachEvent('onchange',cbFunc);
I also have an array that goes:
var xs = ['x1','x2','x3'...];
Given all of these, I have some code that does this:
for(var i = 0; i < xs.length; i++)
{
buildSelect(blah,function(){ CallBack(xs[i],...) },...);
}
The issue is that when onchange gets fired on one of those selects it correctly goes to CallBack() but the first parameter is incorrect. For example if I change the third select I expect CallBack() to be called with xs[2] instead I get some varying things like xs[3] or something else.
If I modify it slightly to this:
for(var i = 0; i < xs.length; i++)
{
var xm = xs[i];
buildSelect(blah,function(){ CallBack(xm,...) },...);
}
I'm still getting incorrect values in CallBack(). Something tells me this is scope/closure related but I can't seem to figure out what.
I simply want the first select to call CallBack for onchange with the first parameter as xs[0], the second select with xs[1] and so on. What could I be doing wrong here?
I should clarify that xs is a global variable.
Thanks
You need to capture that xm value by closing around it in its own scope.
To do this requires a separate function call:
buildCallback( curr_xm ) {
// this function will refer to the `xm` member passed in
return function(){ CallBack(curr_xm,...) },...);
}
for(var i = 0; i < xs.length; i++)
{
var xm = xs[ i ];
buildSelect(blah,buildCallback( xm ),...);
}
Now the xm that the callback refers to is the one that you passed to buildCallback.
If you have other uses for i that need to be retained, you could send that instead:
buildCallback( curr_i ) {
// this function will refer to the `i` value passed in
return function(){ CallBack( xs[ curr_i ],...) },...);
}
for(var i = 0; i < xs.length; i++)
{
buildSelect(blah,buildCallback( i ),...);
}
The problem is indeed scope-related -- JavaScript has only function scope, not block scope or loop scope. There is only a single instance of the variables i and xm, and the value of these variables changes as the loop progresses. When the loop is done, you're left with only the last value that they held. Your anonymous functions capture the variables themselves, not their values.
To capture the actual value of a variable, you need another function where you can capture the local variable:
function makeCallback(value) {
return function() { CallBack(value, ...) };
}
Each call to makeCallback gets a new instance of the value variable and if you capture this variable, you essentially capture the value:
for(var i = 0; i < xs.length; i++)
{
buildSelect(blah,makeCallback(xs[i]),...);
}
Yes, I think a closure would help:
for(var i = 0, l = xs.length; i < l; i++)
{
buildSelect(
blah,
function(xm){
return function(){
CallBack(xm,...)
};
}(xs[i]),
...
);
}
Edit: I also optimised your for loop slightly.
Edit: I guess I'll add an explanation. What you're doing is creating an anonymous function which takes one argument (xm) and calling the function straight away (with the parenthesis right after). This anonymous function must also return your original function as an argument of buildSelect().
Apparently there is a new let keyword that does what you want:
for(var i = 0; i < xs.length; i++)
{
let xm = xs[i];
buildSelect(blah,function(){ CallBack(xm,...) },...);
}

Categories

Resources