Vue.jsでお手製RangeSlider

## 完成形
それっぽいものを作ってみました。
![range_slider.gif](https://qiita-image-store.s3.amazonaws.com/0/123946/cd8949f3-70ce-316c-840c-00182d852001.gif)

## なぜ実装しようと思ったか
私は普段はReactを使っています。ReactでもComponent外のマウス操作で画面を更新したいときがあります。そんなときはbodyなどに対して`addEventListener`によるイベントを設定し、`ref`を用いてDOMからleftやwidthを取得してstateを更新することがあります。

Vue.jsではどんな感じかなーということでやってみました。

## 実装
```vuejs
<template>
  <div class="range_slider" :style="{width: width + 'px'}">
    <div class="range_wrapper" ref="range" @mousedown="mousedown">
      <div class="range" :style="{'background-color': color}"></div>
    </div>
    <div class="circle" :style="circleStyle" @mousedown="mousedown"></div>
  </div>
</template>

<script>
  export default {
    name: 'RangeSlider',
    props: {
      color: {type: String, default: '#eee'},
      width: {type: Number, default: 200},
      value: {type: Number, default: 0},
      max: {type: Number, default: 100},
      min: {type: Number, default: 0}
    },
    data () {
      return {
        isMoving: false
      }
    },
    methods: {
      moveCircle (e) {
        const width = this.$refs.range.getBoundingClientRect().width
        const rangeLeft = this.$refs.range.getBoundingClientRect().left
        const eventLeft = e.clientX
        const left = eventLeft - rangeLeft

        if (left <= 0) {
          this.$emit('update:value', this.min)
        }
        if (width <= left) {
          this.$emit('update:value', this.max)
        }
        if (left >= 0 && left <= width) {
          const ratio = left / width
          const value = (this.max - this.min) * ratio + this.min
          this.$emit('update:value', Math.round(value))
        }
      },
      mousedown (e) {
        this.isMoving = true
        this.moveCircle(e)
        // mousedownしたタイミングでマウス操作に対するイベントを設定
        document.body.addEventListener('mousemove', this.mousemove)
        document.body.addEventListener('mouseup', this.mouseup)
      },
      mousemove (e) {
        if (this.isMoving) {
          this.moveCircle(e)
        }
      },
      mouseup () {
        this.isMoving = false
        // ドラッグ操作が終わったタイミングでイベントを削除
        document.body.removeEventListener('mousemove', this.mousemove)
        document.body.removeEventListener('mouseup', this.mouseup)
      }
    },
    computed: {
      circleStyle () {
        const ratio = (this.value - this.min) / (this.max - this.min)
        const left = this.width * ratio
        return {
          left: `${left - 10}px`
        }
      }
    }
  }
</script>

<style scoped>
  .range_slider {
    height: 20px;
    margin: 0 auto;
    position: relative;
    display: inline-block;
  }

  .range_wrapper {
    position: relative;
    height: 100%;
    width: 100%;
  }

  .range {
    height: 4px;
    width: 100%;
    background-color: #eee;
    position: absolute;
    top: calc(50% - 2px);
    border-radius: 3px;
  }

  .circle {
    width: 0;
    height: 0;
    top: 0;
    border: 10px solid #DDD;
    border-radius: 50%;
    position: absolute;
  }
</style>

```

## 補足
Reactと違う点は`this.$refs`を使用する点でしょうか。Reactでも`this.refs`として取得する方法があったと思いますが、Reactでは非推奨になっているはずですね。
ここまで実装して思ったのが、HTML5のinputのrangeを使用しても実装できるかもしれないという点です。inputのrangeの円よりも大きい円を上に被せるような実装をすればマウス操作を気にする必要はないかもしれません。