Кастомные атрибуты

Slim Select позволяет расширять стандартный <select> за счёт кастомных атрибутов, которые становятся основой для сложных интерфейсов: иконок, меток, статусов, метаданных, поведения поиска и динамического рендеринга. Основной механизм расширения строится вокруг data-* атрибутов HTML и пользовательских полей в объектах данных, которые Slim Select прокидывает в шаблоны отображения.

Базовый HTML остаётся совместимым со стандартной разметкой, при этом дополнительные данные добавляются через data-*:

<select id="country-select">
  <option value="kz" data-code="KZ" data-flag="??" data-region="asia">
    Kazakhstan
  </option>
  <option value="de" data-code="DE" data-flag="??" data-region="europe">
    Germany
  </option>
  <option value="jp" data-code="JP" data-flag="??" data-region="asia">
    Japan
  </option>
</select>

Каждый атрибут, начинающийся с data-, автоматически попадает в dataset DOM-элемента. Slim Select не игнорирует эти данные, а передаёт их в объект опции при построении списка.

Инициализация:

new SlimSelect({
  select: '#country-select'
});

На этом уровне Slim Select уже умеет отображать стандартные значения, но ключевая часть начинается при кастомизации рендера.

Доступ к кастомным атрибутам через dataset

Каждая опция в Slim Select получает доступ к исходному DOM-элементу. Через него извлекаются атрибуты:

new SlimSelect({
  select: '#country-select',
  render: {
    option: (data) => {
      const option = data.option;

      const flag = option.dataset.flag;
      const code = option.dataset.code;
      const region = option.dataset.region;

      return `
        <div class="option-item">
          <span class="flag">${flag}</span>
          <span class="label">${data.text}</span>
          <span class="meta">${code} · ${region}</span>
        </div>
      `;
    }
  }
});

Здесь data.option — это DOM <option>, а dataset содержит все кастомные атрибуты. Такой подход превращает обычный select в структурированный источник данных.

Передача дополнительных данных через JS-конфигурацию

Slim Select поддерживает не только DOM-инициализацию, но и программное создание списка через data. В этом случае кастомные атрибуты задаются как поля объекта:

new SlimSelect({
  select: '#country-select',
  data: [
    {
      text: 'Kazakhstan',
      value: 'kz',
      flag: '??',
      code: 'KZ',
      region: 'asia'
    },
    {
      text: 'Germany',
      value: 'de',
      flag: '??',
      code: 'DE',
      region: 'europe'
    }
  ],
  render: {
    option: (item) => {
      return `
        <div>
          <strong>${item.text}</strong>
          <small>${item.code} (${item.region})</small>
        </div>
      `;
    }
  }
});

В этом режиме item уже содержит все дополнительные поля напрямую, без необходимости обращаться к dataset.

Разделение логики отображения и данных

Кастомные атрибуты используются как слой данных, не влияющий на значение формы. Важно различать:

  • value — отправляется в форму
  • text — отображается по умолчанию
  • кастомные поля (data-* или дополнительные свойства объекта) — используются только для UI/логики

Пример:

{
  text: 'Tokyo',
  value: 'tokyo',
  timezone: 'Asia/Tokyo',
  population: 13960000,
  status: 'active'
}

Отображение может учитывать статус:

render: {
  option: (item) => {
    const statusClass = item.status === 'active' ? 'is-active' : 'is-disabled';

    return `
      <div class="${statusClass}">
        ${item.text}
        <span>${item.timezone}</span>
      </div>
    `;
  }
}

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

Slim Select использует встроенный поиск по text, однако кастомные поля позволяют расширить поведение через фильтрацию данных до инициализации или через трансформацию массива.

Пример предобработки:

const rawData = [
  { text: 'Almaty', value: 'almaty', tags: ['city', 'kazakhstan'] },
  { text: 'Berlin', value: 'berlin', tags: ['city', 'germany'] }
];

