Javascript

Building a Simple Interactive Segmented Progressbar With Javascript

Building a Simple Interactive Segmented Progressbar With Javascript

In this snippet we will build a simple interactive progress bar with animated segments using javascript code.

 

 

 

Progress bars are special UI tools used in many software systems to indicate that some process completed or not. In web applications also powered with javascript progress bars also used in large scale. Progress bars come can be in many shapes, for example can be line indicators, circles, etc.

In this tutorial we will build a simple progress bar using javascript logic.

 

What we will be building

What we will be building is a segmented progress bar like this diagram.

Building a Simple Interactive Segmented Progress Bar With Javascript - progress steps

In our example i will imagine that we have a web app and we are on final stage of the installation process. In the installation process there are a basic form to fill the app details and on reaching the final stage a progress bar will show up to complete the installation process.

To achieve such progress bar the same as the above diagram we should specify the bar steps as an array. These steps basically can be retrieved from server side but in our example we will specify the steps statically.

Each step contain the amount or percent to complete and the text that show on the progress step like this:

const steps = [
                {
                    text: 'Checking environment',
                    percent: 15
                },
                {
                    text: 'Creating Files',
                    percent: 25
                },
                {
                    text: 'Building Database',
                    percent: 50
                },
                {
                    text: 'finalizing',
                    percent: 10
                }
            ];

The sum of overall steps percents should equal to 100% which is the total completed amount of the progress bar.

 

Implementing The Progress Bar

Create new folder named whatever you want for example, “interactive-progress-bar”. Inside this folder create those files:

  • index.html
  • progress.js
  • style.css

Open index.html and add this code:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>progress bar</title>

    <link type="text/css" rel="stylesheet" href="./style.css" />
</head>
<body>

    <div class="container">
        <div class="progress-bar"></div>
    </div>

    <script type="text/javascript" src="./progress.js"></script>
</body>
</html>

I included “style.css” which will contain the progress bar style, and “progress.js” which contain the javascript logic to animate the progress bar.

The <div /> with class “progress-bar” is the container for the progress bar.

Now let’s think about the Javascript logic to process the progress bar.

According to the above diagram for each step object in the array, a dom node will be created and appended in the progress bar container like this figure:

Interactive Segmented Progress Bar Javascript - Progress Bar Console Log

 

So the algorithm to create the steps nodes and animation as follows:

  • Iterate over the steps array, for each step generate the steps nodes dynamically.
  • On each step creation assign a css class to each node of “progress-step“. Also set random background color. Finally append the created node to the progress container.
  • Once the step nodes created start a timer using javascript setInterval() function which will handle the progress animation.
  • Inside the setInterval() callback check if the total progress amount reaches 100%, if so then mark the progress as completed by showing text “Completed 100%” and stopping the timer.
  • Instead if the timer less than 100% then watch if the current step amount less than current step percent declared in the steps array, if so then increment the amount and increment the step width, otherwise move to the next step.

Now let’s move to the implementation logic. I will be using javascript Class to wrap the necessary logic.

Open progress.js and add this class skeleton:

class ProgressBar {

            selector = ".progress-bar";				// progress bar selector
            steps = [];                     		        // expected array of step objects

            #totalCompletedThreshold = 0;    		        // private property to indicate the overall completed progress
            #currStepCompleted = 0; 
            #currStepIndex = 0;
            #animationSpeed = 1000;
            #showLog = true;

            constructor(selector, steps, showLog = true) {
                this.selector = selector;
                this.steps = steps;
                this.#showLog = showLog;
            }

            get progressDom() {
                return document.querySelector(this.selector);
            }

            get progressCompleteDom() {
                return document.querySelector('.progress-complete');
            }


            generateStepsDom() {

                
            }


            mapStyleObjToStyleStr(styleObj) {      // convert style object to style string
                
            }

            
            setStepWidth(index, width) {     // set the step dom node width

            }

