Делаем двухползунковый слайдер на VueUse

от автора

в
Время чтения: 2 мин.

При разработке фильтров товаров, недвижимости или объявлений часто возникает задача выбора диапазона значений. Например, пользователь хочет указать цену от 10 000 до 50 000 рублей.

Первое желание — подключить готовую библиотеку. Но если нужен простой и легковесный компонент, двухползунковый слайдер можно реализовать самостоятельно с помощью Vue и VueUse.

Что должно получиться

Компонент должен поддерживать:

  • два ползунка (минимальное и максимальное значение);
  • перетаскивание мышью;
  • клик по треку для изменения ближайшего ползунка;
  • защиту от пересечения значений;
  • двустороннюю привязку через v-model.

Почему VueUse

VueUse предоставляет множество готовых composable-функций для работы с DOM и событиями браузера.

Для слайдера особенно полезны:

  • useElementBounding
  • useEventListener
  • useMousePressed
  • useClamp

Вместо ручной подписки на события мы получаем более декларативный и читаемый код.

Интерфейс компонента

Начнем с API:

<RangeSlider
    v-model:min="minPrice"
    v-model:max="maxPrice"
    :min="0"
    :max="100000"
/>

Родительский компонент получает два реактивных значения и может использовать их для фильтрации данных.

Расчет позиций

Для отображения ползунков необходимо перевести значения в проценты.

const minPercent = computed(() => {
    return ((props.modelMin - props.min) / range) * 100
})

const maxPercent = computed(() => {
    return ((props.modelMax - props.min) / range) * 100
})

где:

const range = props.max - props.min

Теперь можно позиционировать элементы через CSS.

<div
    class="thumb"
    :style="{ left: `${minPercent}%` }"
/>

Получение координат трека

VueUse позволяет быстро получить размеры элемента.

const track = useTemplateRef('track')

const bounds = useElementBounding(track)

Теперь доступны:

bounds.left.value
bounds.width.value

Это позволит вычислять значение по позиции курсора.

Перетаскивание ползунков

Создаем состояние активного ползунка.

const activeThumb = ref<'min' | 'max' | null>(null)

При нажатии запоминаем, какой элемент пользователь начал двигать.

function startDrag(type: 'min' | 'max') {
    activeThumb.value = type
}

Затем подписываемся на движение мыши.

useEventListener(window, 'mousemove', (event) => {
    if (!activeThumb.value) {
        return
    }

    updateValue(event.clientX)
})

И завершаем перетаскивание.

useEventListener(window, 'mouseup', () => {
    activeThumb.value = null
})

Преобразование координаты в значение

Сначала вычисляем относительную позицию.

const position =
    (clientX - bounds.left.value) /
    bounds.width.value

Ограничиваем диапазон.

const normalized = useClamp(position, 0, 1)

Затем переводим в значение.

const value =
    props.min +
    normalized * range

Запрещаем пересечение ползунков

Минимальное значение не должно стать больше максимального.

Для левого ползунка:

const nextValue = Math.min(
    value,
    props.modelMax
)

Для правого:

const nextValue = Math.max(
    value,
    props.modelMin
)

Так ползунки никогда не поменяются местами.

Клик по треку

Удобно позволить пользователю кликать по полосе вместо перетаскивания.

При клике вычисляем новое значение и определяем ближайший ползунок.

const distanceToMin =
    Math.abs(value - props.modelMin)

const distanceToMax =
    Math.abs(value - props.modelMax)

Обновляем тот, который находится ближе.

if (distanceToMin < distanceToMax) {
    emit('update:min', value)
} else {
    emit('update:max', value)
}

Такое поведение ощущается гораздо естественнее.

Подсветка выбранного диапазона

Ширина активного диапазона вычисляется очень просто.

const selectedStyle = computed(() => ({
    left: `${minPercent.value}%`,
    width: `${maxPercent.value - minPercent.value}%`
}))

Использование:

<div
    class="selected"
    :style="selectedStyle"
/>

Что получилось

В результате мы получили легковесный двухползунковый слайдер без сторонних UI-библиотек.

Плюсы такого подхода:

  • полный контроль над логикой;
  • отсутствие лишних зависимостей;
  • простая кастомизация дизайна;
  • удобная интеграция через v-model;
  • использование VueUse вместо ручной работы с DOM.

Для большинства фильтров товаров, цен, размеров и дат такой реализации более чем достаточно. А если позже потребуется поддержка touch-событий, клавиатуры или вертикального режима, компонент можно постепенно расширять без переписывания архитектуры.


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Сколько будет 9 + 8?