For the loop that assigns only the last value of an array to event listeners

advertisements

I have the following AngularJS controller;

app.controller('playerController', ['$scope', '$http', 'playerService', function ($scope, $http, playerService) {
     $scope.soundeffects = playerService.getSoundeffects();

     function soundeffectCreation() {
        var maxrows = 3;
        var n = 0;

        var buttonTable = "<table class='table text-center'><tr>";

        for (var item = 0; item < $scope.soundeffects.length; item++) {
            var soundeffect = $scope.soundeffects[item];
            var soundeffectName = soundeffect.name;
            buttonTable += "<td><label>" + soundeffectName + "</label><button type='button' class='btn btn-default btn-block' id='" + soundeffectName + "'><i class='fa fa-play'></i></button></td>"
             if(n == maxrows) {
                buttonTable += "</tr><tr>"
                n = 0;
             }
             else{
                n++;
            }
        }
        buttonTable += "</tr></table>";
        document.getElementById('soundeffects').innerHTML = buttonTable;

         for (var item = 0; item < $scope.soundeffects.length; item++) {
             var soundeffect = $scope.soundeffects[item];
             var soundeffectName = soundeffect.name;
             var button = document.getElementById(soundeffectName);

             console.log(soundeffect);
             console.log(soundeffectName);
             console.log(button);
             button.addEventListener('click', function () {
                console.log(soundeffectName);
            })
        }
    }

    soundeffectCreation();
}

It's working its magic on the following HTML:

<div id="soundeffects"></div>

Using a JSON array that so far only contains 3 items:

{
   "soundeffects": [
      {
        "name": "Wilhelm Scream",
        "video": "r6JK-gRELI0"
      },
      {
        "name": "Fireball",
        "video": "AHRf27GPhQc"
      },
      {
        "name": "Mario Jump",
        "video": "37-paiEz0mQ"
      }
   ]
}

This JSON is retrieved succesfully in the playerService. The buttons in the table are created just fine, they get their respective labels and the id's are assigned correctly, the problem appears in the second for-loop. (The reason there's a second for-loop is because I can't add eventlisteners before the table is actually created).

The first three console.log's outside of the addEventListener return the correct values: Console

Three different Objects with their respective names and videos. Three different Names (Wilhelm Scream, Fireball & Mario Jump). Three different buttons with the id's equal to the names described above.

However, when I click the created buttons, each of them returns "Mario Jump", the last value of the array, instead of the name of the respective element. This despite the id's of the buttons being correct and everything. Any suggestions where I'm going wrong?


ALTERNATIVE

As an alternative I tried putting ng-click='$scope.playSoundeffect(" + soundeffect + ")' in the original button creator, but that puts soundeffect in there as simply [Object object]. Any suggestions on how to make that method work would be welcome aswell.


EDIT FOR POSTERITY

After getting a bit more AngularJS experience I remade this entire section. I removed the entire soundeffectCreation() function and replaced it with the following html:

<div ng-repeat="soundeffect in soundeffects track by $index" class="col-xs-6 col-md-4 col-lg-3">
    <div class="text-nowrap"><label>{{soundeffect.name}}</label></div>
    <div>
        <button class="btn btn-default" ng-click="playSoundeffect(soundeffect)">
            <i class="fa fa-play"></i>
        </button>
    </div>
</div>


Use let instead of var inside for loop:

for (let item = 0; item < $scope.soundeffects.length; item++) {
     let soundeffect = $scope.soundeffects[item];
     let soundeffectName = soundeffect.name;
     let button = document.getElementById(soundeffectName);

     console.log(soundeffect);
     console.log(soundeffectName);
     console.log(button);
     button.addEventListener('click', function () {
        console.log(soundeffectName);
    })
}

The problem was lack of distinct closure for each loop iteration - please check this SO answer for detailed explanation of closures in for loops.

Solution for browsers that doesn't support let

You can also use self-invoking anonymous function to create new scope for each iteration:

for (var item = 0; item < $scope.soundeffects.length; item++) {
     (function(i) {
         var soundeffect = $scope.soundeffects[i];
         var soundeffectName = soundeffect.name;
         var button = document.getElementById(soundeffectName);

         console.log(soundeffect);
         console.log(soundeffectName);
         console.log(button);
         button.addEventListener('click', function () {
            console.log(soundeffectName);
        })
     })(item);
}