            createProgressCompleteNode() {
                
            }

            markProgressAsComplete() {
                
            }

            start() {
                
            }

}

The javascript class ProgressBar contains the implementation details. Now let’s discuss the properties and functions in this class. First of all we have some properties like selector, steps, etc.

The steps, selector, and #showLog properties passed to the class constructor. The steps store the array of steps, like the steps array you saw earlier. the selector stores is the css selector for the progress bar div. The #showLog property indicates whether or not to show log in browser console.

The # operator before the property indicates that this property is private.

The #totalCompletedThreshold is private property that store the overall progress completion amount.

The #currStepIndex refers to the current step index the timer is currently processing.

The #currStepCompleted refers to completed amount for the current step index.

The #animationSpeed stores the animation timeout for the setInterval() callback.

 

Also we have these functions and getters:

  • The progressDom() is a class getter to query the progress bar selector using javascript querySelector() function.
  • The progressCompleteDom() is another getter to retrieve the div that will show at the end of the animation.
  • generateStepsDom() the function responsible for iterating over the steps and generate each step div.
  • setStepWidth() responsible for updating a step width by index.
  • createProgressCompleteNode() responsible for creating the div that will show upon progress completion.
  • mapStyleObjToStyleStr() this utility function maps a style object to style string so that it can parsed to each element style property.
  • start() is the entry function that will be called to trigger the progress and start the setInterval().

 

So Let’s add the code for the start() function:

