How we wrapped Select2 in a helper

    I think many are familiar with Select2 . Everything in it is wonderful: both the elements are beautiful, and the car customization, and c ajax works and knows how to do many more useful things. Only one problem: initialization is rather cumbersome (you need to write js, have action for ajax loading of results, and so on). It was not very convenient, and we decided to create our own add-in for Select2 , in which you do not need to write js either, and you almost never have to leave the view. About how we did it and what happened read under the cut.



    What did they strive for?


    Everyone knows the behavior of helpers from ActionView :: Helpers :: FormTagHelper . For example select_tag :
    select_tag "people", options_from_collection_for_select(@people, "id", "name")
    

    I would like to work with Select2 as easy. And with any variation of Select2 : whether it is a static replacement for the usual select , ajax option or multiselect.
    What happened? It turned out approximately the following:
    = 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!'
    

    This is the easiest helper call. No additional gestures are needed. As a result, we get a selection with dynamic loading of options from the MyModelName model .

    Of course, this is the easiest way to use. If there is a desire to make complex selections, to give different results depending on other parameters on the page, to customize the display of options in the selection, etc., then it does not matter, all this can be done, but you will have to add a little Ruby .

    Examples in the studio!


    For those who already want to feel with their hands, as well as those who perceive the source code better than a leisurely verbal description, I give a link to the RoR project with an example of use.

    Alternatives


    At one time, we did not find them, so the unhurried invention of this heme happened. However, given the specifics of the resource and the power of the comments, I am sure that someone will tell you an analog that we did not notice. So I will be glad if you point out the alternative.

    What is this all about?


    These are two gems AutoSelect2 and AutoSelect2Tag whose idea was proposed by Tab10id . Both gems are based on select2-rails and allow you to create Select2 elements without heavy thoughts about js initialization and other overheads.

    How to use it?


    Each gem has detailed Readme , but they are, as usual, in English. Therefore, I will tell you in Russian about how to properly install gems, what they require for work, and how to use their functionality in the end.

    Installation


    First and foremost, I must say that if you have disabled the asset pipeline, the installation becomes quite non-trivial and goes beyond the scope of this article, so we will assume that everything is in order with the pipeline.

    Now point by point:
    1. you need to install select2-rails , register its scripts in application.js and styles in application.css (or what do you have instead of them?)
    2. install AutoSelect2 and register its scripts in application.js (or add helpers to the necessary partials)
    3. verify that the project does not have a controller named Select2AutocompletesController and similar routes
      get 'select2_autocompletes/:class_name'
      
    4. prepare the folder 'app / select2_search_adapter' for your SearchAdapter
    5. install AutoSelect2Tag gem


    Using Static Select2


    After these actions, you can use the helper for static selection:
    = select2_tag :select2_name,
                   my_options_for_select2(my_init_value),
                   placeholder: 'Fill me!',
                   include_blank: true,
                   select2_options: {width: 'auto'}
    

    In essence, the method is a wrapper for a regular select_tag . It adds the necessary classes to initialize Select2 and forwards the constructor parameters .

    Using ajax Select2


    Out of the box, the easiest helper call is available:
    = 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!'
    

    Here the second parameter of the helper should speak for itself.

    If you want more complex constructions in the select, you will have to write your SearchAdapter . What it is? This is a class that pushes hashes with options for the page and is responsible for the initialization value, if present. This class is used in the Select2AutocompletesController controller , and its name is specified in the second select2_ajax_tag parameter (see the example below). Here is the set of requirements for the SearchAdapter :
    • class files should be located in 'app / select2_search_adapter' (similar to formtastic 's inputs ); In fairness, it’s worth saying that they can be located in any other autoload directory, but the above method seems to be the most optimal
    • class names must end with a SearchAdapter
    • SearchAdapter must inherit from AutoSelect2 :: Select2SearchAdapter :: Base
    • the class must implement the search_default method (see details below)

    In order not to describe the requirements for search_default for a long time, I will give an example of a minimalistic 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

    As you can see from the example, if the options key is missing: init, then search_default should return a hash of the form:
    { items:
        [ 
          { text: 'first element', id: 'first_id' },
          { text: 'second element', id: 'second_id' }
        ],
      total: count }
    

    If: init is present, then the function should return:
    {text: 'displayed text', id: 'id_of_initial_element'}
    

    After defining such a class, you can use ajax select2 as follows:
    = select2_ajax_tag :my_select2_name,
                       :system_role,
                       init_value,
                       additional_options
    

    And that’s it. The syntax is as close as possible to select_tag and can be used anywhere in the application.

    Using multi ajax Select2


    Here everything is similar to the previous paragraph with ajax select2 with the only difference being that you need to connect the multi_ajax_select2_value_parser.js script and add select2_options: {multiple: true} to the hash :
    = select2_ajax_tag :multi_countries_select2,
                       :country,
                       '',
                       class: 'is-multiple',
                       select2_options: {multiple: true}
    

    Somewhere here, developers familiar with Select2 should have a question: why the script? I answer: the script implements serialization of the selections in the form of an array, and not as a string with options separated by a comma. The author of Select2 promised to do the same in the next major version, but he did not want to wait.

    Additional features


    Most of them are described in the example . For those who are too lazy to start a project, and just for the sake of completeness, I will do a brief review of them.

    Search_default extension


    Suppose you created your SearchAdapter and implemented search_default in it . Let this adapter be for the User model. Everything is fine, but once it was required to create a similar select, but so that only active users were in the options. In order not to create a new class for the same entity, you can add the search_active method to the previously created UserSearchAdapter and specify this method when initializing Select2 :
    = select2_ajax_tag :active_user,
                       :user,
                       '',
                       search_method: :active # здесь указываем какой search_ метод хотим использовать
    

    Dependent Samples


    Another case: implement 2 (or more) selects, the options in which depend on each other. For example, cascading selection of a country and a city (example from auto_select2_tag_example ). If you have chosen a country, then you can choose a city only inside this country and vice versa. How this is inconveniently done with static selects is clear. And here is how to do it with select2_ajax_tag : firstly, you need to assign all the dependent elements a class, for example dependent-input ; secondly, specify this class in 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'}}
    

    After that, when sending an ajax-request for options, the select will look for all elements with the dependent-input class , exclude itself from them, convert the remaining ones to json, where the key is the name attribute of the element and value is the value attribute and send the received json along with the request. The controller will forward all these parameters to the SearchAdapter and in the search_default method (or any other search_ ), the options parameter will contain values ​​from the form. Then they can be used in any convenient way and give only those options that meet the requirements.

    To_select2 method


    You can omit the text_column and id_column options if the model has a to_select2 method . In this case, it will be automatically called to get options when generating json . If you need to use a method other than to_select2 , you can pass the hash_method parameter :
    = select2_ajax_tag :default_country_id,
                       {class_name: :country, hash_method: :to_select2_alternate},
                       Country.first.id
    

    The benefit is obvious. Without creating a SearchAdapter, you can pass more complex options to the select.

    A few words about initialization


    Elements are automatically initialized after the page loads (that is, in $ (document) .ready () ), after ajax requests (by the ajaxSuccess event ), and after the cocoon: after-insert event from cocoon . There is no need to worry about reinitialization, nothing is called twice and there are no problems. If, for some reason, manual initialization was still required, then you need to call initAutoAjaxSelect2 () and / or initAutoStaticSelect2 () .

    Future plans


    Since gems grew out of the Redmine project, I want to make pluging to it. Of course, the question immediately arises here about the pipeline, which in Redmine does not work in principle and for which you need to work hard . Next, I want to make friends with formtastic .

    For sim everything


    Thank you for reading to the end. For typos and inaccuracies, write to me on PM, I will be glad to fix it. Other questions / comments / suggestions / indignation write in the comments, it will be interesting.

    Also popular now: