Работа с URL-параметрами

В веб-приложениях выпадающие списки часто становятся частью состояния интерфейса, которое требуется сохранять при обновлении страницы, передавать через ссылки или восстанавливать при навигации. При использовании Slim Select ключевым механизмом становится синхронизация выбранных значений с параметрами URL через URLSearchParams.

URL в данном подходе выступает как источник истины для состояния интерфейса. Это позволяет реализовать глубокие ссылки, повторяемые состояния фильтров и восстановление интерфейса после перезагрузки страницы.

Основной инструмент работы с параметрами — стандартный объект браузера:

const params = new URLSearchParams(window.location.search);

Чтение параметров при инициализации

При загрузке страницы первым шагом извлекаются параметры URL и преобразуются в значения, подходящие для Slim Select.

Пример базовой инициализации для одиночного выбора:

const params = new URLSearchParams(window.location.search);
const category = params.get('category');

const select = new SlimSelect({
  select: '#category-select',
  placeholder: 'Выбор категории'
});

if (category) {
  select.set(category);
}

В этом сценарии значение из URL напрямую соответствует value опции <option>.

HTML-структура:

<select id="category-select">
  <option value="books">Книги</option>
  <option value="music">Музыка</option>
  <option value="games">Игры</option>
</select>

Запись выбранного значения в URL

После изменения выбора необходимо синхронизировать состояние компонента с адресной строкой.

Slim Select предоставляет событие afterChange, которое используется для отслеживания изменений:

const select = new SlimSelect({
  select: '#category-select',
  afterChange: (newVal) => {
    const params = new URLSearchParams(window.location.search);

    if (newVal.value) {
      params.set('category', newVal.value);
    } else {
      params.delete('category');
    }

    const newUrl = `${window.location.pathname}?${params.toString()}`;
    history.replaceState(null, '', newUrl);
  }
});

Использование replaceState предотвращает засорение истории браузера при каждом изменении.


Работа с множественным выбором

При включённом режиме multiple Slim Select возвращает массив значений, что требует иной стратегии сериализации в URL.

HTML:

<select id="tags-select" multiple>
  <option value="frontend">Frontend</option>
  <option value="backend">Backend</option>
  <option value="devops">DevOps</option>
</select>

Инициализация и запись:

const select = new SlimSelect({
  select: '#tags-select',
  afterChange: (selected) => {
    const params = new URLSearchParams(window.location.search);

    const values = selected.map(item => item.value);

    if (values.length) {
      params.set('tags', values.join(','));
    } else {
      params.delete('tags');
    }

    history.replaceState(null, '', `${location.pathname}?${params}`);
  }
});

Чтение при загрузке:

const params = new URLSearchParams(window.location.search);
const tags = params.get('tags');

if (tags) {
  const values = tags.split(',');
  select.set(values);
}

Кодирование и декодирование значений

При передаче данных через URL необходимо учитывать ограничения на символы. Значения должны быть корректно закодированы.

Используется стандартное поведение URLSearchParams, но при ручной обработке массивов применяется явное кодирование:

const encoded = encodeURIComponent(values.join(','));
params.set('tags', encoded);

И декодирование:

const raw = params.get('tags');
const values = decodeURIComponent(raw).split(',');

Такой подход полезен при наличии пробелов, Unicode-символов или специальных знаков.


История браузера: pushState и replaceState

При синхронизации Slim Select с URL важно различать два режима обновления истории:

  • replaceState — обновление текущей записи без добавления новой
  • pushState — создание новой записи истории

Использование pushState оправдано при фильтрации, где каждое изменение считается отдельным состоянием:

history.pushState(null, '', newUrl);

Однако при частых изменениях (например, при выборе нескольких тегов) предпочтительным является replaceState, чтобы избежать перегрузки истории.


Обработка события popstate

При использовании истории браузера необходимо учитывать возврат назад и вперёд.

window.addEventListener('popstate', () => {
  const params = new URLSearchParams(window.location.search);

  const category = params.get('category');
  const tags = params.get('tags');

  if (category) {
    selectCategory.set(category);
  }

  if (tags) {
    selectTags.set(tags.split(','));
  }
});

Slim Select обновляется программно через set, что позволяет синхронизировать интерфейс с URL без повторного срабатывания событий изменения.


Масштабирование на несколько селектов

При наличии нескольких компонентов состояние URL становится агрегированным объектом.

Пример структуры:

?category=books&tags=frontend,backend&sort=asc

Общий паттерн синхронизации:

function updateUrl(paramsObj) {
  const params = new URLSearchParams();

  Object.entries(paramsObj).forEach(([key, value]) => {
    if (!value || (Array.isArray(value) && !value.length)) return;

    params.set(
      key,
      Array.isArray(value) ? value.join(',') : value
    );
  });

  history.replaceState(null, '', `${location.pathname}?${params}`);
}

Каждый Slim Select инстанс обновляет только свою часть состояния:

categorySelect.onCha nge = (val) => {
  state.category = val.value;
  updateUrl(state);
};

tagsSelect.onCha nge = (val) => {
  state.tags = val.map(v => v.value);
  updateUrl(state);
};

Восстановление состояния при частичной или некорректной URL-разметке

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

const allowedValues = new Set(['books', 'music', 'games']);

const raw = params.get('category');

if (allowedValues.has(raw)) {
  select.set(raw);
}

Для множественных значений:

const allowed = new Set(['frontend', 'backend', 'devops']);

const rawTags = (params.get('tags') || '').split(',');

const filtered = rawTags.filter(v => allowed.has(v));

select.set(filtered);

Согласование состояния URL и динамически загружаемых опций

При асинхронной загрузке опций возможна ситуация, когда URL загружается раньше данных.

Решение — отложенная установка значения:

let pendingValue = params.get('category');

fetch('/api/categories')
  .then(res => res.json())
  .then(options => {
    select.setData(options);

    if (pendingValue) {
      select.set(pendingValue);
      pendingValue = null;
    }
  });

Оптимизация частоты обновления URL

При интенсивных изменениях состояния (например, быстрый выбор нескольких значений) применяется троттлинг:

function throttle(fn, delay) {
  let last = 0;

  return (...args) => {
    const now = Date.now();
    if (now - last < delay) return;

    last = now;
    fn(...args);
  };
}

const updateUrlThrottled = throttle(updateUrl, 200);

Это снижает количество операций history.replaceState и уменьшает нагрузку на браузер.


Поведение при пустых значениях и дефолтных фильтрах

Параметры, совпадающие с состоянием по умолчанию, часто исключаются из URL для упрощения адреса:

if (value === defaultValue) {
  params.delete('category');
}

Для массивов:

if (values.length === 0) {
  params.delete('tags');
}

Такой подход делает URL компактным и снижает избыточность состояния.