Using your technique, you can only add event listeners to elements that exist at the time you execute the loop. Instead, set up a single listener on an ancestor of the current and future elements and leverage event bubbling so that you can implement "event delegation", where you let the events bubble up to the common ancestor and handle them there.
Not only does this approach solve your problem, but it avoids a loop entirely and only sets up one event listener instead of many (saving resources).
// In this example, the <ul> always exists and will
// be the common ancestor that dynamically created
// elements will have
const list = document.querySelector("ul");
// Make some new elements upon clicking the button:
document.querySelector("button").addEventListener("click", function(){
const bullet = document.createElement("li");
bullet.textContent = "Dynamically Created Element (click me)";
bullet.classList.add("bullet");
list.appendChild(bullet);
});
list.addEventListener("click", function(event){
// All DOM event handlers are automatically
// passed a reference to the event object that
// they are responding to. That object provides
// a .target property that references the element
// that triggered the event
// Determine the source of the event
if(event.target.classList.contains("bullet")){
console.log("You clicked a dynamically created element.");
}
});
.bullet { color:blue; pointer:cursor; }
<button>Click to add new bullet</button>
<ul></ul>