0

I'm trying to add an eventlistener for a button collection inside of a for loop, but I'm kind of lost because in following example the eventlistener only works for the last button in the collection, and my country in addCountry function is undefined. I would appreciate any kind of feedback or help.

I have tried to console.log(i) and obviously it is working as it should. And I have console.log('.add'[i]) and that is displaying the button element so I don't understand how this works really.

let countries = ["Sweden", "Norway", "Denmark", ]

const main = document.getElementById("maincontent")
const cart = document.getElementById('cart')

for (i = 0; i < countries.length; i++) {

  addContent()

  document.querySelectorAll('.add')[i].addEventListener('click', addCountry)
}

function addContent() {
  main.innerHTML += `<div>
  <h1>${countries[i]}</h1>
  <div class="one hide">
      <p>Some text</p>
      <button class="add">Add</button>
  </div>
  </div>`
}

function addCountry() {
  cart.innerHTML = `<div>
<h1>${countries[i]}</h1>
<div class="one hide">
    <p>Some text</p>
    <button class="delete">Delete</button>
</div>
</div>`
}
<main id="maincontent">

</main>
<div id="cart">

</div>
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Gips1
  • 5
  • 5
  • `countries` is undefined – Chris G May 09 '23 at 17:57
  • I edited that now, missed to paste it. – Gips1 May 09 '23 at 17:58
  • Well, your loop iterates over all the elements to add an event listener. The click happens after the loop has ran, after which `i` has the value of `countries.length`. – Ivar May 09 '23 at 18:00
  • Where is `i` coming from in `addContent` and `addCountry`? – Konrad May 09 '23 at 18:00
  • .innerHTML will remove all of the event handler – epascarello May 09 '23 at 18:11
  • Okey but what i dont understand is how can addContent function work? Without i as an argument? And @Ivar do you mean all my button elements? When i have: document.querySelectorAll('.add')[i] how come that it dont iterates through all buttons one by one? And what would be the solution? Thank you for the reply. – Gips1 May 09 '23 at 18:12
  • @epascarello how come? And what would i replace that with instead? – Gips1 May 09 '23 at 18:13
  • how come? becuase innerHTML is like erasing the whiteboard and adding it all back. The events are not stored. You are creating new elements that look like the old ones. – epascarello May 09 '23 at 18:30

4 Answers4

2

You can't use i in addCountry. It's a global variable, not closed over the loop iteration.

You need to put a closure into the loop, and then pass i as an argument to addCountry().

let countries = ["Sweden", "Norway", "Denmark", ]

const main = document.getElementById("maincontent")
const cart = document.getElementById('cart')

for (let i = 0; i < countries.length; i++) {
  addContent(i)
  document.querySelectorAll('.add')[i].addEventListener('click', () => addCountry(i))
}

function addContent(i){
  main.innerHTML+=`<div>
  <h1>${countries[i]}</h1>
  <div class="one hide">
      <p>Some text</p>
      <button class="add">Add</button>
  </div>
  </div>`
}
function addCountry(i) {
  cart.innerHTML = `<div>
<h1>${countries[i]}</h1>
<div class="one hide">
    <p>Some text</p>
    <button class="delete">Delete</button>
</div>
</div>`
}
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • I did tried that but it didnt work? And why did the addContent function in that case worked? It alsow had i variable? – Gips1 May 09 '23 at 18:06
  • @Gips1 It will not work. Because still there is [i] so it will bind to specific only. And secondly there is no need to put that line inside the for loop. – Developer May 09 '23 at 18:42
  • @Gips1 Did you remember to use `let i` in the `for` loop so you'll get a new binding each iteration? See https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – Barmar May 09 '23 at 19:35
  • `addContent` works because it's running immediately while the loop is running, so it can use the current value of the global variable. But `addCountry` is called later, when the user clicks, and `i` will have a different value. – Barmar May 09 '23 at 19:37
0

Using event delegation and data attributes you can avoid having to add multiple event handlers.

let countries = ["Sweden", "Norway", "Denmark", ]

const main = document.getElementById("maincontent")
const cart = document.getElementById('cart')

const html = countries.map(addContent);
main.innerHTML = html.join('');

main.addEventListener("click", function (e) {
  const btn = e.target.closest('[data-country]');
  if (!btn) return;
  const country = btn.dataset.country;
  addCountry(country);
});

