user1253359
user1253359

Reputation: 81

JS dynamic change divs width in loop

I want to create simple bar chart with animated bars (width from 0 to final size).

I started from css transitions but opera and chrome sometimes have problems.

Now I try to use mechanism what I saw here:

https://www.w3schools.com/w3css/w3css_progressbar.asp

using JS.

I have few "bar-areas":

  <div id="chart-bars">
    <div class="chart-bar-area">
        <div class="chart-bar" data-percent="80"  ></div>

    </div>

        <div class="chart-bar-area">
        <div class="chart-bar" data-percent="60"   ></div>
    </div>
  </div>

and JS code which works fine with one bar, but if I try to implement that mechanism for all in loop this script works correctly only for last one bar. Other bars not grooving.

My JS code below:

  var foo = document.getElementById('chart-bars');
  var width;
  var elem;
  var id;
  for (var i = 0; i < foo.children.length; i++) {

   elem = foo.children[i].children[0];

     width = 1;
     id = setInterval(frame, 10);
    function frame() {
      if (width >= elem.dataset.percent) {
        clearInterval(id);
      } else {
        width++; 
        elem.style.width = width + '%'; 
      }
    }

  }

Can somebody help me ?

Thanks

Upvotes: 2

Views: 1496

Answers (4)

Leroy Stav
Leroy Stav

Reputation: 718

Congratulations! Today will learn what a "closure" is.

You are experiencing a scoping issue. The reason this only works for the last bar is because the frame function only sees elem as it currently exists on scope. That is to say, by the time your setInterval runs your loop will have ended and elem will be firmly set to what amounts to foo.children[foo.children.length - 1].children[0]

The way to fix this is by creating a new closure. That is to say, you "close" the variable in a scope.

var foo = document.getElementById('chart-bars');

for (var i = 0; i < foo.children.length; i++) {
  (function (elem, width) {
    var id = setInterval(frame, 10);
    function frame() {
      if (width >= elem.dataset.percent) {
        clearInterval(id);
      } else {
        width++; 
        elem.style.width = width + '%'; 
      }
    }
  })(foo.children[i].children[0], 1)
}

now this, specifically, is a far from perfect way of accomplishing your goal, but I wrote it this way in order to preserve as much of your code as possible in an attempt to reduce the cognitive load for your specific example.

Essentially what I am doing is wrapping your code in an IIFE (Immediately Invoked Function Expression), which creates a new scope with your elem and width variables fixed to that function's scope.

A further step in the right direction would be to pull your frame function outside of the loop and have it only created once and have it accept as its arguments an elem, width, and id parameter, but I'll leave that as an exercise for the reader :-)

Upvotes: 1

Air1
Air1

Reputation: 567

Looks like a variable scope issue, could you try something like this :

var foo = document.getElementById('chart-bars');
for (var i = 0; i < foo.children.length; i++) {

   var elem = foo.children[i].children[0];
   elem.style.width = '1%';

   var id = setInterval(function() { frame(elem, id) }, 10);
}

function frame(elem, idInterval) {
   var width = parseInt(elem.style.width);
   if (width >= elem.dataset.percent) {
     clearInterval(idInterval);
   } else {
     width++; 
     elem.style.width = width + '%'; 
   }
}

Basically before you were erasing at each loop your previous elem, width, and id variables because they were declared outside the for loop. Hence the weird behavior.

Edit : putting the frame function outside so that it's clearer.

Edit2 : removing width from frame function as it is not needed.

Upvotes: 0

AB Udhay
AB Udhay

Reputation: 753

This is because value of i is out of scope of setInterval callback, setInterval takes only last value of iteration as it is a window method. So, you have to go with the closure or solution like this.

    var foo = document.querySelectorAll(".chart-bar");
    var width;
    var elem;
    var id;
    for (var i = 0; i < foo.length; i++) {

        var myElem = {
            x: i
        }

        width = 1;
        id = setInterval(function() {
            if (width >= foo[this.x].dataset.percent) {
                clearInterval(id);
            } else {
                width++;
                foo[this.x].style.width = width + '%';
            }
        }.bind(myElem), 10);//Here you are binding that setInterval should run on the myElem object context not window context

   }

Upvotes: 1

David Marabottini
David Marabottini

Reputation: 327

why don't you use the specific jquery lybraries to create the charts like morrischart or chartjs, there are very simple and they work very well there are the documentations http://morrisjs.github.io/morris.js/ https://www.chartjs.org

Upvotes: 0

Related Questions