DEV Community

Cover image for Making a seekable progress component a.k.a slider in Vue.
James Sinkala
James Sinkala

Posted on • Edited on • Originally published at jamesinkala.com

Making a seekable progress component a.k.a 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 logo xinnks / xns-audio-player

A simple customizable web music player powered by vue & HTMLAudioElement

xns-audio-player

A persistent audio player powered by vue and some visuals from tailwindcss, v-tooltip, v-progress, vue-ionicons & xns-seek-bar

xns-audio-player

Adding playlists

To add a new playlist call the addPlaylist() method from within a method or the mounted hook

    ...
        this.addPlaylist({
            title: 'Playlist 1',
            songs: this.demoPlaylist
        })
    ...
Enter fullscreen mode Exit fullscreen mode

Where demoPlaylist is an array of song objects in the following format

{ audio: "link_to_audio_file.mp3", artist: "Artist's name", title: "Song title", album: "album name", cover: "link_to_album_or_song_cover_image.jpg"}
Enter fullscreen mode Exit fullscreen mode

Buy Me A Coffee

Project setup

npm install

Compiles and hot-reloads for development

npm run serve

Compiles and minifies for production

npm run build



.

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

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) 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.



<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>


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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



<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>


Enter fullscreen mode Exit fullscreen mode

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.



<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>


Enter fullscreen mode Exit fullscreen mode

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.



<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>


Enter fullscreen mode Exit fullscreen mode

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:



<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>



Enter fullscreen mode Exit fullscreen mode

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 logo xinnks / xns-seek-bar

A seekable progress bar component for Vue.js

xns-seek-bar

A seekable progress bar component for Vue.js

xns-seek-bar

install

$ npm i xns-seek-bar
Enter fullscreen mode Exit fullscreen mode

Import & initiate plugin on your entry js file

import XnsSeekBar from 'xns-seek-bar'

Vue.use(XnsSeekBar)
Enter fullscreen mode Exit fullscreen mode

In Browser

// Latest update
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xns-seek-bar/dist/index.umd.js"></script>
Enter fullscreen mode Exit fullscreen mode

Example

<xns-seek-bar :bar-color="'#ffdd00'" :current-value="33" :total-value="100"></xns-seek-bar>
Enter fullscreen mode Exit fullscreen mode

Options

Option Type Required Default
currentValue Number false 0
totalValue Number false 300
listen Boolean false true
barHeight Number false 0.5
barColor String (Hex) false false
barShadeColor String (Hex) false false
intensity Number (0.1 - 1)) false 0

Options Details

listen : Enable touch / tap.

Events

seekedTo Returns a Number representing value of seeked position.




Here's a demo pen:

 
 

Buy Me A Coffee

Top comments (0)