VGRangeSlider

Слайдер диапазона значений

Форма и API

Внешние поля, форматирование, методы, события и жизненный цикл.

Слайдеры используют нативные input[type="range"]: перетаскивайте ручку или используйте Tab и стрелки, Home / End. Примеры подключены к общей теме VGApp и OKAUX 1.0.2. Ручную инициализацию выполняйте после появления разметки.

Форматирование и внешние поля формы

Модуль обновляет два существующих hidden-input вне контейнера и форматированный output. Кнопка показывает FormData локально: запрос на сервер не отправляется.

Бюджет проекта
Нажмите «Показать FormData». Запрос на сервер не отправляется.
HTML
<form id="range-form" class="range-demo">
    <div id="range-budget-label" class="range-demo-label">Бюджет проекта</div>
    <div id="range-budget" role="group" aria-labelledby="range-budget-label"></div>
    <input type="hidden" id="range-budget-from" name="budget_from">
    <input type="hidden" id="range-budget-to" name="budget_to">
    <output id="range-budget-value" class="range-demo-result" aria-live="polite"></output>
    <div class="range-demo-actions">
        <button type="submit" class="btn btn-primary btn-sm">Показать FormData</button>
        <button id="range-budget-reset" type="button" class="btn btn-surface btn-sm">Сбросить диапазон</button>
    </div>
    <pre id="range-form-result" class="range-demo-result" aria-live="polite">Нажмите «Показать FormData». Запрос на сервер не отправляется.</pre>
</form>
JavaScript
/**
 * Описание: рабочий пример VGRangeSlider — form.
 * Возможности: форматирование валюты, внешние hidden-поля и локальный просмотр FormData.
 */
import { VGRangeSlider } from 'vgapp';

function initRangeDemo() {
    const root = document.querySelector('#range-budget');
    if (!root || VGRangeSlider.getInstance(root)) return;

    const formatter = new Intl.NumberFormat('ru-RU', {
        style: 'currency', currency: 'RUB', maximumFractionDigits: 0
    });
    const slider = VGRangeSlider.init(root, {
        range: true, min: 0, max: 100000, step: 1000, start: [20000, 80000],
        input: { min: '#range-budget-from', max: '#range-budget-to' },
        output: '#range-budget-value',
        formatValue: value => formatter.format(value)
    });
    root.querySelector('.vg-range-slider__input--from').setAttribute('aria-label', 'Минимальный бюджет');
    root.querySelector('.vg-range-slider__input--to').setAttribute('aria-label', 'Максимальный бюджет');
    document.querySelector('#range-form').addEventListener('submit', (event) => {
        event.preventDefault();
        const values = Object.fromEntries(new FormData(event.currentTarget));
        document.querySelector('#range-form-result').textContent = JSON.stringify(values, null, 2);
    });
    document.querySelector('#range-budget-reset').addEventListener('click', () => slider.setValue([20000, 80000]));
}

initRangeDemo();
CSS
/* Композиция демо; трек, ручки, шкала и подсказки оформляются VGRangeSlider. */
.range-demo {
    min-width: 0;
    padding: 1rem;
}
.range-demo-label {
    display: block;
    margin-bottom: 2.5rem;
    font-weight: 600;
}
.range-demo-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    margin-top: 2rem;
}
.range-demo-result {
    display: block;
    margin: 1.5rem 0 0;
    padding: .75rem;
    border: 1px solid var(--vg-border-default);
    border-radius: .5rem;
    color: var(--vg-text-primary);
    background: var(--vg-background-secondary);
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}
.range-demo-log {
    max-height: 15rem;
    overflow-y: auto;
}

Методы, события и пересоздание

Сравните обычную и тихую установку, отключите ручки и прочитайте значение. Пересоздание вызывает dispose, заменяет принадлежащий примеру контейнер и создаёт новый экземпляр.

Диапазон под управлением API

HTML
<div class="range-demo">
    <div id="range-api-label" class="range-demo-label">Диапазон под управлением API</div>
    <div id="range-api-slider" role="group" aria-labelledby="range-api-label"></div>
    <div id="range-api-actions" class="range-demo-actions">
        <button type="button" class="btn btn-primary btn-sm" data-range-action="set">Установить [70, 30]</button>
        <button type="button" class="btn btn-surface btn-sm" data-range-action="silent">Тихо [40, 60]</button>
        <button type="button" class="btn btn-surface btn-sm" data-range-action="disable">Отключить</button>
        <button type="button" class="btn btn-surface btn-sm" data-range-action="enable">Включить</button>
        <button type="button" class="btn btn-surface btn-sm" data-range-action="read">Прочитать значение</button>
        <button type="button" class="btn btn-surface btn-sm" data-range-action="recreate">Пересоздать</button>
    </div>
    <output id="range-api-value" class="range-demo-result" aria-live="polite"></output>
    <pre id="range-api-log" class="range-demo-result range-demo-log" aria-live="polite" aria-label="Журнал слайдера"></pre>
