Имитация select с помощью Alpine.js

Все верстальщики знают, что тэг SELECT один из самых капризных и плохо поддающихся кастомизации. Разработчики браузеров до сих пор не могут договориться какими css-стилями их можно настраивать, поэтому нам как правило приходится довольствоваться типовым поведением этого элемента с небольшим изменением дизайна.

Нужно понимать, что SELECT — это сложный элемент. Это не только внешний вид, но и достаточно сложное поведение, которое работает на события click, keydown, space, стрелки курсора, а также получение фокуса через Tab. Из-за этого все попытки переписать SELECT на какой-то свой html/js/css-вариант обычно ограничиваются только дизайнерской частью — повторить стандартное поведение достаточно сложно.

Ниже я покажу, как это можно сделать с помощью Alpine.js. Пример больше учебный, показывающий общий подход к кастомизации и созданию собственных интерактивных элементов.

Для начала рассмотрим как работает SELECT, тогда будет понятно что нужно получить в итоге.

<select>
	<option>First</option>
	<option>Second</option>
	<option>Third</option>
<select>
 
<select class="form-input">
	<option>First</option>
	<option>Second</option>
	<option>Third</option>
<select>

Первый элемент — нативный, второй с классом form-input, который немного меняет оформление основного блока.

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

Можно сказать, что по своему поведению SELECT очень близок к выпадающему dropdown, который я уже много раз показывал. Вместо кнопки должен быть обычный DIV, который отображает выбранный элемент. Рядом с ним иконка (или юникод-символ ▼), которая и показывает, что это выпадающий элемент. Клик на этом блоке показывает содержимое - список выбора.

Список выбора — это обычные блочные элементы. Поскольку мы не будем делать выбор элементов курсором, а только на click, то задача упрощается — достаточно отследить это событие на каждом элементе и записать его значение как value всего компонента.

Ещё нюанс. Поскольку SELECT как правило — часть формы, то нам нужно каким-то образом в неё «вклиниться», чтобы выбранное значение оказалось частью полей формы.

Самый простой способ это сделать — использовать скрытое поле INPUT, которое может сразу же и хранить выбранное значение, благодаря реактивности Alpine.

Вот так это будет выглядеть в итоге:


Вначале рассмотрим код «каркаса».

<div x-data="{
		val: 'Second', 
		items: ['First', 'Second', 'Third'],
		show: false
		}" 
		class="w200px pos-relative b-inline">
		
	<input x-model="val" type="hidden" name="mySelect">
	
	<div 
		@click="show = true" 
		@keydown.enter="show = !show" 
		tabindex="3" 
		class="hover-t-blue700 pos-relative bordered rounded5 transition-var">
		
	    <div x-text="val" class="pad10-rl pad3-tb"></div>
	    <div :class="{'t-blue bg-blue100': show}" class="rounded5-r pos-absolute pos0-t pos0-r pad5-rl h100 lh200 cursor-pointer">▼</div>
    </div>
    
    <div x-show="show" x-cloak>
    	... элементы списка...
    </div>
</div>

Элементы списка можно задать в виде массива в items. Переменная valхранит текущий выбор. Через переменную showмы управляем видимостью элементов списка.

Сразу связываем INPUT с переменной valчерез x-modelи сразу её скрываем для отображения. Когда произойдёт отправка формы (submit) это поле автоматом попадёт в форму.

Блок выбора состоит из двух элементов. Они располагаются рядом и имитируют стандартное поведение с помощью css-классов.

Первый элемент отображает текущее состояние valс помощью директивы x-text. Во втором элементе просто добавляется для красоты подсветка треугольника (это может быть любой символ) когда список элементов будет открыт.

Список открывается на обычный click. Также мы ловим события клавиатуры Enter, но чтобы они работали нужно заставить блок принимать фокус ввода. Делается это с помощью стандартного html-атрибута tabindex, где нужно будет подобрать значение под конкретную форму/страницу. Кроме того этот трюк позволит переместиться к элементу с помощью клавиши табуляции.

Теперь посмотрим как устроен блок выбора элементов.

<div x-show="show" x-cloak>
    	<div 
    		@click.away="show = false" 
    		:class="{'bor-blue': show}" 
    		class="mar5-t bordered pos-absolute w200px z-index1 bg-white cursor-pointer">
    	
    		<template x-for="item in items">
    			<div 
    				@click="val = item; show = false" 
    				:class="{'t-white bg-blue600': val == item}" 
    				x-text="item" 
    				class="hover-bg-blue700 hover-t-white pad10-rl lh200">
    			</div>
    		</template>
    		
    	</div>
    </div>

Событие click.awayзакрывает блок, когда будет клик вне его области. Когда блок открыт, то подсвечиваем его синим бордюром. Расположение блока определяется через css-классы.

Каждый элемент выбора формируется через x-for, где мы просто перебираем элементы items. По клику на элементе присваиваем его значение val, которое автоматом будет передано в скрытый INPUT и отобразится в верхнем блоке. Ну и сразу скрываем выпадающий список. Вот собственно и вся логика.

Фокус

Фокус в данном примере создаётся автоматически за счёт tabindex. Если такое поведение не нужно (большинство работает мышкой, а на телефоне это вообще не используется), то можно убрать tabindex, а фокусную рамку имитировать с помощью css-классов с привязкой к переменной show.

Клавиатура

Нетрудно заметить, что в примере не работает выбор элементов клавишами курсора. Теоретически нет сложностей повесить на элементы обработчики keydown, но логика компонента усложнится. Более того, возникнет проблема смены фокуса для каждого элемента. Лично мне кажется, что решать такую задачу можно только ради «спортивного» интереса — на практике, если нужно добиться полного соответствия поведению SELECT, то лучше уж и взять его как есть. Это хотя бы гарантирует его корректную работу. :-)

Итого

  • Компонент достаточно объёмный. Его HTML-код можно сократить, если использовать x-speadи вынести логику в отдельную js-функцию.
  • В этом компоненте можно поменять любое оформление, чтобы удовлетворить любые дизайнерские причуды.
  • Если вы изучите код и поймёте его алгоритм, то сможете создавать любые подобные варианты.
  • Минус такого компонента в том, что он не нативный для ОС и не подстроится под телефон, как стандартный SELECT.
  • Есть проблемы с фокусом и клавиатурой.
Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)