How to enable infinite scrolling in select2 4.0 without ajax
Asked Answered
E

8

13

I am using select2 with custom data adapter. All of the data provided to select2 is generated locally in web page (so no need to use ajax). As query method can generate a lot of results (about 5k) opening select box is quite slow.

As a remedy, I wanted to use infinite scroll. Documentation for custom data adapter says that query method should receive page parameter together with term:

@param params.page The specific page that should be loaded. This is typically provided when working with remote data sets, which rely on pagination to determine what objects should be displayed.

But it does not: only term is present. I tried to return more: true or more: 1000, but this didn't help. I guess this is because, by default, infinite scroll is enabled iff ajax is enabled.

I am guessing that enabling infinite scroll will involve using amd.require, but I am not sure what to do exactly. I tried this code:

$.fn.select2.amd.require(
    ["select2/utils", "select2/dropdown/infiniteScroll"],
    (Utils, InfiniteScroll) =>
      input.data("select2").options.options.resultsAdapter = 
        Utils.Decorate(input.data("select2").options.options.resultsAdapter, InfiniteScroll)
)

This is coffee script, but I hope that it is readable for everyone. input is DOM element containing select box - I earlier did input.select2( //options )

My question is basically, how do I enable infinite scroll without ajax?

Epiphenomenon answered 24/9, 2015 at 8:24 Comment(2)
I would be very interested in an answer to this. Did you figure anything out?Spotlight
@happytimeharry Yes, I did. I described my solution in the answer. I hope it helps!Epiphenomenon
E
15

Select2 will only enable infinite scroll, if ajax is enabled. Fortunately we can enable it and still use our own adapter. So putting empty object into ajax option will do the trick.

$("select").select2({
  ajax: {},
  dataAdapter: CustomData
});

Next, define your own data adapter. Inside it, inn query push pagination info into callback.

    CustomData.prototype.query = function (params, callback) {
        if (!("page" in params)) {
            params.page = 1;
        }
        var data = {};
        # you probably want to do some filtering, basing on params.term
        data.results = items.slice((params.page - 1) * pageSize, params.page * pageSize);
        data.pagination = {};
        data.pagination.more = params.page * pageSize < items.length;
        callback(data);
    };

Here is a full fiddle

Epiphenomenon answered 15/10, 2015 at 9:54 Comment(7)
Searching seems to be broken with this solution?Joanne
@Joanne i expanded on the answer to show how to keep search intact right here https://mcmap.net/q/330432/-how-to-enable-infinite-scrolling-in-select2-4-0-without-ajaxSpotlight
Ah! Missed that! Let me check out your answer (then give you a +1 on the answer)...Joanne
This answer deserved more likes! perfectly working example, yet I don't grasp why they had to change from version 3 to version 4 of Select2 and opt for such a crazy syntax!!!Braasch
But from your example the filter options not working. if you try to seach something its not working. Can you bring the matcher feature to work better way.?Sicular
It doesn't work on fiddle because the github resources are gone. here's one with the resources on a cdn jsfiddle.net/m07c1Ldv/1Tangle
may i know how the code in fiddle should be changed when you have two different dropdowns in the same page?Condign
A
12

I found it was just easier to hijack the ajax adapter rather than creating a whole new CustomAdapter like the above answers. The above answers don't actually seem to support paging because they all start from array, which doesn't support paging. It also doesn't support delayed processing.

window.myarray = Array(10000).fill(0).map((x,i)=>'Index' + i);
    
let timer = null;
$('select[name=test]')
    .empty()
    .select2({
        ajax: {
            delay: 250,
            transport: function(params, success, failure) {
                let pageSize = 10;
                let term = (params.data.term || '').toLowerCase();
                let page = (params.data.page || 1);

                if (timer)
                    clearTimeout(timer);

                timer = setTimeout(function(){
                    timer = null;
                    let results = window.myarray // your base array here
                    .filter(function(f){
                        // your custom filtering here.
                        return f.toLowerCase().includes(term);
                    })
                    .map(function(f){
                        // your custom mapping here.
                        return { id: f, text: f}; 
                    });

                    let paged = results.slice((page -1) * pageSize, page * pageSize);

                    let options = {
                        results: paged,
                        pagination: {
                            more: results.length >= page * pageSize
                        }
                    };
                    success(options);
                }, params.delay);
            }
        },
        tags: true
    });
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.7/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.7/js/select2.full.min.js"></script>
<select name='test' data-width="500px"><option>test</option></select>
Among answered 12/8, 2019 at 21:24 Comment(5)
Best answer! Clear solution!Anniceannie
This works well with a <select> element, but doens't seem to load any data if applied to an <input type="text"> element -- Any solution for this?Propagandize
@Propagandize I'm afraid select2.js only works with selects.Among
Other solutions are deprecated with time or their fiddle are no more working. This solution is simple and best. ThanksGearwheel
Simple and decent working solution. Fiddle for the same: jsfiddle.net/shahjay748/s742ygbf/1James
S
11

Expanding on this answer to show how to retain the search functionality that comes with select2. Thanks Paperback Writer!

Also referenced this example of how to achieve infinite scrolling using a client side data source, with select2 version 3.4.5.

This example uses the oringal options in a select tag to build the list instead of item array which is what was called for in my situation.

function contains(str1, str2) {
    return new RegExp(str2, "i").test(str1);
}

CustomData.prototype.query = function (params, callback) {
    if (!("page" in params)) {
        params.page = 1;
    }
    var pageSize = 50;
    var results = this.$element.children().map(function(i, elem) {
        if (contains(elem.innerText, params.term)) {
            return {
                id:[elem.innerText, i].join(""),
                text:elem.innerText
            };
        }
    });
    callback({
        results:results.slice((params.page - 1) * pageSize, params.page * pageSize),
        pagination:{
            more:results.length >= params.page * pageSize
        }
    });
};

Here is a jsfiddle

Spotlight answered 16/10, 2015 at 16:7 Comment(8)
Can you provide a working jsFiddle? I'm not able to get this one working either.Joanne
@Joanne there you go homieSpotlight
LOL, I almost finished creating a fiddle for you! I did finally get it working!Joanne
yeah, the problem was you are using option tags and I was using JSON directly.Joanne
indeed! sorry you didnt catch that earlier but glad you were able to get it working. the fiddle can help anybody that needs it in the future anyways ;-)Spotlight
I created one more answer (as if 2 wasn't enough!) using the JSON directly for clarification for others as well. It also demonstrates that Select2 4.0.0 actually has good performance. (I had been using Selectize before, but had problems on mobile).Joanne
the jsfiddle is not working, it returns empty results when searching, modifying a working solution with this example in order to add search functionality broke my solution as well...Braasch
the fiddle is still working fine for me using Chrome Version 50.0.2661.102 m. when i search it returns the appropriate results. if the example didnt work for you then why would you modify "a working solution"???Spotlight
J
10

I felt the answers above needed better demonstration. Select2 4.0.0 introduces the ability to do custom adapters. Using the ajax: {} trick, I created a custom dataAdapter jsonAdapter that uses local JSON directly. Also notice how Select2's 4.0.0 release has impressive performance using a big JSON string. I used an online JSON generator and created 10,000 names as test data. However, this example is very muddy. While this works, I would hope there is a better way.

See the full fiddle here: http://jsfiddle.net/a8La61rL/

 $.fn.select2.amd.define('select2/data/customAdapter', ['select2/data/array', 'select2/utils'],
    function (ArrayData, Utils) {
        function CustomDataAdapter($element, options) {
            CustomDataAdapter.__super__.constructor.call(this, $element, options);
        }

        Utils.Extend(CustomDataAdapter, ArrayData);

        CustomDataAdapter.prototype.current = function (callback) {
            var found = [],
                findValue = null,
                initialValue = this.options.options.initialValue,
                selectedValue = this.$element.val(),
                jsonData = this.options.options.jsonData,
                jsonMap = this.options.options.jsonMap;

            if (initialValue !== null){
                findValue = initialValue;
                this.options.options.initialValue = null;  // <-- set null after initialized              
            }
            else if (selectedValue !== null){
                findValue = selectedValue;
            }

            if(!this.$element.prop('multiple')){
                findValue = [findValue];
                this.$element.html();     // <-- if I do this for multiple then it breaks
            }

            // Query value(s)
            for (var v = 0; v < findValue.length; v++) {              
                for (var i = 0, len = jsonData.length; i < len; i++) {
                    if (findValue[v] == jsonData[i][jsonMap.id]){
                       found.push({id: jsonData[i][jsonMap.id], text: jsonData[i][jsonMap.text]}); 
                       if(this.$element.find("option[value='" + findValue[v] + "']").length == 0) {
                           this.$element.append(new Option(jsonData[i][jsonMap.text], jsonData[i][jsonMap.id]));
                       }
                       break;   
                    }
                }
            }

            // Set found matches as selected
            this.$element.find("option").prop("selected", false).removeAttr("selected");            
            for (var v = 0; v < found.length; v++) {            
                this.$element.find("option[value='" + found[v].id + "']").prop("selected", true).attr("selected","selected");            
            }

            // If nothing was found, then set to top option (for single select)
            if (!found.length && !this.$element.prop('multiple')) {  // default to top option 
                found.push({id: jsonData[0][jsonMap.id], text: jsonData[0][jsonMap.text]}); 
                this.$element.html(new Option(jsonData[0][jsonMap.text], jsonData[0][jsonMap.id], true, true));
            }

            callback(found);
        };        

        CustomDataAdapter.prototype.query = function (params, callback) {
            if (!("page" in params)) {
                params.page = 1;
            }

            var jsonData = this.options.options.jsonData,
                pageSize = this.options.options.pageSize,
                jsonMap = this.options.options.jsonMap;

            var results = $.map(jsonData, function(obj) {
                // Search
                if(new RegExp(params.term, "i").test(obj[jsonMap.text])) {
                    return {
                        id:obj[jsonMap.id],
                        text:obj[jsonMap.text]
                    };
                }
            });

            callback({
                results:results.slice((params.page - 1) * pageSize, params.page * pageSize),
                pagination:{
                    more:results.length >= params.page * pageSize
                }
            });
        };

        return CustomDataAdapter;

    });

var jsonAdapter=$.fn.select2.amd.require('select2/data/customAdapter');
Joanne answered 5/11, 2015 at 3:24 Comment(7)
I wish I could up-vote this 10,000+ times. I'm sure there's good reason, but it seems v4 is over-engineered. This took hours to figure out, but thankfully I stumbled across your answer. Thank you @JoanneLorindalorine
This is a great adapter example. However, I'm trying to figure out the best way to map in other custom object values. Normally, you can retrieve the $(selector).select2("data") and get things other than id and text, but this adapter limits that part.Pucker
Any chance of putting example on how this adapter would be implemented on huge select lists (in my case required by design), and accessing data-* attributes? ThanksIntemperance
@Kosta, I've since abandoned the use of Select2. The code was just too messy IMHO. I'm using VueJS now and it's sooo easy and clean to implement this yourself if you wanted to. And there's existing components out there already that are super simple to use: github.com/monterail/vue-multiselectJoanne
@Joanne nice example ... just one question. How do I get around having no returned initial data ie initialValue: [] instead of say initialValue: [2, 8, 6]?Contracted
@Joanne Are you still happy with Vue over Select2?Spotlight
@happytimeharry Yup, very happy. I actually made a autocomplete/filterable select for the VuetifyJs framework select component: vuetifyjs.com/components/selects but we temporarily removed those abilities since there were a few issues we wanted to fix first. Should be back up later this month when I have some free time from work. The trick is to build a list component first, then a menu component, and then finally the select component. So clean and fast (we only load the list items when scrolled). Crushes the complex frustrating code of Select2 IMO.Joanne
B
3

My solution for angular 9

  this.$select2 = this.$element.select2({
    width: '100%',
    language: "tr",
    ajax: {
      transport: (params, success, failure) => {
        let pageSize = 10;
        let page = (params.data.page || 1);
        let results = this.options
          .filter(i => new RegExp(params.data.term, "i").test(i.text))
          .map(i => {
            return {
              id: i.value,
              text: i.text
            }
          });
        let paged = results.slice((page - 1) * pageSize, page * pageSize);

        let options = {
          results: paged,
          pagination: {
            more: results.length >= page * pageSize
          }
        };
        success(options);
      }
    }
  });
}
Babism answered 4/9, 2020 at 13:40 Comment(0)
C
1