cart.addEventListener("click", function (e) {
  const btn = e.target.closest('.delete');
  if (!btn) return;
  btn.closest(".wrapper").remove();
});


function addContent(country) {
  return `
  <div>
    <h1>${country}</h1>
    <div class="one hide">
      <p>Some text</p>
      <button class="add" data-country="${country}">Add</button>
    </div>
  </div>`;
}

function addCountry(country) {
  cart.innerHTML += `
  <div class="wrapper">
    <h1>${country}</h1>
    <div class="one hide">
      <p>Some text</p>
      <button class="delete">Delete</button>
    </div>
  </div>`;
}
<main id="maincontent"></main>
<div id="cart"></div>
epascarello
  • 204,599
  • 20
  • 195
  • 236
  • Thank you, i think this is in the direction of what im looking for, i got som reading to do! – Gips1 May 10 '23 at 10:17
0

As .add buttons are created after for loop. So, you can bind the event after that.

Now logically, the number of add button must be equal to the number of countries. So, we can use index of foreach loop also.

Here is the complete code:-

let countries = ["Sweden", "Norway", "Denmark", ]

const main = document.getElementById("maincontent")
const cart = document.getElementById('cart')

for(i = 0; i < countries.length; i++) {
  addContent(i);
}

document.querySelectorAll('.add').forEach((item, i)=>{
   item.addEventListener('click', function(e){
     addCountry(i);
   });
});

function addContent(i) {
  main.innerHTML += `<div>
  <h1>${countries[i]}</h1>
  <div class="one hide">
      <p>Some text</p>
      <button class="add">Add</button>
  </div>
  </div>`
}

function addCountry(i) {
  cart.innerHTML = `<div>
<h1>${countries[i]}</h1>
<div class="one hide">
    <p>Some text</p>
    <button class="delete">Delete</button>
</div>
</div>`
}
<main id="maincontent"></main>
<div id="cart"></div>

If you are not sure about the order of the code. Then I would say use createElement to create Element dynamically and it will store in the memory. And inside the addContent function, just append them. And I would say that if you can implement createElement approach it will be much better.

Developer
  • 1,297
  • 1
  • 4
  • 15
  • Thank you for the reply. But isnt that creating multiple eventlisteners? Its gonna make event listeners for each item in .add as long as i < countries.length i.e 3 times in this example? I could be way of here but im thinking out loud. @Developer – Gips1 May 10 '23 at 10:32
  • @Gips1 I think that's the thing you were asking. You were saying that event listener on collection of buttons. Can you explain what you need? – Developer May 10 '23 at 10:57
  • Yes that is what i want. But from the example above, isnt it multiple event listeners for the same collection? First you have addContent() that runs 3 times and creates 3 buttons, and then you have the .foreach that also is run 3 times? I dont know how this is executed really, but if it runs .foreach every time isnt that mean it creates eventlisteners for same buttons twice? Like i said i dont really understand how this is executed really. @Developer – Gips1 May 10 '23 at 11:40
  • @Gips1 I have update the code. It will work. – Developer May 10 '23 at 12:19
  • Okey yes many thanks. I did have that in mind i just wanted to know if it was possible to do the way i intended it and sort of get feedback on wich way is THE way to approach a situation like this, and this was very helpful, thank you. @Developer – Gips1 May 10 '23 at 12:33
  • But hold on, i have a question on your latest example. When i run addeventlistener .foreach, outside the for loop, i dont have the variable i counting, and therefore my addCountry function wont work? Country[i] will be undefined? @Developer – Gips1 May 10 '23 at 12:35
  • @Gips1 Exactly. That was your previous code and that's why I add the foreach loop inisde the for because we need that variable i – Developer May 10 '23 at 12:38
  • @Gips1 Listen I have an idea. Try this. I am updating the code – Developer May 10 '23 at 12:39
  • @Gips1 I have updated the code. Try now. This is what you need – Developer May 10 '23 at 12:45
  • In the latest example, addCountry() runs without clicking on add button, and i variable is 2? And the eventlistener wont work? Since the for loop is done so is counting i and therefore it is static on 2? So it wont ececute all? But i dont understand why it runs without me clicking on add button? @Developer – Gips1 May 10 '23 at 12:55
  • @Gips1 Do you want to run the addCountry() function on page load?? – Developer May 10 '23 at 12:57
  • No i want to run addCountry when i press add button? Or what do you mean im not following, sry bare with me im newbie. @Developer – Gips1 May 10 '23 at 13:02
  • @Gips1 No worries. One minute, I am updating. – Developer May 10 '23 at 13:03
  • @Gips1 Updated the code. I hope now you get your desired results. – Developer May 10 '23 at 13:14
  • 1
    That worked, much appriciate. I have a few questions thoug since i am a newbie and not really sure what is what here and its working. First of what is item in this context, and second what is function(e) i ran it without the e and it still works, so why do i need it and what is it? Thanks a lot for the help! @Developer – Gips1 May 10 '23 at 15:07
  • @Gips1 "e" is event object and it has lot of details like which element trigger the event etc. And yes without e, still it will run. And this e is present in all event handler functions. If you need it you can just write e or event or anything in the parameter and you can use it. And in your case, we don't need that, you can remove it. – Developer May 10 '23 at 18:25
  • @Gips1 as we select all buttons that has class of add. So when we select all , then there is a collection of buttons. So we are using foreach loop to iterate over all items one by one. So in every iteration, item is referred to a button. So on first iteration of foreach loop the item refers to first button and then on second iteration, the item is referred to second button and so on. As there are only three buttons in the start, so foreach loop iterate three times. And you can name it anything. I just name it item. You can name as you wish. I hope you understand it. – Developer May 10 '23 at 18:29
  • @Gips1 And mark the answer correct so that it visible to future visitors. – Developer May 10 '23 at 18:31
