VGSelect

Стилизованный HTML Select

AJAX и предзагрузка

Реальный поиск, пагинация, пустой ответ, ошибка и сохранённый выбор.

VGSelect сохраняет исходный <select> для формы и строит интерфейс рядом с ним. Каждый пример запускается один раз после появления HTML. Параметры из data-* читаются при инициализации; библиотека сама не сканирует страницу в поисках полей. По умолчанию модуль использует position: 'auto': список выбирает сторону с большим свободным местом.

Оба примера используют тестовый GET endpoint /api/select/demo с параметрами q, page и per_page. Это фиксированный справочник городов, а не production API. Введите м для нескольких страниц, empty для пустого ответа или error для HTTP 503. После ошибки введите ка, чтобы проверить восстановление. Индикатор загрузки виден на время запроса; для его наблюдения можно включить ограничение скорости сети.

Поиск и «Загрузить ещё»

Три результата на страницу, задержка 300 мс. Журнал под полем показывает начало поиска, результат, пагинацию и ошибку. Сообщения журнала — UI демо, а не встроенный alert VGSelect.

Откройте список и введите запрос.
HTML
<div class="select-demo">
  <label for="select-remote">Удалённый поиск города</label>
  <select id="select-remote" class="vg-select" name="remote_city" data-select-demo
    data-placeholder="Введите м, empty или error">
    <option value="" selected hidden></option>
  </select>
  <output id="select-remote-status" aria-live="polite">Откройте список и введите запрос.</output>
</div>
JavaScript
/**
 * Описание: рабочий пример VGSelect — remote.
 * Возможности: инициализация и демонстрация публичного контракта на исходном select.
 */
import {VGSelect} from 'vgapp';

function initSelectDemo() {
    const select = document.querySelector('#select-remote');
    if (!select || select.dataset.inited === 'true') return;
    const status = document.querySelector('#select-remote-status');
    VGSelect.init(select, {
        search: {remote: true, route: '/api/select/demo', pagination: true, perpage: 3, minterm: 1, delay: 300},
        onSearch(wrapper, payload) {
            if (!payload || !('term' in payload)) return;
            status.textContent = payload.data
                ? (payload.data.results.length ? 'Получено: ' + payload.data.results.length : 'Ничего не найдено')
                : 'Ожидание поиска: ' + payload.term;
        },
        onLoadNext(wrapper, payload) {
            status.textContent = 'Загружена страница ' + payload.page;
        }
    });
    select.nextElementSibling.addEventListener('vg.select.error', event => {
        status.textContent = 'Ошибка: ' + event.detail.error + '. Введите другой запрос.';
    });
    select.addEventListener('change', () => { status.textContent = 'Выбрано: ' + select.value; });
}

initSelectDemo();
CSS
.select-demo {
    display: grid;
    gap: 12px;
    max-width: 560px;
    min-width: 0;
}
.select-demo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.select-demo output {
    overflow-wrap: anywhere;
}
.select-demo-log {
    max-height: 240px;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    padding: 12px;
    border: 1px solid var(--vg-border-default);
    border-radius: var(--vg-radius-md);
    background: var(--vg-surface-muted-bg);
    color: var(--vg-text-primary);
}
.select-demo-placement {
    padding-top: 180px;
}
.vg-select.select-demo-compact {
    --vg-select-current-font-size: 14px;
    --vg-select-current-padding-top: 8px;
    --vg-select-current-padding-bottom: 8px;
    --vg-select-current-border-radius: 10px;
    --vg-select-list-hover-background-color: var(--vg-surface-muted-bg);
}
.module-select-content {
    min-width: 0;
}
.module-select-reference {
    margin-top: 32px;
    overflow-wrap: anywhere;
}
.module-select-reference dt {
    margin-top: 16px;
    font-weight: 600;
}
.module-select-reference dd {
    margin: 6px 0 0;
}

Редактирование с сохранённым значением

Калуга загружена заранее и помечена data-preserve. Введите «м»: сохранённая опция останется в списке при замене результатов. Для других выбранных значений такой гарантии нет без data-preserve.

Сохранённое значение: Калуга (9).
HTML
<div class="select-demo">
  <label for="select-preloaded">Город в редактируемой записи</label>
  <select id="select-preloaded" class="vg-select" name="saved_city" data-select-demo>
    <option value="9" selected data-preserve>Калуга</option>
  </select>
  <output id="select-preloaded-status" aria-live="polite">Сохранённое значение: Калуга (9).</output>
</div>
JavaScript
/**
 * Описание: рабочий пример VGSelect — preloaded.
 * Возможности: инициализация и демонстрация публичного контракта на исходном select.
 */
import {VGSelect} from 'vgapp';

function initSelectDemo() {
    const select = document.querySelector('#select-preloaded');
    if (!select || select.dataset.inited === 'true') return;
    const status = document.querySelector('#select-preloaded-status');
    VGSelect.init(select, {
        search: {remote: true, route: '/api/select/demo', pagination: true, perpage: 3},
        onSearch(wrapper, payload) {
            if (payload?.data) status.textContent = 'Результатов: ' + payload.data.results.length + '; текущее значение: ' + select.value;
        }
    });
    select.nextElementSibling.addEventListener('vg.select.error', () => {
        status.textContent = 'Ошибка запроса. Сохранённая опция остаётся доступной.';
    });
    select.addEventListener('change', () => { status.textContent = 'Выбрано: ' + select.value; });
    // data-preserve сохраняет именно эту опцию, а не любой последующий выбор.
    // addOptions не удаляет дубликаты id: production endpoint должен исключать уже сохранённые id.
}

initSelectDemo();
CSS
.select-demo {
    display: grid;
    gap: 12px;
    max-width: 560px;
    min-width: 0;
}
.select-demo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.select-demo output {
    overflow-wrap: anywhere;
}
.select-demo-log {
    max-height: 240px;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    padding: 12px;
    border: 1px solid var(--vg-border-default);
    border-radius: var(--vg-radius-md);
    background: var(--vg-surface-muted-bg);
    color: var(--vg-text-primary);
}
.select-demo-placement {
    padding-top: 180px;
}
.vg-select.select-demo-compact {
    --vg-select-current-font-size: 14px;
    --vg-select-current-padding-top: 8px;
    --vg-select-current-padding-bottom: 8px;
    --vg-select-current-border-radius: 10px;
    --vg-select-list-hover-background-color: var(--vg-surface-muted-bg);
}
.module-select-content {
    min-width: 0;
}
.module-select-reference {
    margin-top: 32px;
    overflow-wrap: anywhere;
}
.module-select-reference dt {
    margin-top: 16px;
    font-weight: 600;
}
.module-select-reference dd {
    margin: 6px 0 0;
}

Формат ответа

JSON (формат ответа)
{
    "results": [{ "id": "1", "text": "Москва" }],
    "pagination": { "current_page": 1, "total_pages": 3 }
}

Используйте уникальные строковые id. Для пустого ответа: results: [] и total_pages: 1. Названия параметров в JavaScript — minterm и perpage, в нижнем регистре. Сервер получает per_page. Поиск запускается вводом, не открытием списка.