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

в 7:22, , рубрики: gem, ruby, ruby on rails, select2, Веб-разработка

Как мы Select2 в хелпер заворачивали - 1Думаю, многие знакомы с 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.

За сим все

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

Автор: Loriowar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js