0

Here's another approach it seems easy to understand but don't hesitate if you have any question.

This solution avoid to always use innerHTML!

I don't understand if you want to duplicate nodes or just create a document as specified and add a listener to it. in the clickHandler() method you can easily do what you want... Duplicate an Object, add the destination to a cart...

If you want to see the logs, click on "Full page" for the snippet.

EDIT :

I was curious if I really gain functionalities with the method I used in the first snippet, so I searched a while to see if I was able to deal with the document.createElement() method.

I finally get a result. For sure there's a lot of code and not so easy to read I suppose but I post what I've tried if someone is interested in the second snippet.

There you may create a real cart, select the number of tickets you want, calculate the prize for each destination and get all the information in the div bellow with the total prize of your cart. The data's may be submitted by a form (not implemented) an so on...

So the second snippet is a study of case, just to know if some things can be done.

If you want to see the result, click on "Full page" for the second snippet too.

END OF EDIT

Best regards.

let countries = ["Sweden", "Norway", "Denmark"];
    function addElements(){
        for(var i=0; i<countries.length; i++){
            constructElement(i)
        }
    }
    function constructElement(i){
        var newDiv = document.createElement("div");
        var newH1 = document.createElement("h1");
        var newTitle = document.createTextNode(countries[i]);
        var innerDiv = document.createElement("div");
        innerDiv.classList.add("inDiv");
        var paragraph = document.createElement("p");
        var btn = document.createElement("button");
        btn.setAttribute("value", i);
        btn.classList.add("inButton");
        var newText = document.createTextNode("Country n°" + (i+1) + " :");
        var buttonText = document.createTextNode(countries[i]);
        newDiv.appendChild(newH1);
        newH1.appendChild(newTitle);
        newDiv.appendChild(innerDiv)
        innerDiv.appendChild(paragraph);
        paragraph.appendChild(newText);
        innerDiv.appendChild(btn);
        btn.appendChild(buttonText)
        document.body.appendChild(newDiv);
        btn.addEventListener("click", clickEvent);
    }
    addElements();
    function clickEvent(event){
        console.log("button clicked value = " + this.value + ", country = " + this.textContent);
        // then you may add conditions related to the button value...
        if(this.value == 0){
            console.log("Do you live in " + countries[this.value] + "?");
        }else if(this.value == 1){
            console.log("Are you happy in " + countries[this.value] + "?");  
        }else if(this.value == 2){
            console.log("Are you really leaving " + countries[this.value] + "?");    
        }
    }
        html, body{
            font-family: sans-serif;
        }
        h1{
            font-size: 1.2em;
        }
        p{
            margin:0;
        }
        .inDiv{
            padding:10px;
            color:#009900;
            border:1px solid #000000;
            width:300px;
            font-weight:bold;
        }
        .inButton{
            padding:5px;
            color:#990000;
            width:100px;
        }

Here is the second snippet

