Думаю, многие знакомы с Select2. Всё в нём замечательно: и элементы красивые, и кастомизации вагон, и c ajax работает и ещё много много полезного делать умеет. Только проблема одна: инициализация довольно громоздкая (js писать надо, экшн иметь для ajax-овой подгрузки результатов и так далее). Это было не шибко удобно и решили мы сделать свою надстройку для Select2, в которой и js писать не надо, да и за пределы вьюхи уходить почти не придётся. О том как мы это делали и что получилось читайте под катом.
К чему стремились?
Все знают поведение хелперов из ActionView::Helpers::FormTagHelper. Например select_tag:
select_tag "people", options_from_collection_for_select(@people, "id", "name")
Хочется чтобы и с Select2 работать было так же просто. Причём с любой вариацией Select2: будь то статическая замена обычного select, ajax вариант или мультиселект.
Что получилось? Получилось примерно следующее:
= select2_ajax_tag :my_select2_name,
{class_name: :my_model_name, text_column: :name, id_column: :id},
my_init_value_id,
placeholder: 'Fill me now!'
Это самый простой вызов хелпера. Не надо никаких дополнительных телодвижений. Как результат получим селект с динамической загрузкой вариантов из модели MyModelName.
Конечно же, это самый простой способ использования. Если появится желание делать сложные выборки, выдавать разные результаты в зависимости от других параметров на странице, кастомизировать отображение вариантов в селекте и тд, то не беда, всё это можно сделать, но прийдётся добавить немного Ruby.
Примеры в студию!
Для тех, кто уже хочет пощупать руками, а так же тем, кто воспринимает исходники лучше, нежели неспешное словесное описание, даю ссылку на RoR проект с примером использования.
Альтернативы
В своё время мы их не нашли, поэтому и случилось неспешное изобретение сего гема. Однако, учитывая специфику ресурса и мощь комментариев, я уверен, что кто-нибудь подскажет аналог который мы не заметили. Так что буду рад, если укажете на альтернативу.
Что это вообще такое?
Это два гема AutoSelect2 и AutoSelect2Tag идею которых предложил Tab10id. Оба гема основываются на select2-rails и позволяют создавать Select2 элементы без тяжёлых дум о js-инициализации и о прочих накладных расходах.
Как этим пользоваться?
Подробные Readme есть у каждого из гемов, но они, как водится, на английском. Поэтому расскажу по-русски о том как правильно поставить гемы, что они требуют для работы и как в итоге пользоваться их функционалом.
Установка
Перво-на-перво нужно сказать, что если у Вас отключён asset pipeline установка становится довольно нетривиальной и выходит за рамки этой статьи, так что будем считать что с pipeline у Вас всё в порядке.
Теперь по пунктам:
- нужно установить select2-rails, прописать его скрипты в application.js и стили в application.css (или что там у Вас вместо них?)
- установить AutoSelect2 и прописать его скрипты в application.js (или добавить хелперы в нужные партиалы)
- проверить, что в проекте нет контроллера с именем Select2AutocompletesController и роутов на подобие
get 'select2_autocompletes/:class_name'
- подготовить папку 'app/select2_search_adapter' для своих SearchAdapter
- установить гем AutoSelect2Tag
Использование статического Select2
После сих дейсвий можно пользоваться хелпером для статического селекта:
= select2_tag :select2_name,
my_options_for_select2(my_init_value),
placeholder: 'Fill me!',
include_blank: true,
select2_options: {width: 'auto'}
По сути, метод является обёрткой обычного select_tag. Он добавляет нужные классы для инициализации Select2 и пробрасывает параметры конструктора.
Использование ajax Select2
Прямо «из коробки» доступен самый простой вызов хелпера:
= select2_ajax_tag :my_select2_name,
{class_name: :my_model_name, text_column: :name, id_column: :id},
my_init_value_id,
placeholder: 'Fill me now!'
Здесь второй параметр хелпера должен говорить сам за себя.
Коли же хочется более сложных конструкций в селекте, то прийдётся писать свой SearchAdapter. Что это такое? Это класс, который постранично выдаёт хэши с опциями для селекта и отвечает за инициализирующее значение, если оно присутствует. Класс этот используется в контроллере Select2AutocompletesController, а его название указывается во втором параметра select2_ajax_tag (см. пример ниже). Вот набор требований для SearchAdapter:
- файлы с классами должны располагаться в 'app/select2_search_adapter' (по аналогии с inputs у formtastic); справедливости ради стоит сказать, что располагаться они могут и в любой другой autoload директории, но вышеописанный способ кажется самым оптимальным
- имена классов должны оканчиваться на SearchAdapter
- SearchAdapter должен наследоваться от AutoSelect2::Select2SearchAdapter::Base
- класс должен реализовывать метод search_default (подробности см. ниже)
Чтобы долго не описывать требования к search_default приведу пример минималистичного SearchAdapter:
class SystemRoleSearchAdapter < AutoSelect2::Select2SearchAdapter::Base
class << self
def search_default(term, page, options)
if options[:init].nil?
roles = default_finder(SystemRole, term, page: page)
count = default_count(SystemRole, term)
{
items: roles.map do |role|
{ text: role.name, id: role.id.to_s } # здесь ещё можно добавить 'class_name'
end,
total: count
}
else
get_init_values(SystemRole, options[:item_ids])
end
end
end
end
Как видно из примера, если в options отсутствует ключ :init, то search_default должен возвращать хэш вида:
{ items:
[
{ text: 'first element', id: 'first_id' },
{ text: 'second element', id: 'second_id' }
],
total: count }
Если же :init присутствует, то функция должна вернуть:
{text: 'displayed text', id: 'id_of_initial_element'}
После определения такого класса можно использовать ajax select2 следующим образом:
= select2_ajax_tag :my_select2_name,
:system_role,
init_value,
additional_options
И всё. Синтаксис максимально приближен к select_tag и использовать можно в любом месте приложения.
Использование multi ajax Select2
Здесь всё аналогично предыдему пункту с ajax select2 с той лишь разницей, что надо подключить скрипт multi_ajax_select2_value_parser.js и добавить в хэш select2_options: {multiple: true}:
= select2_ajax_tag :multi_countries_select2,
:country,
'',
class: 'is-multiple',
select2_options: {multiple: true}
Где-то здесь у разработчиков, знакомых с Select2, должен появиться вопрос: а скрипт зачем? Отвечаю: скрипт реализует сериализацию вариантов селекта в виде массива, а не в виде строки с вариантами через запятую. Автор Select2 обещал сделать то же самое в следующей мажорной версии, но ждать-то не охото.
Дополнительные возможности
Большинство из них описано в примере. Для тех, кому лень запускать проект, да и просто для полноты картины, сделаю их краткий обзор.
Расширение search_default
Допустим Вы создали свой SearchAdapter и реализовали в нём search_default. Пусть этот адаптер будет для модели User. Всё хорошо, но однажды потребовалось создать аналогичный селект, но чтобы в вариантах были только активные пользователи. Дабы не создавать новый класс для той же самой сущности можно добавить метод search_active в ранее созданный UserSearchAdapter и указать этот метод при инициализации Select2:
= select2_ajax_tag :active_user,
:user,
'',
search_method: :active # здесь указываем какой search_ метод хотим использовать
Зависимые выборки
Ещё один случай: реализовать 2 (или более) селектов, варианты в которых зависят друг от друга. Например каскадный выбор страны и города (пример из auto_select2_tag_example). Если выбрали страну, то выбрать город можно только внутри этой страны и наоборот. Как это неудобно делается со статическими селектами и так понятно. А вот как это делается с помощью select2_ajax_tag: во-первых, нужно присвоить всем зависимым элементам какой-либо класс, например dependent-input; во-вторых, указать это класс в additional_ajax_data:
= select2_ajax_tag :country_id,
:country,
'',
placeholder: 'Select country',
class: 'dependent-input',
select2_options: {additional_ajax_data: {selector: '.dependent-input'}}
= select2_ajax_tag :city_id,
:city,
'',
placeholder: 'Select city',
class: 'dependent-input',
select2_options: {additional_ajax_data: {selector: '.dependent-input'}}
После этого, при отправке ajax-запроса на получение вариантов, селект будет искать все элементы с классом dependent-input, исключать из них самого себя, преобразовывать оставшиеся в json, где ключ — это name-атрибут элемента, а значение — value-атрибут, и отправлять полученный json вместе с запросом. Контроллер же пробросит все эти параметры в SearchAdapter и в методе search_default (или любом другом search_) в параметре options будут присутствовать значения с формы. Далее их можно использовать любым удобным способом и отдавать только те варианты, которые соответствуют требованиям.
Метод to_select2
Можно не указывать опции text_column и id_column, если у модели присутствует метод to_select2. В этом случае он будет автоматически вызываться для получения опций при генерации json. Если же нужно использовать метод отличный от to_select2, то можно передать параметр hash_method:
= select2_ajax_tag :default_country_id,
{class_name: :country, hash_method: :to_select2_alternate},
Country.first.id
Выгода очевидна. Без создания SearchAdapter можно передавать более сложные опции в селект.
Несколько слов про инициализацию
Элементы автоматически инициализируются после загрузки страницы (то есть в $(document).ready()), после ajax-запросов (по эвенту ajaxSuccess) и по событию cocoon:after-insert из cocoon. Нет нужды переживать за повторную инициализацию, дважды ничего не вызывается и проблем нет. Если же по каким-либо причинам всё-таки потребовалась ручная инициализация, то нужно вызвать initAutoAjaxSelect2() и/или initAutoStaticSelect2().
Планы на будущее
Так как гемы вырасли из Redmine проекта, то хочется сделать pluging к нему. Конечно же тут сразу встаёт вопрос о pipeline, который в Redmine не работает в принципе и для запуска которого нужно основательно постараться. Далее хочется подружиться с formtastic.
За сим все
Спасибо что дочитали до конца. Про опечатки и неточности пишите в лс, буду рад исправить. Прочие вопросы/замечания/предложения/возмущения пишите в комментах, будет интересно.
Автор: Loriowar