Библиотека Slim Select
отвечает за визуальное оформление и управление
<select>-элементами, однако полноценная
бизнес-валидация почти всегда реализуется отдельно. В сложных
интерфейсах стандартного атрибута required недостаточно.
Возникают сценарии, где необходимо:
Кастомные правила позволяют превратить Slim Select в полноценный компонент сложной формы с гибкой логикой поведения.
Наиболее распространённая схема включает:
Пример базовой структуры:
<form id="user-form">
<select id="skills" multiple>
<option value="js">JavaScript</option>
<option value="ts">TypeScript</option>
<option value="php">PHP</option>
<option value="python">Python</option>
</select>
<div id="skills-error"></div>
<button type="submit">Отправить</button>
</form>
const slim = new SlimSelect({
select: '#skills'
})
const form = document.querySelector('#user-form')
const errorBlock = document.querySelector('#skills-error')
form.addEventListener('submit', (event) => {
const values = slim.getSelected()
if (values.length === 0) {
event.preventDefault()
errorBlock.textContent = 'Необходимо выбрать минимум один навык'
return
}
errorBlock.textContent = ''
})
При большом количестве правил валидацию необходимо выносить в отдельные функции.
function validateSkills(values) {
const errors = []
if (values.length === 0) {
errors.push('Список навыков не может быть пустым')
}
if (values.length > 3) {
errors.push('Максимум 3 навыка')
}
return errors
}
Использование:
form.addEventListener('submit', (event) => {
const values = slim.getSelected()
const errors = validateSkills(values)
if (errors.length > 0) {
event.preventDefault()
errorBlock.innerHTML = errors.join('<br>')
return
}
errorBlock.textContent = ''
})
function validateMin(values, min) {
return values.length >= min
}
Применение:
if (!validateMin(values, 2)) {
errors.push('Нужно выбрать минимум 2 элемента')
}
function validateMax(values, max) {
return values.length <= max
}
Пример:
if (!validateMax(values, 5)) {
errors.push('Допустимо максимум 5 элементов')
}
Иногда определённые значения несовместимы между собой.
Пример:
function validateForbiddenPairs(values) {
const forbidden = [
['js', 'php'],
['python', 'ts']
]
for (const pair of forbidden) {
const hasAll = pair.every(item => values.includes(item))
if (hasAll) {
return false
}
}
return true
}
Использование:
if (!validateForbiddenPairs(values)) {
errors.push('Обнаружена запрещённая комбинация')
}
Часто выбор одного значения влияет на другое поле формы.
<select id="country">
<option value="kz">Казахстан</option>
<option value="ru">Россия</option>
</select>
<select id="city">
<option value="karaganda">Караганда</option>
<option value="almaty">Алматы</option>
<option value="moscow">Москва</option>
</select>
const countrySelect = new SlimSelect({
select: '#country'
})
const citySelect = new SlimSelect({
select: '#city'
})
Правило проверки:
function validateCity(country, city) {
const allowed = {
kz: ['karaganda', 'almaty'],
ru: ['moscow']
}
return allowed[country]?.includes(city)
}
Использование:
const country = countrySelect.getSelected()[0]
const city = citySelect.getSelected()[0]
if (!validateCity(country, city)) {
errors.push('Выбранный город не соответствует стране')
}
Slim Select может работать с динамически добавляемыми значениями. Это особенно актуально при использовании тегов.
function validateTag(tag) {
const regex = /^[a-z0-9-_]{3,20}$/i
return regex.test(tag)
}
Пример:
const tags = slim.getSelected()
for (const tag of tags) {
if (!validateTag(tag)) {
errors.push(`Некорректный тег: ${tag}`)
}
}
В сложных интерфейсах данные могут дублироваться после динамической загрузки.
function validateUnique(values) {
return new Set(values).size === values.length
}
Пример:
if (!validateUnique(values)) {
errors.push('Обнаружены дублирующиеся значения')
}
Иногда требуется проверить данные через сервер.
Например:
async function validateOnServer(values) {
const response = await fetch('/api/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ values })
})
return await response.json()
}
Использование:
form.addEventListener('submit', async (event) => {
event.preventDefault()
const values = slim.getSelected()
const result = await validateOnServer(values)
if (!result.valid) {
errorBlock.textContent = result.message
return
}
form.submit()
})
Проверка только при отправке формы создаёт плохой UX. Намного эффективнее валидировать поле сразу после изменения.
afterChangeconst slim = new SlimSelect({
select: '#skills',
events: {
afterChange() {
validate()
}
}
})
Функция:
function validate() {
const values = slim.getSelected()
if (values.length === 0) {
errorBlock.textContent = 'Выберите минимум один элемент'
return
}
errorBlock.textContent = ''
}
При большом проекте полезно формировать набор независимых валидаторов.
const validators = [
validateRequired,
validateUnique,
validateForbiddenPairs
]
Обработка:
function runValidators(values) {
const errors = []
for (const validator of validators) {
const result = validator(values)
if (result !== true) {
errors.push(result)
}
}
return errors
}
function validateRequired(values) {
if (values.length === 0) {
return 'Поле обязательно'
}
return true
}
function validateUnique(values) {
if (new Set(values).size !== values.length) {
return 'Обнаружены дубликаты'
}
return true
}
class SlimValidator {
constructor(slim, errorElement) {
this.slim = slim
this.errorElement = errorElement
this.rules = []
}
addRule(rule) {
this.rules.push(rule)
}
validate() {
const values = this.slim.getSelected()
for (const rule of this.rules) {
const result = rule(values)
if (result !== true) {
this.showError(result)
return false
}
}
this.clearError()
return true
}
showError(message) {
this.errorElement.textContent = message
}
clearError() {
this.errorElement.textContent = ''
}
}
const validator = new SlimValidator(
slim,
document.querySelector('#skills-error')
)
validator.addRule((values) => {
return values.length > 0 || 'Нужно выбрать минимум один элемент'
})
validator.addRule((values) => {
return values.length <= 3 || 'Допустимо максимум 3 элемента'
})
Проверка:
form.addEventListener('submit', (event) => {
if (!validator.validate()) {
event.preventDefault()
}
})
function setInvalid(element) {
element.classList.add('is-invalid')
}
function clearInvalid(element) {
element.classList.remove('is-invalid')
}
Пример:
if (errors.length > 0) {
setInvalid(document.querySelector('.ss-main'))
}
.is-invalid {
border: 1px solid #d93025;
}
#error-message {
margin-top: 5px;
color: #d93025;
font-size: 14px;
}
Slim Select часто работает вместе с AJAX.
async function loadOptions() {
const response = await fetch('/api/options')
const data = await response.json()
const validOptions = data.filter(item => {
return item.active === true
})
slim.setData(validOptions)
}
При использовании режима добавления новых элементов особенно важно фильтровать вводимые данные.
function validateTagLength(tag) {
return tag.length <= 15
}
function validateTagSymbols(tag) {
return !/[<>]/g.test(tag)
}
function validateTag(tag) {
if (!validateTagLength(tag)) {
return 'Слишком длинный тег'
}
if (!validateTagSymbols(tag)) {
return 'Недопустимые символы'
}
return true
}
При частой проверке через сервер необходимо ограничивать количество запросов.
function debounce(callback, delay) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
callback(...args)
}, delay)
}
}
Использование:
const validateDebounced = debounce(async () => {
const values = slim.getSelected()
await validateOnServer(values)
}, 500)
Иногда вместо вывода ошибки лучше предотвращать выбор значения.
function removeForbidden(values) {
return values.filter(value => value !== 'admin')
}
Пример:
slim.settings.events.afterChange = () => {
const values = slim.getSelected()
slim.setSelected(removeForbidden(values))
}
async function submitForm() {
const values = slim.getSelected()
const errors = runValidators(values)
if (errors.length > 0) {
errorBlock.innerHTML = errors.join('<br>')
return
}
const response = await fetch('/api/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
skills: values
})
})
const result = await response.json()
console.log(result)
}
В крупных проектах правила делятся на категории:
| Тип правила | Назначение |
|---|---|
| Required | Проверка обязательности |
| Range | Проверка диапазонов |
| Dependency | Проверка зависимостей |
| Async | Серверная проверка |
| Security | Фильтрация опасных данных |
| Business | Бизнес-ограничения |
| UI | Проверка пользовательского интерфейса |
Нельзя ограничиваться исключительно клиентской проверкой. Все правила должны дублироваться на сервере.
Частая ошибка — добавление сообщения без удаления предыдущего состояния.
Неправильно:
errorBlock.textContent = 'Ошибка'
Правильно:
errorBlock.textContent = ''
перед новой проверкой.
Плохой подход:
if (values.length === 0) {
errorBlock.textContent = 'Ошибка'
document.body.style.background = 'red'
}
Корректный подход — разделение:
Проверка после каждого изменения без debounce может перегружать API.
Проблемный вариант:
afterChange: async () => {
await fetch('/validate')
}
Оптимальный вариант — debounce или batch-валидация.
function validate(values) {
const errors = []
if (values.length === 0) {
errors.push('Выберите минимум один элемент')
}
if (values.length > 5) {
errors.push('Максимум 5 элементов')
}
if (!validateForbiddenPairs(values)) {
errors.push('Запрещённая комбинация')
}
if (!validateUnique(values)) {
errors.push('Дублирующиеся значения')
}
return errors
}
Использование:
const errors = validate(slim.getSelected())
if (errors.length > 0) {
errorBlock.innerHTML = errors.join('<br>')
}