Как мы Select2 в хелпер заворачивали

4ec9c98757c64eb0a60102b2ddeb920c.pngДумаю, многие знакомы с 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.За сим все Спасибо что дочитали до конца. Про опечатки и неточности пишите в лс, буду рад исправить. Прочие вопросы/замечания/предложения/возмущения пишите в комментах, будет интересно.

© Habrahabr.ru