Making a seekable progress component a.k.a slider in Vue.
Tags: webdev, progress, seek, vue
Published: 2020-02-05 17:15:28

---
title: Making a seekable progress component a.k.a slider in Vue.
published: true
description: A short tutorial on making a seekable progress component while utilizing javascript event listeners.
tags: webdev, progress, seek, vue
cover_image: https://images.unsplash.com/photo-1576269076024-0e3b5a5bf918?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80
canonical_url: https://jamesinkala.com/blog/making-a-seekable-progress-component-aka-slider-in-vue/
---

Lately I've been working on an update of an audio player that I created last year as visible on github:

{% github xinnks/xns-audio-player %}.

In short it's an audio player that's based to work with vue, the idea being, it should support persistent playback on route changes in a javascript environment, in this case Vue.

Like on a couple other projects, I always start with an idea then execute it with more bloated code and plugins than favorable. Then I usually proceed with cutting back on the plugins in favor of custom components, re-inventing the wheel so to speak, but with the target of reducing code size and hopefully increasing performance by reducing dependencies.

So, amongst the plugins I decided to cut off from the project, was a slider component that I used to convey audio playback position and seeking to the UI, which brings us to this article. I decided to share this because I think it might be useful to someone out there who at first might assume creating such functionality on their project is a complicated task, well, no it's not.

###Let's get down to business.###

Our objective is to achieve this 👇
![The final component](https://res.cloudinary.com/djx5h4cjt/image/upload/v1580915392/dev.to/seekable.gif)

Since this project is based on Vue, I'll be using code snippets from the component itself which is in a Vue environment, but likewise, you can apply the same concept on any Javascript environment as we will be utilizing Javascript event listeners.

After setting up the Vue project environment (here for [beginners](https://vuejs.org/v2/guide)) we will start by creating our seekable component and name it 'SeekProgress.vue'

Our template will contain only two div blocks, a wrapper which will be setting the dimensions for our component, and it's child which will be an absolute positioned div covering the parent based on the percentage of the total width.

```html
<template>
  <div id="app">
    <div class="progress-wrapper">
      <div class="progress"></div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'SeekProgress',
}
</script>

<style lang="scss">
.progress-wrapper{
  display: block;
  height: 200px;
  margin: 200px 20px;
  position: relative;
  background: #e1e1e1;

  .progress{
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;
    background: teal;
  }
}
</style>
```

Next, we will add a reference to our wrapper block by using a $ref, whereas we'd utilize the div's id or class to reference it in but not limited to vanilla js.

```html
<template>
  <div id="app">
    <div ref="listenTo" class="progress-wrapper">
      <div class="progress"></div>
    </div>
  </div>
</template>
```

After listening to the Javascript events we'll be modifying the '.progress' div's width by adding inline styling to it, passing the width percentage.

```html
<template>
  <div id="app">
    <div ref="listenTo" class="progress-wrapper">
      <div :style="'width:'+progress+'%'" class="progress"></div>
    </div>
  </div>
</template>
```

Then, we will listen to the events on the wrapper and react accordingly.

```html
<script>
export default {
  name: 'SeekProgress',
  data(){
    return {
      progress: 0,
      wrapperWidth: 0
    }
  },
  mounted(){
    this.$refs.listenTo.addEventListener("click", this.getClickPosition, false)
  },
  methods: {
    getClickPosition(e){
      e = e || window.e

      // get target element
      let target = e.target || e.srcElement
      if(target.nodeType == 3) target = target.parentNode // fix for a safari bug
      this.wrapperWidth = this.wrapperWidth || target.offsetWidth // set initial wrapper width

      // get the seek width
      let seekWidth = e.offsetX

      // change seek position
      this.progress = (seekWidth / this.wrapperWidth) * 100
    },
  }
}
</script>
```

__A breakdown of the script above:__
__*progress*__: a variable that sets the width of the progress div in percent.
__*wrapperWidth*__: a variable that stores the dynamic width of the wrapper div, which we derive our progress percent from.
__*getClickPosition()*__: A callback function executed when a click event is performed on the wrapper div block.

On the __*getClickPosition()*__ function we make sure we get the object that the event is based on, in our case the wrapper block; doing this just after error proofing different browser types on acquiring this object. After, we set our initial wrapper width and then obtain the position where the event occurred based on the horizontal offset from the left side of our component.
Next we get the percent of this offset to the total block width and store it in 'progress'.

It is important to make sure that the __*wrapperWidth*__ variable will be modified when the window is resized, otherwise we end up with not interesting results when we interact with our component after resizes.

We'll add a window resize listener that will be doing just that.

```html
<script>
...
    //add a listener that will listen to window resize and modify progress width accordingly
    window.addEventListener('resize', this.windowResize, false)
...
...
   windowResize(e){
      let prog = this
      setTimeout(()=>{
        prog.wrapperWidth = prog.$refs.listenTo.offsetWidth
      }, 200)
    }
  }
...
}
</script>
```


###That's all... right!?###
If your target is just modifying the progress on mere clicks and not including drags, that's it really. But if you want to have smooth drag seeks you'll need to  listen to a couple more events.

Our friends _"mousedown"_, _"mousemove"_ and _"mouseup"_ will help us with that.


```html
<script>
  ...
  mounted(){
    ...
    this.$refs.listenTo.addEventListener("mousedown", this.detectMouseDown, false)
    this.$refs.listenTo.addEventListener("mouseup", this.detectMouseUp, false)
    ...
  },
  methods: {
    ...
    detectMouseDown(e){
      e.preventDefault() // prevent browser from moving objects, following links etc

      // start listening to mouse movements
      this.$refs.listenTo.addEventListener("mousemove", this.getClickPosition, false)
    },
    detectMouseUp(e){
      // stop listening to mouse movements
      this.$refs.listenTo.removeEventListener("mousemove", this.getClickPosition, false)
    },
    ...
  }
}
</script>
```

We start by listening to a __*mousedown*__ event inside which we commence listening to __*mousemove*__ events and updating our progress accordingly by utilizing our first callback function __getClickPosition()__; what to note here is the __*e.preventDefault()*__ which tells the browser not to drag stuff on the screen.
When the mouse is released and we hear a __*mouseup*__ event, we stop listening to the __*mousemove*__ event and voila! we have added drag capability to our progress component.

Compiling the code above, we then have:

```html
<template>
  <div id="app">
    <div ref="listenTo" class="progress-wrapper">
      <div :style="'width:'+progress+'%'" class="progress"></div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'SeekProgress',
  data(){
    return {
      progress: 0,
      wrapperWidth: 0
    }
  },
  mounted(){
    this.$refs.listenTo.addEventListener("click", this.getClickPosition, false)
    this.$refs.listenTo.addEventListener("mousedown", this.detectMouseDown, false)
    this.$refs.listenTo.addEventListener("mouseup", this.detectMouseUp, false)

    //add a listener that will listen to window resize and modify progress width accordingly
    window.addEventListener('resize', this.windowResize, false)
  },
  methods: {
    getClickPosition(e){
      e = e || window.e

      // get target element
      let target = e.target || e.srcElement
      if(target.nodeType == 3) target = target.parentNode // fix for a safari bug
      this.wrapperWidth = this.wrapperWidth || target.offsetWidth // set initial progressbar width

      // get the seek width
      let seekWidth = e.offsetX

      // change seek position
      this.progress = (seekWidth / this.wrapperWidth) * 100
    },
    detectMouseDown(e){
      e.preventDefault() // prevent browser from moving objects, following links etc

      // start listening to mouse movements
      this.$refs.listenTo.addEventListener("mousemove", this.getClickPosition, false)
    },
    detectMouseUp(e){
      // stop listening to mouse movements
      this.$refs.listenTo.removeEventListener("mousemove", this.getClickPosition, false)
    },
    windowResize(e){
      let prog = this
      setTimeout(()=>{
        prog.wrapperWidth = prog.$refs.listenTo.offsetWidth
      }, 200)
    }
  }
}
</script>

<style lang="scss">
.progress-wrapper{
  display: block;
  height: 200px;
  margin: 200px 20px;
  position: relative;
  background: #e1e1e1;

  .progress{
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;
    background: teal;
  }
}
</style>

```

Now go out there and build stuff!


## Update ##

So I went ahead and bundled this code up into a Vue plugin.
It's available for use both within the Vue environment and the browser:

{% github xinnks/xns-seek-bar %}


Here's a demo pen:

{% codepen https://codepen.io/xinnks/pen/VwLvLNX %}

&nbsp;
&nbsp;

<center><a href="https://www.buymeacoffee.com/Xinnks" target="_blank"><img src="https://res.cloudinary.com/djx5h4cjt/image/upload/c_scale,h_50/james-sinkala/bmc-button.png" alt="Buy Me A Coffee" style="height: 50px !important;width: auto !important;" ></a></center>