</div>
JavaScript
/**
 * Описание: рабочий пример VGRangeSlider — api.
 * Возможности: методы, события, тихое обновление и безопасное пересоздание.
 */
import { VGRangeSlider } from 'vgapp';

function initRangeDemo() {
    let root = document.querySelector('#range-api-slider');
    if (!root || VGRangeSlider.getInstance(root)) return;

    const log = document.querySelector('#range-api-log');
    const entries = [];
    const write = (text) => {
        entries.unshift(text);
        log.textContent = entries.slice(0, 12).join('\n');
    };
    let slider;
    const mount = () => {
        ['init', 'input', 'change', 'update'].forEach((name) => {
            root.addEventListener('vg.rangeslider.' + name, (event) => {
                write(name + ': ' + JSON.stringify(event.detail.value));
            });
        });
        slider = VGRangeSlider.init(root, {
            range: true, min: 0, max: 100, step: 5, start: [20, 80],
            output: '#range-api-value',
            onUpdate: (element, detail) => write('onUpdate callback: ' + JSON.stringify(detail.value))
        });
        root.querySelector('.vg-range-slider__input--from').setAttribute('aria-label', 'Нижняя граница API');
        root.querySelector('.vg-range-slider__input--to').setAttribute('aria-label', 'Верхняя граница API');
    };
    mount();
    document.querySelector('#range-api-actions').addEventListener('click', (event) => {
        const action = event.target.closest('[data-range-action]')?.dataset.rangeAction;
        if (action === 'set') slider.setValue([70, 30]);
        if (action === 'silent') slider.setValue([40, 60], { silent: true });
        if (action === 'disable') { slider.disable(); write('disable'); }
        if (action === 'enable') { slider.enable(); write('enable'); }
        if (action === 'read') write('getValue: ' + JSON.stringify(slider.getValue()));
        if (action === 'recreate') {
            slider.dispose();
            const replacement = root.cloneNode(false);
            root.replaceWith(replacement);
            root = replacement;
            write('dispose → новый контейнер');
            mount();
        }
    });
}

initRangeDemo();
CSS
/* Композиция демо; трек, ручки, шкала и подсказки оформляются VGRangeSlider. */
.range-demo {
    min-width: 0;
    padding: 1rem;
}
.range-demo-label {
    display: block;
    margin-bottom: 2.5rem;
    font-weight: 600;
}
.range-demo-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    margin-top: 2rem;
}
.range-demo-result {
    display: block;
    margin: 1.5rem 0 0;
    padding: .75rem;
    border: 1px solid var(--vg-border-default);
    border-radius: .5rem;
    color: var(--vg-text-primary);
    background: var(--vg-background-secondary);
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}
.range-demo-log {
    max-height: 15rem;
    overflow-y: auto;
}

Публичный API

VGRangeSlider.init(element, params)
Создаёт или возвращает существующий экземпляр. Повторный вызов не применяет новые параметры.
getValue()
Число для одной ручки; массив [from, to] для диапазона.
setValue(value, options)
Приводит значения к границам и шагу, сортирует пару. По умолчанию генерирует update; emit: 'change' выбирает change, silent: true обновляет UI и поля без события и callback.
enable() / disable()
Включает или отключает нативные ручки. Программная установка значения остаётся доступной.
dispose()
Удаляет обработчики и экземпляр, но не восстанавливает исходный DOM. В примере старый контейнер явно заменяется перед повторной инициализацией.
input / name
input.min и input.max связывают существующие поля; если они не найдены, создаются hidden-input с именами name.min и name.max. Создавайте внешние поля до инициализации.
output / formatValue
output.target, output.min, output.max получают форматированный текст. formatValue(value, element) заменяет стандартный prefix + value + suffix; поля формы сохраняют числа.
Ограничения
connect пока не отключает заливку. Нативный reset формы не синхронизирует состояние экземпляра автоматически — используйте явный setValue, как в примере.

События и callbacks

vg.rangeslider.init
После построения и первой синхронизации. Подписывайтесь до ручного init, если нужно получить это событие.
vg.rangeslider.input / change
При изменении ручки и при завершении изменения. События возникают на исходном input или контейнере.
vg.rangeslider.update
После программного setValue с настройками по умолчанию.
event.detail
value, from, to, min, max, step, status, instance. status содержит by, sourceValue, percent, tone, label и состояния обеих границ.
onInit / onInput / onChange / onUpdate
Callback получает (element, detail); this указывает на экземпляр. События информационные: preventDefault не отменяет изменение значения.