Flama
Flama

Reputation: 868

Listener not being added to a creted div

So I have the following function called addPanels() which adds an event listener to every element with class "accordion"

function addPanels(){
    var acc = document.getElementsByClassName("accordion");
    var i;

    for (i = 0; i < acc.length; i++) {
        acc[i].addEventListener("click", function() {
        var panel = this.nextElementSibling;
        if (panel.style.display === "block") {
            panel.style.display = "none";
        } else {
            panel.style.display = "block";
        }

        var arrow = this.children[0].children[0];

        if (panel.style.display === "block") {
            arrow.src = "Iconos/arrow_up.png";
        } else {
            arrow.src = "Iconos/arrow_down.png";
        }
    });
  } 
}

Then I have this:

$(document).ready(function() {
    api.routines.getRoutines().done(function(data) {
       $.each(data, function(i, item){
       var list = document.getElementById("routines_list");
       var acc = document.createElement("div");
       acc.setAttribute("class", "accordion");
       var panel = document.createElement("div");
       panel.setAttribute("class", "panel");
        //more stuff 
      });
    acc.appendChild(panel);
    list.appendChild(acc);  
    // ...
    });
    addPanels(); //WORKS ONLY WITH PREVIOUS ACCORDIONS NOT WITH THE NEW ONE! WHY??

}).fail(function(jqXHR, textStatus, errorThrown) {
    //do sth
    });
});

addPanels seems to add the listener ONLY to the elements with class ACCORDION that weren't loaded from API (they were already in the HTML). Why is this happening?

Upvotes: 1

Views: 45

Answers (1)

Scott Marcus
Scott Marcus

Reputation: 65825

Because at the time the listeners were added, your new elements didn't exist.

api.routines.getRoutines().done specifies a callback function that should run at some future point in time - we don't know when that will be. So, while that API goes out to begin its process, addPanels(); is immediately called next, which sets up the event handlers, but getRoutines() hasn't completed yet, so you wind up only setting up the handlers on the panels that exist before getRoutines() is finished. This is how asynchronous processing works.

To solve a problem like this, you need to use event delegation, which is when you set up the event handler on an object that exists right from the start and then you capture events that were initiated by descendant elements via event bubbling. When the event is handled, you check the source of the event and see if it matches the criteria you want. This way, it doesn't matter when the element gets created.

Here's a simplified example:

// Set up a click event handler on the document (which all descendant element events will bubble up to)
document.addEventListener("click", function(evt){
  console.log("Click event handled!");
 
  // Is the clicked element NOT the document?
  if(evt.target.classList){
    // Check for dynamically created elements that were given
    // the "dynamic" class upon creation (you can use any criteria you want here)
    if(evt.target.classList.contains("dynamic")){
      console.log("Click event for dynamically created element, " + evt.target.nodeName + " handled by document");
    }
  }
});

// Now, we'll create some elements and inject them into the DOM AFTER the event handler
// has been set up. Also, these elements were not specifically set up with event handlers.
var div = document.createElement("div");
div.classList.add("dynamic");
div.textContent = "Click Me";
document.body.appendChild(div);

var btn = document.createElement("input");
btn.classList.add("dynamic");
btn.type = "button";
btn.value = "Click Me!";
document.body.appendChild(btn);
.dynamic { font-weight:bold; color:#800080; }

Upvotes: 1

Related Questions