Ok so you need to create a styled form element, but you are too lazy (or smart) to copy that markup all the time and lets be honest, no one does…

It just so happens that you are using Vue in your project, well this is a perfect opportunity to bond. It doesn’t matter if you are using it as a child component or as a global one, the concept of the element is the same.

I will be using .vue files with a Webpack setup (because that’s pretty much what any normal Vue user does), but you could use the non transpiled components, just split the template and script tags into a their respective places.

First things first, lets start creating our quantity picker element.

// quantityPicker.vue

<template>
  <div class="quantity-picker">
    <div class="picker-value">
      {{ innerValue }}
    </div>
    <div class="picker-controls">
      <button class="picker-control up" :disabled="value === max" @click.prevent="inc">UP</button>
      <button class="picker-control down" :disabled="value === min" @click.prevent="dec">DOWN</button>
    </div>
  </div>
</template>

Pretty basic and should be familiar with everything here. We are showing our innerValue in .picker-value and binding .picker-control buttons to their respective methods. We are using the .prevent modifier so we don’t accidentally submit a parent form if we put our component in one.

Lets move to the fun stuff.

// quantityPicker.vue
<script>
  export default {
    data () {
      return {
        innerValue: ''
      }
    },
    mounted () {
      this.innerValue = this.shouldSetValue(this.value, this.min, this.max)
    },
    props: {
      value: {
        type: Number,
        default: 1
      },
      min: {
        default: 1,
        type: Number
      },
      max: {
        default: 1,
        type: Number
      },
      step: {
        default: 1,
        type: Number
      }
    },
    watch: {
      value (newValue) {
        this.innerValue = this.shouldSetValue(newValue, this.min, this.max)
      }
    },
    methods: {
      inc () {
        const updatedVal = this.innerValue + this.step
        if (updatedVal <= this.max) {
          this.innerValue = updatedVal
          this.$emit('input', this.innerValue)
        }
      },
      dec () {
        const updatedVal = this.innerValue - this.step
        if (updatedVal >= this.min) {
          this.innerValue = updatedVal
          this.$emit('input', this.innerValue)
        }
      },
      shouldSetValue (val, min, max) {
        return Math.max(min, Math.min(val, max))
      }
    }
  }
</script>

OK so in our data we have innerValue which is being set to an empty string. This will be our internal Value (obviously), that we will be modifying.

On the mounted hook we are setting  innervalue to this.value which is a prop we will be getting from our parent component. This will be an initial value for our picker, but we are having some validations right there.  Our shouldSetValue method checks whether its OK to set that value compared to our min and max ranges.

Our props are self explanatory.

The watch property watches over the value prop so if the parent decides to change its value by any means we should know if we should update ours as well. 

Methods contains our inc, dec and shouldSetvalue methods. The thing to note here is, we are checking if the action of addition or subtraction sing the step value is in the required range. If its OK we set it and $emit an event, but not any event, an input event. Why? Well because if you have read your Vue docs you should know that you can v-model over custom components that emit input event and bind that to a value on the parent component.

shouldSetValue is a simple validation for our data. It returns the passed value if its in range or the max/min value if its over/under that range.

<style lang="scss" rel="stylesheet/scss">

.quantity-picker {
  display: flex;
  width: 60px;
  height: 35px;
  border: 1px solid #0C93BB;
  background: #fff;
  color: #0C93BB;
  margin-bottom: 10px;
  .picker {
    &-value, &-controls {
      width: 50%;
    }
    &-controls {
      display: flex;
      flex-flow: column;
      border-left: 1px solid #0C93BB;
    }
    &-control {
      flex: 1 1 50%;
      color: #0C93BB;
      cursor: pointer;
      &:first-child {
        border-bottom: 1px solid #0C93BB;
      }
    }
    &-value {
      font-weight: bold;
      font-size: 1em;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  }
}

</style>

Some basic styling here, nothing fancy.

To use our freshly created component we have to import it and set it as a global component or as a child one.

// app.js
import Vue from 'vue'
import quantityPicker './vue-components/quantityPicker.vue'
Vue.component('quantity-picker', quantityPicker ) //globally register our component

// Or if we want to use it as a child component and not set it globally 

const app = new Vue({
  el: '#app',
  components: [quantityPicker]
})

Now we simply use our component everywhere we need to

// some form

<quantity-picker :max="5" :min="2" :value="6"></quantity-picker>

OK I tried to break it by passing a higher value than max allows us, but thanks to shouldSetvalue we know that’s not a problem. I am using the colons  with the props to pass real number instead of strings. If you use it as a child component you will be passing data so they will be mandatory.

And that’s it. We have a fully working custom quantity picker that can be used as a child component or even just as a normal quantity picker in a normal form (that would require some sort of input, like a :hidden input)