start() {
                this.generateStepsDom();

                let interval = setInterval(() => {
                
                    if(this.#totalCompletedThreshold >= 100) {
                        
                        this.createProgressCompleteNode();

                        this.markProgressAsComplete();

                        clearInterval(interval);
                    } else {
                        if(this.steps[this.#currStepIndex] && this.steps[this.#currStepIndex].percent === this.#currStepCompleted) {

                            this.setStepWidth(this.#currStepIndex, this.steps[this.#currStepIndex].percent);

                            ++this.#currStepIndex;
                            this.#currStepCompleted = 0;	
                        } else {
                            
                            ++this.#currStepCompleted;
                            
                            this.setStepWidth(this.#currStepIndex, this.#currStepCompleted);
                            
                            ++this.#totalCompletedThreshold;
                        }
                    }

                    if(this.#showLog) {
                        console.info(`current step index: ${this.#currStepIndex} - current step completed ${this.#currStepCompleted} - total completed ${this.#totalCompletedThreshold}`);
                    }

                }, this.#animationSpeed);
            }

Inside the start() function we invoke generateStepsDom() first, which we will see below to generate the each step div element. Next i am calling javascript setInterval() and assigns the result to a variable interval so that we can clear the variable on completion.

Next i am applying the algorithm above by checking for #totalCompletedThreshold if it’s greater than or equal 100 than we invoke createProgressCompleteNode() and markProgressAsComplete() functions respectively to show the progress complete div.

If #totalCompletedThreshold less than 100 then we check for the current step using #currStepIndex in the steps array and compare the current step percent with #currStepCompleted. If they are equal then we update the step width and increment the index so than it can move to the next step and reset #currStepCompleted to be 0. Otherwise we update the step width and increment both #currStepCompleted and #totalCompletedThreshold

At the end i am checking for #showLog, if true then print console.info() message.

 

The code for generateStepsDom():

generateStepsDom() {

                steps.forEach(step => {    // for each step create a div element inside the main progress container
                    const createdStep = document.createElement("div");

                    step.class = 'progress-step';        // assign css class name

                    createdStep.setAttribute('class', step.class);
                    
                    const randomColor = Math.floor(Math.random()*16777215).toString(16);

                    step.style = {
                        'background-color': `#${randomColor}`
                    };

                    createdStep.setAttribute('style', this.mapStyleObjToStyleStr(step.style));

                    this.progressDom.appendChild(createdStep);

                    step.domNode = createdStep;
                });
            }

In this code we use javascript each() function to apply a callback for each item in the steps array. Using document.createElement() to create element dynamically we created a div to hold the step. Then in each step we set a css class using setAttribute(), then setting the style which is background color for the step using the above formula to generate random color:

const randomColor = Math.floor(Math.random()*16777215).toString(16);

To set the style we have to convert the style object to style string using mapStyleObjToStyleStr() function and finally we appended the div to the progress container using this.progressDom.appendChild().

Now let’s see the code for setStepwidth():

setStepWidth(index, width) {     // set the step dom node width

                const stepSelector = document.getElementsByClassName("progress-step")[index];

                if(stepSelector) {
                    const stepStyle = {...this.steps[index].style};

                    stepStyle.width = `${width}%`;
                    
                    this.steps[index].style = stepStyle;

                    stepSelector.style = this.mapStyleObjToStyleStr(stepStyle);

                    stepSelector.innerText = `${this.steps[index].text} (${stepStyle.width})`;

                    this.steps[index].domNode = stepSelector;
                }
            }

This function controls the step width by updating the step div width in each interval. It accepts the index and width value. Next we we retrieve the dom node first using getElementsByClassName(classname) which return array of nodes. To return just the node by index:

const stepSelector = document.getElementsByClassName("progress-step")[index];

Then we retrieve the style object for this step from the steps array using this.steps[index].style to update the width. The style object is converted again to style string using mapStyleObjToStyleStr() and also we update the innerText for this step.

The code for createProgressCompleteNode() and markProgressAsComplete() as follows:

createProgressCompleteNode() {
    const node = document.createElement("div");

    node.setAttribute('class', 'progress-complete');
        
        this.progressDom.appendChild(node);
}
markProgressAsComplete() {
                const progressComplete = this.progressCompleteDom;

                progressComplete.innerText = 'Completed 100%';
                progressComplete.style.display = "block";

                this.steps.forEach(step => {
                    if(step.domNode) {
                        step.domNode.style = this.mapStyleObjToStyleStr({...step.domNode.style, display: 'none'});
                    }
                });
}

The code for createProgressCompleteNode() is simple we just create a simple div using document.createElement() then appending it to the progress container.

The other function markProgressAsComplete() just invokes the progress complete div we just created in the above div and updates the inner text and css display property to “block”. Also it inverts all the other step div’s display to “none”.

 

The last function mapStyleObjToStyleStr():

mapStyleObjToStyleStr(styleObj) {      // convert style object to style string
                const styleArr = [];

                Object.entries(styleObj).forEach(([key, value]) => {
                    styleArr.push(`${key}:${value}`);
                });

                return styleArr.length ? styleArr.join(';'): '';
            }

 

The full code for the class:

class ProgressBar {

            selector = ".progress-bar";				// progress bar selector
            steps = [];                     		// expected array of step objects

            #totalCompletedThreshold = 0;    		// private property 
            #currStepCompleted = 0;
            #currStepIndex = 0;
            #animationSpeed = 1000;
            #showLog = true;

            constructor(selector, steps, showLog = true) {
                this.selector = selector;
                this.steps = steps;
                this.#showLog = showLog;
            }

            get progressDom() {
                return document.querySelector(this.selector);
            }

            get progressCompleteDom() {
                return document.querySelector('.progress-complete');
            }


            generateStepsDom() {

                steps.forEach(step => {    // for each step create a div element inside the main progress container
                    const createdStep = document.createElement("div");

                    step.class = 'progress-step';        // assign css class name

                    createdStep.setAttribute('class', step.class);
                    
                    const randomColor = Math.floor(Math.random()*16777215).toString(16);

                    step.style = {
                        'background-color': `#${randomColor}`
                    };

                    createdStep.setAttribute('style', this.mapStyleObjToStyleStr(step.style));

                    this.progressDom.appendChild(createdStep);

                    step.domNode = createdStep;
                });
            }


            mapStyleObjToStyleStr(styleObj) {      // convert style object to style string
                const styleArr = [];

                Object.entries(styleObj).forEach(([key, value]) => {
                    styleArr.push(`${key}:${value}`);
                });

                return styleArr.length ? styleArr.join(';'): '';
            }

            
            setStepWidth(index, width) {     // set the step dom node width

                const stepSelector = document.getElementsByClassName("progress-step")[index];

                if(stepSelector) {
                    const stepStyle = {...this.steps[index].style};

                    stepStyle.width = `${width}%`;
                    
                    this.steps[index].style = stepStyle;

                    stepSelector.style = this.mapStyleObjToStyleStr(stepStyle);

                    stepSelector.innerText = `${this.steps[index].text} (${stepStyle.width})`;

                    this.steps[index].domNode = stepSelector;
                }
            }

            createProgressCompleteNode() {
                const node = document.createElement("div");

                node.setAttribute('class', 'progress-complete');

                this.progressDom.appendChild(node);
            }

            markProgressAsComplete() {
                const progressComplete = this.progressCompleteDom;

                progressComplete.innerText = 'Completed 100%';
                progressComplete.style.display = "block";

                this.steps.forEach(step => {
                    if(step.domNode) {
                        step.domNode.style = this.mapStyleObjToStyleStr({...step.domNode.style, display: 'none'});
                    }
                });
            }

            start() {
                this.generateStepsDom();

                let interval = setInterval(() => {
                
                    if(this.#totalCompletedThreshold >= 100) {
                        
                        this.createProgressCompleteNode();

                        this.markProgressAsComplete();

                        clearInterval(interval);
                    } else {
                        if(this.steps[this.#currStepIndex] && this.steps[this.#currStepIndex].percent === this.#currStepCompleted) {

                            this.setStepWidth(this.#currStepIndex, this.steps[this.#currStepIndex].percent);

                            ++this.#currStepIndex;
                            this.#currStepCompleted = 0;	
                        } else {
                            
                            ++this.#currStepCompleted;
                            
                            this.setStepWidth(this.#currStepIndex, this.#currStepCompleted);
                            
                            ++this.#totalCompletedThreshold;
                        }
                    }

                    if(this.#showLog) {
                        console.info(`current step index: ${this.#currStepIndex} - current step completed ${this.#currStepCompleted} - total completed ${this.#totalCompletedThreshold}`);
                    }

                }, this.#animationSpeed);
            }

}

Open style.css and add some styling:

.container {
    padding: 100px 6px;
}

.progress-bar {
            height: 27px;
            border-radius: 7px;
            border: 1px solid #ccc;
            display: flex;
        }

        .progress-bar .progress-step {
            display: inline-block;
            height:  100%;
            transition: all 0.3s;
            text-align: center;
            font-family: Arial;
            line-height: 2.5;
            font-size: 10px;
            font-weight: bold;
            white-space: nowrap;
        }

        .progress-bar .progress-complete {
            display: none;
            text-align: center;
            width: 100%;
            background: lightgreen;
            color:  #fff;
            font-weight: bold;
            line-height: 1.6;
        }

Finally apply this class in index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>progress bar</title>

    <link type="text/css" rel="stylesheet" href="./style.css" />
</head>
<body>

    <div class="container">
        <div class="progress-bar"></div>
    </div>

    <script type="text/javascript" src="./progress.js"></script>
    <script>
        const steps = [
                {
                    text: 'Checking environment',
                    percent: 15
                },
                {
                    text: 'Creating Files',
                    percent: 25
                },
                {
                    text: 'Building Database',
                    percent: 50
                },
                {
                    text: 'finalizing',
                    percent: 10
                }
            ];

        const myProgress = new ProgressBar(".progress-bar", steps);
        
        myProgress.start();  // start the progress process
        
    </script>
</body>
</html>

Now if you clicked and opened index.html in your browser you will see the progress bar animation.

Repository url

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments