vue.jsvuejs3vuelidate

A small price slider bug when entering another value for the first time


I have a price slider and two inputs for min and max price. I have a small bug. When I try to enter another value in the min price field and start erasing the previous one, it immediately changes to 50 (the least value of a price slider), and only after the input "switches", I can enter a new min price.

I use a multi-range slider component by developergovindgupta.

This is how this code's working at the moment: https://streamable.com/r9rfsh.

Here's my price slider with inputs:

<script setup>
import { computed, ref, reactive, watch } from "vue";
import { useVuelidate } from "@vuelidate/core";
import { minValue, maxValue } from "@vuelidate/validators";
import MultiRangeSlider from "multi-range-slider-vue";

const props = defineProps({
  listPrice: {
    type: Object
  },
  modelValue: {
    type: Object
  },
})

const emit = defineEmits(["update:modelValue"])

function updatePrice(e) {
  valsPrices.min = e.minValue;
  valsPrices.max = e.maxValue;
}

const rangeMargin = 50;

const listPrices = reactive(JSON.parse(JSON.stringify(props.listPrice)))
const valsPrices = reactive(JSON.parse(JSON.stringify(props.modelValue)))

const rules = computed(() => ({
  min: {
    minValue: minValue(listPrices.min),
    maxValue: maxValue(
        ((valsPrices.max - rangeMargin) >= listPrices.min) ?
            valsPrices.max - rangeMargin :
            listPrices.min
    )
  },
  max: {
    minValue: minValue(
        ((valsPrices.min + rangeMargin) <= listPrices.max) ?
            valsPrices.min + rangeMargin :
            listPrices.max
    ),
    maxValue: maxValue(listPrices.max)
  },
}))

const v = useVuelidate(rules, valsPrices)

function cleanMinPrice() {
  if (v.value.min.minValue.$invalid) {
    valsPrices.min = listPrices.min
    console.log(v.value.min.$model)
  }
  if (v.value.min.maxValue.$invalid) {
    valsPrices.min = valsPrices.max - rangeMargin
  }
}

function cleanMaxPrice() {
  console.log(valsPrices.max)
  if (v.value.max.minValue.$invalid) {
    valsPrices.max = valsPrices.min + rangeMargin
  }
  if (v.value.max.maxValue.$invalid) {
    valsPrices.max = listPrices.max
  }
}

watch(valsPrices, async (val) => {
  emit("update:modelValue", val);
}, {
  deep: true,
  immediate: true
})
</script>

<template>
  <h6>Price</h6>
  <div class="d-flex justify-content-between">
    <div class="d-flex align-items-center">
      <label for="minPrice" class="form-label m-0">Min:&nbsp;&nbsp;</label>
      <input type="number"
             class="form-control form-control-sm"
             id="minPrice"
             v-model="v.min.$model"
             @blur="cleanMinPrice">
    </div>
    <div class="d-flex align-items-center">
      <label for="maxPrice" class="form-label m-0">Max:&nbsp;&nbsp;</label>
      <input type="number"
             class="form-control form-control-sm"
             id="maxPrice"
             v-model="v.max.$model"
             @blur="cleanMaxPrice">
    </div>
  </div>
  <MultiRangeSlider
    baseClassName="multi-range-slider"
    :min="props.listPrice.min"
    :max="props.listPrice.max"
    :minValue="valsPrices.min"
    :maxValue="valsPrices.max"
    :ruler="false"
    :rangeMargin="rangeMargin"
    @input="updatePrice"
  />
</template>

This is how the component is used inside a filter component:

<script setup>
import {reactive, watch} from "vue";
import PriceSlider from "./PriceSlider.vue";

const props = defineProps({
  filterList: Object,
  modelValue: Object,
  loading: Boolean,
})

const emit = defineEmits(["filter", "update:modelValue"])

let filterVals = reactive(JSON.parse(JSON.stringify(props.modelValue)))

watch(
    filterVals,
    (value) => {
      emit("update:modelValue", value);
    },
    { deep: true, immediate: true }
)
</script>

<template>
  <section class="col-3">
    ...
    <div class="mb-3">
      <h3 class="fs-3">Price</h3>
      <PriceSlider
          :listPrice="filterList.price"
          v-model="filterVals.price" />
    </div>
  </section>
</template>

These are the filterList and filterValues I pass to the slider in Products.vue - the page where the products are displayed. filterList contains all possible filter values, like all colors, categories and the least and greatest values of the price slider. filterVals contains filter options selected by a user.

const filterList = reactive({
  price: {
    min: 50,
    max: 900
  }
})

const filterValues = reactive({
  price: {
    min: 80,
    max: 600
  }
})

They're declared inside the Products.vue component. This is how the filter component is used inside the products page.

<script setup>
import HeaderOther from "../components/layout/HeaderOther.vue";
import Filter from "../components/Products/Filter.vue";
import { reactive } from "vue";

const filterList = reactive({
  price: {
    min: 50,
    max: 900
  }
})

const filterValues = reactive({
  price: {
    min: 80,
    max: 600
  }
})

function getFilterResults() {
  console.log(filterValues.value)
}
</script>

<template>
  <div class="container-fluid">
    <HeaderOther></HeaderOther>
    <div class="row mw-2000 mx-5 my-0">
      <Filter :filterList="filterList"
              v-model="filterValues"
              @filter="getFilterResults" />
    </div>
  </div>
</template>

I've found out that the issue lies in the function that updates min and max price when the slider's thumb is dragged:

<script setup>
...
function updatePrice(e) {
  valsPrices.min = e.minValue;
  valsPrices.max = e.maxValue;
}
...
</script>

<template>
  ...
  <MultiRangeSlider
    baseClassName="multi-range-slider"
    :min="props.listPrice.min"
    :max="props.listPrice.max"
    :minValue="valsPrices.min"
    :maxValue="valsPrices.max"
    :ruler="false"
    :rangeMargin="rangeMargin"
    @input="updatePrice"
  />
</template>

But I don't know what to do to solve this problem. Thanks in advance!

UPDATE 1

Unfortunately, after changing the event, another problem appeared. valsPrice.min and valsPrice.max don't change when the thumb is dragged. It looks like this right now: https://streamable.com/vg8feh. This is the MultiRangeSlider component now:

<MultiRangeSlider
  baseClassName="multi-range-slider"
  :min="props.listPrice.min"
  :max="props.listPrice.max"
  :minValue="valsPrices.min"
  :maxValue="valsPrices.max"
  :ruler="false"
  :rangeMargin="rangeMargin"
  @keyDown="updatePrice"
/>

Solution

  • The solution for me was using the <InputNumber> PrimeVue component. It updates and validates the value in its v-model on the blur event, not on the input one. It doesn't mean one should use specifically this component to solve a similar issue, the main thing here is that the value in the v-model should be updated and validated, not only validated, on blur.

    Side note: Vuelidate turned out to be redundant in my case.

    Here's the sandbox with the solved issue.