Валидация на стороне клиента

Клиентская валидация в интерфейсах с множественным выбором играет ключевую роль при работе с динамическими списками, тегами и асинхронными источниками данных. При использовании Tom Select контроль корректности выбранных значений перестаёт быть задачей только HTML-формы и переходит в область JavaScript-логики, где можно гибко управлять поведением компонента, ограничениями и пользовательскими ошибками.

Базовая модель валидации выбора

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

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

Ключевая идея заключается в том, что состояние селекта является источником истины, а валидация лишь реагирует на изменения этого состояния.


Обязательное поле (required selection)

Самый простой сценарий — необходимость хотя бы одного выбранного значения. HTML-атрибут required в сочетании с кастомным компонентом часто недостаточен, поэтому логика дополняется JavaScript.

const select = new TomSelect("#tags", {
  onChange: function(value) {
    validateSelect(this);
  }
});

function validateSelect(instance) {
  const isValid = instance.items.length > 0;

  const input = instance.input;
  input.setAttribute("aria-invalid", !isValid);

  const wrapper = instance.wrapper;
  wrapper.classList.toggle("is-invalid", !isValid);
}

Здесь важен момент: валидируется не DOM <select>, а внутреннее состояние items.


Ограничение количества элементов (min / max)

Одно из самых частых требований — ограничение диапазона выбора. Tom Select позволяет реализовать это через конфигурацию и события.

Максимальное количество

const select = new TomSelect("#tags", {
  maxItems: 5,
  onItemAdd: function() {
    enforceLimits(this);
  },
  onItemRemove: function() {
    enforceLimits(this);
  }
});

function enforceLimits(instance) {
  const max = 5;

  if (instance.items.length >= max) {
    instance.control_input.disabled = true;
  } else {
    instance.control_input.disabled = false;
  }
}

Поведение maxItems частично встроено, но дополнительная логика нужна для UI-индикации и блокировки пользовательского ввода.

Минимальное количество

function validateMin(instance) {
  const min = 2;
  const valid = instance.items.length >= min;

  instance.wrapper.classList.toggle("is-invalid", !valid);
}

Минимум чаще проверяется при отправке формы, а не в реальном времени.


Блокировка отправки формы

Интеграция с формой — центральный элемент клиентской валидации. Tom Select не заменяет HTMLForm validation API, но расширяет его.

const form = document.querySelector("form");
const select = new TomSelect("#tags");

form.addEventListener("submit", function(e) {
  const isValid = select.items.length >= 1 && select.items.length <= 5;

  if (!isValid) {
    e.preventDefault();
    select.wrapper.classList.add("is-invalid");
  }
});

Такой подход гарантирует, что даже при обходе UI-ограничений данные не будут отправлены некорректными.


Валидация создаваемых значений (create)

Одной из особенностей Tom Select является возможность создания новых элементов через create: true. Это требует дополнительной проверки входных данных.

const select = new TomSelect("#tags", {
  create: function(input) {
    return validateTag(input) ? {
      value: input,
      text: input
    } : false;
  }
});

function validateTag(value) {
  const regex = /^[a-zA-Z0-9_-]{3,20}$/;
  return regex.test(value);
}

Здесь валидация выполняется до добавления элемента в список, что предотвращает попадание некорректных значений в состояние компонента.


Предотвращение дубликатов

Хотя Tom Select по умолчанию не позволяет дублировать выбранные элементы, при кастомной логике или динамических данных дубликаты могут появляться.

const select = new TomSelect("#tags", {
  onItemAdd: function(value) {
    const duplicates = this.items.filter(v => v === value);

    if (duplicates.length > 1) {
      this.removeItem(value);
    }
  }
});

Более строгий подход — блокировка ещё на этапе добавления:

create: function(input) {
  if (this.items.includes(input)) {
    return false;
  }
  return { value: input, text: input };
}

Состояние ошибки и визуальная индикация

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

.is-invalid .ts-control {
  border-color: #e74c3c;
}

.is-invalid .ts-control:focus {
  box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.2);
}

И JavaScript-логика переключения состояния:

function setInvalid(instance, state) {
  instance.wrapper.classList.toggle("is-invalid", state);
}

Использование событий Tom Select для валидации

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

  • onChange — изменение общего состояния
  • onItemAdd — добавление элемента
  • onItemRemove — удаление элемента
  • onType — ввод текста пользователем

Пример комплексной логики:

const select = new TomSelect("#tags", {
  onChange: validate,
  onItemAdd: validate,
  onItemRemove: validate,
  onType: function() {
    clearError(this);
  }
});

function validate(instance) {
  const valid =
    instance.items.length >= 1 &&
    instance.items.length <= 5;

  setInvalid(instance, !valid);
}

function clearError(instance) {
  instance.wrapper.classList.remove("is-invalid");
}

Интеграция с Constraint Validation API

Хотя Tom Select не является нативным input-элементом, его можно синхронизировать с HTML5 validation API.

const select = new TomSelect("#tags");
const input = select.input;

function syncValidity(instance) {
  const valid = instance.items.length > 0;

  if (!valid) {
    input.setCustomValidity("Выберите хотя бы один элемент");
  } else {
    input.setCustomValidity("");
  }
}

Это позволяет использовать стандартные механизмы браузера: reportValidity(), checkValidity().


Ограничение доступных значений (whitelist validation)

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

const allowed = ["js", "css", "html"];

const select = new TomSelect("#tags", {
  create: false,
  onItemAdd: function(value) {
    if (!allowed.includes(value)) {
      this.removeItem(value);
    }
  }
});

Такой подход предотвращает внедрение недопустимых значений даже при манипуляции DOM.


Асинхронная проверка на клиенте

Иногда требуется клиентская проверка через локальные данные или кеш, без обращения к серверу.

const cache = new Set(["existing-tag"]);

const select = new TomSelect("#tags", {
  create: function(input) {
    if (cache.has(input)) {
      return false;
    }
    return { value: input, text: input };
  }
});

Хотя это похоже на серверную проверку, логика остаётся на клиенте и выполняется синхронно.


Блокировка интерфейса при ошибках

При критических нарушениях правил можно блокировать ввод полностью:

function lockSelect(instance, reason) {
  instance.disable();
  instance.wrapper.setAttribute("data-error", reason);
}

Разблокировка выполняется после исправления состояния:

function unlockSelect(instance) {
  instance.enable();
  instance.wrapper.removeAttribute("data-error");
}

Согласованность состояния UI и данных

Главная сложность клиентской валидации с Tom Select заключается в синхронизации трёх уровней:

  • внутреннее состояние (items)
  • DOM представление (wrapper, control)
  • состояние формы (FormData, submit event)

Корректная архитектура предполагает, что любое изменение проходит через единый слой валидации:

function applyChange(instance, validator) {
  const result = validator(instance.items);

  instance.wrapper.classList.toggle("is-invalid", !result);
  return result;
}

Валидация при динамической загрузке данных

Если элементы подгружаются асинхронно, важно проверять их до добавления:

const select = new TomSelect("#tags", {
  load: function(query, callback) {
    fetch("/api/tags?q=" + query)
      .then(r => r.json())
      .then(data => {
        callback(data.filter(validateRemoteItem));
      });
  }
});

function validateRemoteItem(item) {
  return item.active === true;
}

Контроль пользовательского ввода

Поле ввода внутри Tom Select может быть источником некорректных значений, особенно при включённом create.

const select = new TomSelect("#tags", {
  create: true,
  onType: function(str) {
    if (str.length > 20) {
      this.control_input.value = str.slice(0, 20);
    }
  }
});

Такой подход предотвращает переполнение допустимого формата ещё до создания элемента.