При разработке фильтров товаров, недвижимости или объявлений часто возникает задача выбора диапазона значений. Например, пользователь хочет указать цену от 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-событий, клавиатуры или вертикального режима, компонент можно постепенно расширять без переписывания архитектуры.


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