Here's a shorter, searchable version for Select2 v4 that has paging. It uses lo-dash for searching.

EDIT New fiddle: http://jsfiddle.net/nea053tw/

$(function () {
    items = []
    for (var i = 0; i < 1000; i++) {
        items.push({ id: i, text : "item " + i})
    }

    pageSize = 50

    jQuery.fn.select2.amd.require(["select2/data/array", "select2/utils"],

    function (ArrayData, Utils) {
        function CustomData($element, options) {
            CustomData.__super__.constructor.call(this, $element, options);
        }
        Utils.Extend(CustomData, ArrayData);

        CustomData.prototype.query = function (params, callback) {

            var results = [];
            if (params.term && params.term !== '') {
              results = _.filter(items, function(e) {
                return e.text.toUpperCase().indexOf(params.term.toUpperCase()) >= 0;
              });
            } else {
              results = items;
            }

            if (!("page" in params)) {
                params.page = 1;
            }
            var data = {};
            data.results = results.slice((params.page - 1) * pageSize, params.page * pageSize);
            data.pagination = {};
            data.pagination.more = params.page * pageSize < results.length;
            callback(data);
        };

        $(document).ready(function () {
            $("select").select2({
                ajax: {},
                dataAdapter: CustomData
            });
        });
    })
});

