Monday, August 12, 2013

Building a Modal Progress Bar with Twitter Bootstrap 3

I needed a progress bar to show when loading some data and wanted the screen to block. It sounded like a job for a modal dialog. Bootstrap had both so I tried to combine them to create the desired effect. The idea was to show the modal and then cause the progress bar to animate from 0% to 100%. Bootstrap already has the transition defined in the framework, you just need to change the width to reflect the progress. In my case, I really didn't know the status, I just wanted to make it look like I did. So I created a class that changed the width to 100%. Thinking everything was all set, I tried it out and found it always showed at the 100% point:

See the Pen Bootstrap Modal Progress Bar by bseth99 (@bseth99) on CodePen



Knowing that there are usually issues around elements considered "hidden", I did some digging and found that transitions will not be run on elements with display: "none". Well, I had the show method just before it so it must be some kind of timing issue. I tried adding a setTimeout to force the code to run after the current execution stack completed:

  setTimeout(function() {
    $bar.addClass('animate');
  }, 0);


See the Pen Bootstrap Modal Progress Bar by bseth99 (@bseth99) on CodePen



This worked well, except every few clicks on the button resulted in the progress bar appearing in the final state. So something still wasn't working consistently. I upped the timeout to 10 milliseconds and it "seemed" to work fine. However, I wasn't going to risk it so I decided to really understand what was happening. As it turns out, browsers try to be pretty smart to speed things up so they'll try to batch changes to the DOM before really "committing" them to the DOM. In our case, we're going from display: "none" to display: "block" and then trying to add the animate class. This will get batched into one operation. The 10ms just tried to get outside that window to force a redraw before it was applied. However, this could change or vary between browsers so its probably not a good solution. A better approach is actually quite simple and avoids the setTimeout entirely:

  
  // Force DOM repaint with is()
  $modal.modal('show').is(':visible');
  $bar.addClass('animate');



Because $.is queries the DOM, the browser must render out anything its queued up. I could use anything to trigger it, but checking for ":visible" seemed related to what I was doing so I went with it. Now, the next line can run immediately knowing the progress bar is actually displayed and the transition will occur:

See the Pen Bootstrap Modal Progress Bar by bseth99 (@bseth99) on CodePen

I also fixed the styles to properly show the modal backdrop with a 50% opacity (this is corrected in a later release of Bootstrap 3). Knowing what happens made it easier to understand how to work around it. It looks odd, so a comment is probably good to avoid a friend from removing it and breaking everything, otherwise, this beats the setTimeout approach.