Problem
In the below code snippet, on click of a button, progress bars are added one by one. The bar shows up for 3 seconds. If you continuously click on the button, it will keep adding progress bars. The next bar will start after the previous bar finishes animating.
Is there anyway to make the code better?
;(function(window, document, undefined) {
'use strict';
var container = document.getElementsByClassName('container')[0];
var button = document.getElementsByTagName('button')[0];
var progressBar = document.getElementsByClassName("bar-container")[0];
var promise = Promise.resolve();
button.addEventListener('click', function() {
var newProgressBar = progressBar.cloneNode(true);
container.appendChild(newProgressBar);
promise = promise.then(function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
newProgressBar.classList.add('active');
setTimeout(function() {
resolve();
}, 3000);
}, 0);
});
})
});
})(window, document);
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.bar-container{
width: 100%;
background: #ccc;
border: 1px solid #eee;
margin-bottom: 10px;
}
.loader{
height: 20px;
background: #434334;
width: 0px;
transition: width 3s ease;
}
.active .loader{
width: 100%;
}
</style>
</head>
<body>
<div class="container"></div>
<div style="display: none">
<div class="bar-container">
<div class="loader">
</div>
</div>
</div>
<button id="button">click me</button>
<script src="script.js"></script>
</body>
</html>
emphasized text
Solution
Your code already looks pretty clean. Good work! There are only a few things I would recommend changing.
-
Don’t define timings in CSS that should be defined in JavaScript. In this specific case using a transition made it possible to simplify the code significantly, however in a more involved example it would become problematic. What happens if one progress bar takes 2 seconds to complete, and another takes 5?
-
Instead of using
getElementsByClassName('cls')[0]
, you can usequerySelector('.cls')
for more readable code. Though this can be slower, it really isn’t an issue unless it is being run thousands of times per second. -
There is a built in
<progress>
element that you may want to use. -
It is a good idea to wrap template elements in a
<template>
element.
Here’s a simple demo that defines the timing in the JavaScript.
function Loader(element, time) {
return new Promise(resolve => {
let intervalId;
function tick() {
element.value = parseInt(element.value, 10) + 1
if (element.value == element.max) {
clearInterval(intervalId)
resolve()
}
}
setInterval(tick, time / element.max)
})
}
let promise = Promise.resolve()
let container = document.querySelector('.container')
let template = document.querySelector('template')
function addProgress() {
let progress = document.importNode(template.content, true)
container.appendChild(progress)
// progress is a document fragment, not the template element
return container.querySelector('progress:last-child')
}
document.querySelector('button').addEventListener('click', () => {
let bar = addProgress()
promise = promise.then(() => Loader(bar, 3000))
})
progress {
display: block;
width: 100%;
}
<template>
<progress max="100" value="0"></progress>
</template>
<div class="container"></div>
<button>Click me!</button>
Redefining a standard type undefined
is not a good idea and why redefine it you don’t use it.
-
Update It is a popular method to ensure
undefined
is indeedundefined
, though the pattern is not completely safe and is really just a popular hack. ES6 allows you to ensureundefined
as type using a constant, protecting it from mutation and avoiding accidental redefinition.It is better to use an alias for
undefined
to clearly show your intent. I personally useU
but the use ofsafeUndefined
would be a better option for collaborative work.const safeUndefined = ([])[0]; // first element of empty array will be undefined
If you do not trust the
Array
then use the function return defaultconst safeUndefined = (()=>{})();
Dont define what you do not use its just noise
Why redefine window which remains in scope even when redefined. You pass it and then ignore it when you set the timer events so why pass it. It may have made a little more sense if you did window.setTimeout
rather than letting it default to the window object (not the one you passed)
And reject
, you don’t use it, its not needed so why define it.
Use quotes consistently preferably “” don’t mix and match. You use single quotes for everything bar the progress bar class name. Though I can not work out why.
Use const
in preference over var
if you don’t plan on reassigning the variable.
The only good variable names are progressBar
and newProgressBar
,
button
yes it is but what does it do? maybeaddBarButton
container
of what? maybenewBarContainer
(I had to look further in the code to see what you did with it)promise
promising what? maybeprogressComplete
resolve
we all know that it what it does but what does it actually do, maybestartNextBar
The first progress bar does not work, you need to set its style correctly before you display. Dont set the loader
class to 100% width in the style as that is happening when the page loads and can not be seen. Then when you want to start the progress add the active
class to the the first child of the progress bar. This will stop the first bar from completing before it is seen.
The rewrite
;(function() {
"use strict";
const newBarContainer = document.getElementsByClassName("container")[0];
const addBarButton = document.getElementsByTagName("button")[0];
const progressBar = document.getElementsByClassName("bar-container")[0];
var progressComplete = Promise.resolve();
addBarButton.addEventListener("click", () => {
const newBar = progressBar.cloneNode(true);
newBarContainer.appendChild(newBar);
progressComplete = progressComplete.then(() => {
return new Promise((startNextbar) => {
setTimeout(() => {
newBar.children[0].classList.add("active");
setTimeout(startNextbar, 1000);
}, 0);
});
})
});
})();
.bar-container {
width: 100%;
background: #ccc;
border: 1px solid #eee;
margin-bottom: 10px;
}
.loader {
height: 20px;
background: #434334;
width: 0px;
transition: width 1s ease;
}
.active {
width: 100%;
}
<button id="button">click me</button>
<div class="container"></div>
<div style="display: none">
<div class="bar-container">
<div class="loader">
</div>
</div>
</div>