...

понедельник, 8 декабря 2014 г.

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

Думаю, многие знакомы с 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 у Вас всё в порядке.

Теперь по пунктам:



  1. нужно установить select2-rails, прописать его скрипты в application.js и стили в application.css (или что там у Вас вместо них?)

  2. установить AutoSelect2 и прописать его скрипты в application.js (или добавить хелперы в нужные партиалы)

  3. проверить, что в проекте нет контроллера с именем Select2AutocompletesController и роутов на подобие

    get 'select2_autocompletes/:class_name'


  4. подготовить папку 'app/select2_search_adapter' для своих SearchAdapter

  5. установить гем 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.

За сим все




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

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий