Add event to multiple elements without being inside a for loop

Asked

Viewed 3,350 times

7

I wonder how you would add an event click to all links with the same class without using jQuery, and without this event click is inserted within a loop for.

The reason is this: don’t use functions Inside to loop; from what I’ve seen on the web outside, doing a function inside a loop is bad practice, plus it can cause some performance problems.

How then?

  • 2

    On the link you passed you have a solution to your problem....

  • but I would like to know of other alternatives to this link.

  • @user3632930, I believe you misunderstood, the problem is not using a loop to associate a function to click, but rather instantiating a function within a loop.

  • @Tobymosque ah yes, so I saw instantiating a function within a loop can cause this function to be instantiated several times correct?

  • yes, that’s right, instantiating inside the for, you will have a different function for each element, if you instantiate outside the for, you will have the same function for all elements.

3 answers

10


making a function within a loop is bad practice, plus it can cause some performance problems

There is some truth in these assumptions, but there is also a very big misunderstanding. You need to understand what happens when functions are created inside a loop, to be able to judge when to use it and when not to use it.

It is even necessary to create functions within the loop?

This is the first question you should ask yourself. It’s not that it’s bad practice or that it’s necessarily going to cause performance problems. First of all, it may simply be unnecessary.

Should the behavior of all its elements be exactly the same? It is possible to write a single function that serves to handle the event of all of them, without using anything that depends on the loop (the i)? If it is, you don’t really need to create multiple functions. This would be a waste of resources, since each function created will occupy memory (this one of those "performance problems" that speak so much, and it may be irrelevant if the amount of elements receiving the click function is not too large).

There are two ways to use the same function for association of events such as click to various elements:

1. Associate the function to each element individually

This is quite simple to do, you use a loop only for association, but the function is declared out of it:

// Função que alerta o ID da div clicada
function divClicada() {
    // dentro de um event listener, this é o elemento
    // associado ao listener
    alert('A div #' + this.id + ' foi clicada!');
}

// Todas as divs da página
var divs = document.querySelectorAll('div');

// Associação de cada div à função
for(var i=0; i<divs.length; i++) {
    divs[i].onclick = divClicada; // Sem parênteses!
}

But this can still make the page heavy if there are too many elements in the loop. Even if you create a single function, you still need to associate the elements with it one by one, so you can still suffer from the excess of associations, even these associations pointing to the same fate. There is a way to avoid this, which is preferable when dealing with many elements.

2. Delegation of events

In the DOM certain events propagate ("bubble") through the hierarchy of objects. Consider the following structure:

<div id="avo">
    <div id="pai">
        <div id="filho"></div>
    </div>
</div>

A click on the element #filho can be captured in any of the 3 Divs of the example, as the event goes up the tree until it reaches the top (until it reaches the element window):

document.getElementById('avo').onclick = function(e) {
    alert(e.target.id);  
};

Demo

Look at the e the function receives (no need to call e, you can use any valid name). It is passed by the browser itself, and represents the event. The property target represents the element that originated the event. With this it is then possible to listen to the click on several elements, creating a single association between an ancestor of it in the hierarchy and the function that treats the event.

This technique is called delegation of events, and is widely used when content is inserted dynamically on the page (for example via AJAX). This is because to associate a Istener to an element, it needs to exist at the time of association. Imagine a div with 3 others inside, each of the 3 with its own onclick. If you exchange the contents of the div (for example, update a list of recent news), the original onclicks are lost. But if you have a single Listener of click delegated to the outside element, that same function continues to serve. This is very useful and very practical.

Yes, it is necessary to create functions within the loop. And now?

Despite the many cases that can be avoided, sometimes it is necessary to create functions within a loop. However, this may not work as you expect. Consider the 3 inputs below:

<input value="valor original 1" />
<input value="valor original 2" />
<input value="valor original 3" />

I want to create a Library for the event blur these inputs, so that when they lose focus, they restore their original values. There is how to do this with a single function, but consider that this option does not exist. A naive approach could be this:

var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
    inputs[i].onblur = function() {
        this.value = inputs[i].value; 
    }
}

Demo

Only this code does not work. If you look at the browser console, you will see an error like this at the time of the Blur:

Uncaught Typeerror: Cannot read Property 'value' of Undefined

That means at the time of Blur inputs[i] did not point to the input that just lost focus; instead it contained the value undefined. This occurs as a matter of scope: all 3 functions are created in the same scope, and point to the same variable i. The value of i at the time of Blur (that is, after the end of the loop), it will always be 3. And inputs[3] does not exist, since in our example there are only 3 inputs (with indexes of 0 to 2). Therefore, the value of inputs[i] at that moment is undefined.

If you read the contents of the last link, you should already know what the solution is for this: to introduce another function, called from within the loop and to capture the current value of i every step. There are two ways to do this:

1. Creating a function that returns another

function criaListener(indiceDoElemento) {
    return function() {
        this.value = inputs[indiceDoElemento].value; 
    }
}
var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
    inputs[i].onblur = criaListener(i);
}

This is quite clean and readable. But there are times when we want to solve everything within the same loop.

2. Using a function immediately invoked (IIFE)

The code below works like the previous example, but creates and immediately executes a function within the same loop:

var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
    inputs[i].onblur = (function(indiceDoElemento) {
        return function() {
            this.value = inputs[indiceDoElemento].value; 
        }
    }(i));
}

It is also possible to avoid function return, as seen in the second example of reply by Edgar Muniz Berlinck:

var inputs = document.querySelectorAll('input');
for(var i=0; i<inputs.length; i++) {
    (function(indiceDoElemento) {
        inputs[i].onblur = function() {
            this.value = inputs[indiceDoElemento].value; 
        }
    }(i));
}

4

You can use the native Javascript function getElementsByClassName, for example:

for(var elem in document.getElementsByClassName('BUTTON')) {
  elem.click = function () {
    //seu codigo aqui
  };
}

The object document has other getElement(s)By, as per Name, TagName, Id and TagNameNS, the latter needs a namespace next to tag name.

I believe that the JS code is not 100%, but the idea in general is correct.

But in this way done above, we are creating several copies of the function, which is bad for memory. So the best thing to do is to create the function outside the loop, and assign it to the click, thus:

var metodo = function () {
  //Seu codigo aqui
};

for(var elem in document.getElementsByClassName('PLACEHOLDER')) {
  elem.click = metodo;
}
  • I think I fixed it now. @bfavaretto

4

The alternative passed by the link is already the best alternative, which is to declare a function

function click(param) { ... }

and pass on their reference to the desired elements:

for (var i=0; i<Elementos.lenght; i++) { Elementos[i].addEventListener("click", click) };

What is interesting to explain is why this solution is the best: It uses only a single instance of the function click. On the other hand, when we do so (Code extracted from the link):

var elems = document.getElementsByClassName("myClass"), i;

for (i = 0; i < elems.length; i++) {
    (function (iCopy) {
        "use strict";
        elems[i].addEventListener("click", function () {
            this.innerHTML = iCopy;
        });
    }(i));
}

We create N instances of the same function, which makes no sense since the behavior is the same and we will be using much more memory atoa.

  • Only to render the answer more visual: http://jsfiddle.net/gadqvw0q/

  • thanks for the explanation

Browser other questions tagged

You are not signed in. Login or sign up in order to post.