// HERE IS A LOT OF CODE :D

    let countries = ["Sweden", "Norway", "Denmark"];
    let cart = [];
    let tableIndex = 0;
    let destinationsDiv=document.getElementById("destinationsDiv");
    let cartDiv=document.getElementById("cartDiv");
    let cartTotal=document.getElementById("cartTotal");
    let cartTotalPrice=document.getElementById("cartTotalPrice");
    let fields = [document.getElementById("field1"),
                  document.getElementById("field2"),
                  document.getElementById("field3")
                 ]
    let details = [[],[],[]];
    let counter = [
        {country:"Sweden", quantity:0, id:0, price:100},
        {country:"Norway", quantity:0, id:1, price:220},
        {country:"Denmark", quantity:0, id:2, price:150}
    ];
    let currency = "$"
    let paragraphEmpty;
    let totalPrice = 0;

    // the counter Array may accept a lot of arguments like quantity, totalPrice...
    // and You can update the elements of counter anywhere in the function
    // The createElement is safer than use innerHTML method and avoid injections possibilities
    // I think that the use of createElement is better in terms of performance.
    // In a short file like yours it' not significant for sure.

    function addElements(){
        for(var i=0; i<countries.length; i++){
            constructElement(i);
        }
        updateCartStatus();
        displayEmptyCart();
    }
        
    function updateCart(j){
        counter[j].quantity += 1;
        var inf = document.getElementById(counter[j].country+"_info");
        inf.textContent = "tickets = " + counter[j].quantity + " total price = " + (counter[j].quantity * counter[j].price) + " " + currency;
        updateCartStatus();
    }
        
    function updateCartStatus(){
        // TO CHANGE DISPLAY OF CART ITEMS SEPARETELY
        /*
        FORMAT THE DETAILS FOR THE CART
        */
        totalPrice = 0;
        var str="";
        cartTotalPrice.textContent=""
        for(var j=0; j<counter.length; j++){
            if(counter[j].quantity>0){
                str+=(counter[j].country + ", " +
                "1 ticket = " + counter[j].price + ", " +
                "quantity = " + counter[j].quantity + ", " +
                "sub-total = " + (counter[j].quantity * counter[j].price) + " " + currency
                );
                fields[j].textContent = str;
            }else{
                fields[j].textContent = "";
            }
            totalPrice += (parseInt(counter[j].quantity)*parseInt(counter[j].price));
            str="";
        }
        //console.log("totalPrice = " + parseInt(totalPrice));
        cartTotalPrice.textContent = ("TotalPrice = " + parseInt(totalPrice) + " " + currency);
        displayFilledCart();
    }

    function constructElement(i){
        let newDiv = document.createElement("div");
        var newH1 = document.createElement("h1");
        var newTitle = document.createTextNode(countries[i]);
        var innerDiv = document.createElement("div");
        innerDiv.classList.add("inDiv");
        var paragraph = document.createElement("p");
        var btn = document.createElement("button");
        btn.setAttribute("value", i);
        btn.classList.add("inButton");
        var newText = document.createTextNode("price : " + counter[i].price);
        var buttonText = document.createTextNode("add to cart.");
        newDiv.appendChild(newH1);
        newH1.appendChild(newTitle);
        newDiv.appendChild(innerDiv)
        innerDiv.appendChild(paragraph);
        paragraph.appendChild(newText);
        innerDiv.appendChild(btn);
        btn.appendChild(buttonText);
        destinationsDiv.appendChild(newDiv);
        btn.addEventListener("click", addTicket);
    }

    function addToCart(i){
        var id = counter[i].country;
        var test = document.getElementById(`${id}`);
        if (test != null) {
            updateCart(i);
            return;
        }
        // CREATE A NEW ELEMENT IF IT'S NOT ALREADY IN CART 
        var newDiv = document.createElement("div");
        newDiv.setAttribute("data-id",  i);
        newDiv.setAttribute("id",  counter[i].country);
        
        var newH1 = document.createElement("h1");
        var newTitle = document.createTextNode(countries[i]);
        var innerDiv = document.createElement("div");
        
        var paragraph = document.createElement("p");
        var details = document.createElement("div");
        
        details.setAttribute("id",  counter[i].country+"_info");
        counter[i].quantity+=1;
        
        var ticketsText = document.createTextNode("tickets = " + counter[i].quantity + " total price = " + (counter[i].quantity * counter[i].price) + " " + currency);
        details.classList.add("details");
        details.appendChild(ticketsText);
        var btn = document.createElement("button");
        btn.setAttribute("value", tableIndex);
        btn.classList.add("inButton");
        var newText = document.createTextNode("Booked : price = " + counter[i].price + currency + " / person");
        var buttonText = document.createTextNode("remove 1 ticket");
        newDiv.appendChild(newH1);
        newH1.appendChild(newTitle);
        newDiv.appendChild(innerDiv);
        innerDiv.appendChild(paragraph);
        innerDiv.classList.add("inDiv");
        paragraph.appendChild(newText);
        innerDiv.appendChild(details);
        innerDiv.appendChild(btn);
        btn.appendChild(buttonText);
        cart.push({ div: newDiv, country: countries[i], tableIndex: tableIndex});
        cartDiv.appendChild(newDiv);
        btn.addEventListener("click", removeTicket);
        tableIndex+=1;
        updateCartStatus();
    }

    function displayEmptyCart(){
        paragraphEmpty = document.createElement("p");
        paragraphEmpty.classList.add("emptyCart");
        var newText = document.createTextNode("Your cart is empty");
        paragraphEmpty.appendChild(newText);
        cartDiv.appendChild(paragraphEmpty);
    }
  
    function displayFilledCart(){
        if(paragraphEmpty != null){
            cartDiv.removeChild(paragraphEmpty);
            paragraphEmpty=null;
        }
    }

    addElements();
    function addTicket(event){
        if(this.value == 0){
            addToCart(0);
        }else if(this.value == 1){
            addToCart(1);    
        }else if(this.value == 2){
            addToCart(2);    
        }
    }

    function removeTicket(event) {
        let divToRemove = this.parentNode.parentNode;
        let index = cart.findIndex((item) => item.div === divToRemove);
        let countryCode = parseInt(divToRemove.getAttribute("data-id"));
        
        if (index !== -1) {
            if (counter[countryCode].quantity > 1) {
                counter[countryCode].quantity -= 1;
                let details = divToRemove.querySelector(".details");
                details.textContent = "tickets = " + counter[countryCode].quantity + " total price = " + (counter[countryCode].quantity * counter[countryCode].price) + currency;
            } else {
                counter[countryCode].quantity = 0;
                cartDiv.removeChild(divToRemove);
                // Update tableIndex when removing a ticket
                cart.splice(index, 1);
                for (let i = index; i < cart.length; i++) {
                    cart[i].tableIndex -= 1;
                }
            }
            updateCartStatus();
        }
        //console.log(cart.length);
        if (cart.length === 0) {
            displayEmptyCart();
        }
    }
        html, body{
            font-family: sans-serif;
        }
        h1{
            font-size: 1.2em;
        }
        p{
            margin:0;
        }
        #destinationsDiv{
            border:1px solid #000000;
            padding:10px;
            background-color: rgba( 255,66,00,0.1);
        }
        #cartDiv{
            border:1px solid #000000;
            margin-top:20px;
            padding:10px;
            background-color: rgba( 200,250,200,0.2);
        }
        #cartTotal{
            border:1px solid #000000;
            margin-top:20px;
            padding:10px;
            font-weight: bold;
            font-style: italic;
            background-color: rgba( 255,255,200,0.3);
        }
        #cartTotalPrice{
            margin:10px;
            padding:10px;
            color:#009900;
            font-weight: bold;
            font-size: 1.1em;
            border:1px solid #000000;
            width:200px;
        }
        .details{
            color:#000000;
        }
        .inDiv{
            padding:10px;
            color:#009900;
            border:1px solid #000000;
            width:300px;
            font-weight:bold;
        }
        .inButton{
            padding:5px;
            color:#990000;
            width:200px;
        }
        .emptyCart{
            font-size:1.1em;
            font-style: italic;
        }
    <div id="destinationsDiv">
        <h1>Our destinations :</h1>
    </div>
    <div id="cartDiv">
        <h1>Your cart :</h1>
    </div>
    <div  id="cartTotal">
        <p id="field1"></p>
        <p id="field2"></p>
        <p id="field3"></p>
        <p id="cartTotalPrice"></p>
    </div>
tatactic
  • 1,379
  • 1
  • 9
  • 18
  • Thank you this was helpful. My goal really was to make a button connected to each country and when you clicked it, you put that object in a "cart" and when did so you also changed the add button to a remove button like a shoppingcart sort of! And i wanted to do so with as compact code as possible and with best practice and so on. @tatactic – Gips1 May 10 '23 at 10:27