The search loop is originally from these old Select4 v3 functions: https://mcmap.net/q/326073/-select2-performance-for-large-set-of-items

Convexoconcave answered 29/1, 2019 at 14:5 Comment(3)
This fiddle throws an exception, and the dropdown is empty.Among
Fixed: The fiddle was missing jQuery. Also, jQuery should be used insted of $ in jQuery.fn.select2.amd.require()Convexoconcave
This is good and its working, but how to set selected value here?Paley
C
0

This is not a direct answer: after struggling a lot with this for a while, I ended up switching to selectize. Select2's support for non-Ajax search, since version 4, is terribly complicated, bordering the ridiculous, and not documented well. Selectize has explicit support for non-Ajax search: you simply implement a function that returns a list.

Crake answered 3/11, 2017 at 22:26 Comment(0)
H
0

None of this worked for me. I don't know what original question meant but in my case, I am using angular and HTTP calls and services and wanted to avoid AJAX calls. So I simply wanted to call a service method in place of AJAX. This is not even documented on the library's site but somehow I found the way to do it using transport

ajax: {
        delay : 2000,
        transport: (params, success, failure) => {
          this.getFilterList(params).then(res => success(res)).catch(err => failure(err));
        }
      }

If anyone like me came here for this then There you go!

Housefather answered 4/8, 2020 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.