new SlimSelect({
  select: '#city-select',
  data: rawData,
  searchFilter: (opt, search) => {
    const inText = opt.text.toLowerCase().includes(search.toLowerCase());
    const inTags = opt.tags?.some(t => t.includes(search.toLowerCase()));

    return inText || inTags;
  }
});

Здесь кастомный атрибут tags расширяет стандартную поисковую модель.

Работа с data-атрибутами в DOM-режиме

При использовании чистого <select> Slim Select не требует изменения структуры, но кастомные атрибуты становятся важным источником метаданных:

<option value="dev" data-role="developer" data-level="senior">
  Developer
</option>

В render-функции:

render: {
  option: (data) => {
    const el = data.option;

    return `
      <div>
        <span>${data.text}</span>
        <span>${el.dataset.role}</span>
        <span>${el.dataset.level}</span>
      </div>
    `;
  }
}

Этот подход особенно полезен при интеграции с серверным рендерингом, где данные уже вложены в HTML.

Группировка и кастомные атрибуты

При использовании <optgroup> кастомные атрибуты могут применяться как к группам, так и к опциям внутри них:

<optgroup label="Europe" data-continent="EU">
  <option value="de" data-population="83000000">Germany</option>
  <option value="fr" data-population="67000000">France</option>
</optgroup>

Рендер группы:

render: {
  optgroup: (group) => {
    return `
      <div class="group-header">
        ${group.label}
        <small>${group.element.dataset.continent}</small>
      </div>
    `;
  }
}

Таким образом метаданные уровня группы становятся частью UI.

Динамическое обновление кастомных атрибутов

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

const slim = new SlimSelect({
  select: '#country-select'
});

slim.setData([
  {
    text: 'Kazakhstan',
    value: 'kz',
    flag: '??',
    region: 'asia'
  }
]);

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

Комбинирование кастомных атрибутов и состояния UI

Кастомные атрибуты часто используются для управления состоянием:

  • disabled
  • locked
  • featured
  • hidden

Пример:

{
  text: 'Premium Plan',
  value: 'premium',
  featured: true,
  locked: false
}

Рендер:

render: {
  option: (item) => {
    const featured = item.featured ? 'featured' : '';

    return `
      <div class="plan ${featured}">
        ${item.text}
      </div>
    `;
  }
}

Использование кастомных атрибутов для интеграции с API

При загрузке данных с сервера кастомные атрибуты часто приходят как расширенные поля JSON:

{
  "text": "USD",
  "value": "usd",
  "symbol": "$",
  "precision": 2,
  "market": "forex"
}

В Slim Select такие поля напрямую становятся частью объекта item, что позволяет строить сложные интерфейсы без промежуточных преобразований.

render: {
  option: (item) => {
    return `
      <div>
        ${item.symbol} ${item.text}
        <span>${item.market}</span>
      </div>
    `;
  }
}

Ограничения и особенности использования кастомных атрибутов

Кастомные атрибуты не участвуют в стандартной сериализации формы. Отправляется только value. Поэтому любые дополнительные данные должны обрабатываться отдельно при отправке:

form.addEventListener('submit', (e) => {
  const value = slim.getSelected();

  const enriched = value.map(v => ({
    value: v,
    meta: slim.getData().find(i => i.value === v)
  }));

  console.log(enriched);
});

Это позволяет восстановить контекст, связанный с кастомными полями.

Паттерны проектирования на основе кастомных атрибутов

Часто используется паттерн «обогащённой опции», где каждая запись содержит:

  • идентификатор
  • отображаемый текст
  • визуальные маркеры
  • бизнес-метаданные
{
  text: 'Enterprise',
  value: 'enterprise',
  badge: 'VIP',
  color: 'gold',
  limits: {
    users: 1000,
    storage: '1TB'
  }
}

Такая структура позволяет Slim Select выступать не просто UI-компонентом, а слоем представления над полноценной моделью данных.