JSFiddle - React, Tailwind, and code Playground

by Константин Дралюк

HTML

<div id="app">
  <div>min: 0, max: 99, step: 1</div>
  <ui-number v-model="firstNumberValue" :min="0" :max="99"></ui-number>
  <div>min: 0, max: 50, step: 0.1</div>
  <ui-number v-model="secondNumberValue" :min="0" :max="50" :step=".1"></ui-number>
  <div>min: 1, max: 2, step: .01</div>
  <ui-number v-model="thirdNumberValue" :min="1" :max="2" :step=".01"></ui-number>
</div>

<script type="text/x-template" id="ui-number">
  <div class="ui-number">
    <button @click="stepDown" :disabled="disabled">-</button>
    <input
      ref="number"
      type="text"
      v-bind="$attrs"
      v-on="$listeners"
      :value="value"
      @input="input"
      @keydown.down="stepDown"
      @keydown.up="stepUp"
      :disabled="disabled"
    />
    <button @click="stepUp" :disabled="disabled">+</button>
  </div>
</script>

SCSS

.ui-number, .ui-number * {
  box-sizing: border-box;

}

.ui-number {
  display: inline-flex;
  border-radius: 4px;
  overflow: hidden;
  &__input {
    border-radius: 0;
    outline: none;
    border: 1px solid #000;
    width: 100px;
    padding: 0 5px;
    height: 30px;
  }
  &__control {
    appearance: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    border: none;
    background: #000;
    color: #fff;
  }
}

Vue

Vue.component('ui-number', {
	template: '#ui-number',
	props: {
    min: {
      type: Number,
      default: Number.MIN_VALUE
    },
    max: {
      type: Number,
      default: Number.MAX_VALUE
    },
    step: {
      type: Number,
      default: 1
    },
    disabled: {
    	type: Boolean,
      default: false
    },
    value: {
      default: 0
    }
  },
  computed: {
    stepSize () {
      const afterDot = String(this.step).split('.')[1]
      return afterDot ? afterDot.length : 1
    }
  },
  methods: {
  	input (e) {
	    this.$nextTick(() => {
      	const value = e.target.value
      	const formatted = this.format(value)
        const keepValue = value == formatted && value.includes('.')
      	this.$emit('input', keepValue ? value : formatted)
      })
    },
  	blur (e) {
	    this.$nextTick(() => {
      	this.$emit('input', this.format(e.target.value))
      })
    },
    stepDown () {
      let value = Number(this.value) - this.step
      if (value < this.min) value = this.min

      this.$emit('input', this.format(value))
    },
    stepUp () {
      let value = Number(this.value) + this.step
      if (value > this.max) value = this.max

      this.$emit('input', this.format(value))
    },
    format (value) {
      if (value < this.min) value = this.min
      if (value > this.max) value = this.max

      const float = parseFloat(String(value).replace(/\D^.^-/g, '')).toFixed(this.stepSize)
      return Number(float) || 0
    }
  }
})

new Vue({
	el: '#app',
  data: () => ({
  	firstNumberValue: 0,
  	secondNumberValue: 0,
  	thirdNumberValue: 1,
  })
})