Implementing select2 remote search

Hi everyone,

I’m using Angular 5 and I have made an Angular component called Select2Dropdown for select2 plugin.
My version of Select2 is 4.0.3.
Currently as far as I know, select2 uses only local data for searching.
My goal is to customize my Select2Dropdown to use remote search as well.

Currently, I already manage to do that but it’s not good as it should.

Problems:

  1. The first problem is that search events (keypress, keyup, or else) are not triggering if select2 has no loaded data (options). I was able to ‘trick’ that by inserting hidden option to my select2.
    How can I change this to be able to trigger search with no data available?

  2. Second problem is that I know only for matcher callback method that I can replace with my custom match function that will handle custom search matching and I made that.
    This function looks something like this:

     searchCustomers(params, data) {
     // If there are no search terms, return data
     if ($.trim(params.term) === '') {
       return data;
     }
    
     if (this.customerSearchParams === params.term) {
       return data;
     } else {
       this.customerSearchParams = params.term;
       this.customers = [];
    
       this.customerService.getCustomers()
         .subscribe(
         // Successful responses call the first callback.
         customers => {
           this.customers = customers;
    
       $('#mySelect2').select2('close');
       $('#mySelect2').select2('open');
    
       // Get the search box within the dropdown or the selection
       const $search = $('#mySelect2').data('select2').dropdown.$search ||
       $('#mySelect2').data('select2').selection.$search;
    
        $search.val(params.term);
      }
     });
      }
    
      return data;
      }
    

So, on each search keypress my callback searchCustomers() is called and I’m fetching my customers from database. This is asynchronous method and after success my select2 is loaded with new data and it shows new data in the search results but if I select any of that data it just selects empty.
I manage to ‘trick’ select2 by closing and reopening select2 dropdown and replacing search text with previous value. That happens so fast that user doesn’t notice anything.
But the problem is that each keystroke triggers fetching from database and on success it reopens select2 so if I started to write some word in search it stops and continue and it swallowed me a letter.
So, if I write slowly everything works :slight_smile: but that is not what it should be.

I was wondering how can I change search trigger event to be triggered on pressing enter key?

So to summarize:

  1. How to trigger search if no data?
  2. How to replace search trigger from any keypress to pressing enter key only?
  3. Any other suggestion will be very welcome!!

Best regards,
Josip

Have you looked at the AJAX remote-data retrieval capability, and in particular the rate-limiting capability? https://select2.org/data-sources/ajax#rate-limiting-requests

If you could filter the results on the server side (assuming you control the server side), you should be able to do what you need without requerying the remote data source in the custom matcher.

1 Like

Update:

My first solution/improvisation with remote-data search was not good.
I tried to use angular services for Api calls and that didn’t work well with select2 because select2 has his own ajax calls for remote search.

Now, I changed remote calls to select2 ajax calls and I it looks like this:

$('#myDropdown).select2({
        minimumInputLength: 2,
        ajax: {
          url: 'http://local.api/Article/GetAll',
          headers: { 'Authorization': 'Bearer ' + this.globals.token },
          type: 'POST',
          dataType: 'json',
          delay: 250,
          data: (params) => {
            const searchOptions = this.remoteSearchOptions;
            if (!!(searchOptions) === false) {
              return null;
            }
            searchOptions.filterByColumnValue = params.term;
            searchOptions.offset = params.page || 0;
            return searchOptions;
          },
          processResults: (data, params) => {
            // parse the results into the format expected by Select2
            // since we are using custom formatting functions we do not need to
            // alter the remote JSON data, except to indicate that infinite
            // scrolling can be used
            params.page = params.page || 1;

            // change search offset
            this.remoteSearchOptions.offset = data.length;

            return {
              results: data,
              pagination: {
                more: (params.page * this.remoteSearchOptions.pageSize) <= data.length
              }
            };
          },
          cache: true
        },
        escapeMarkup: function (markup) { return markup; }, // let our custom formatter work
        templateResult: this.formatResult,
        templateSelection: this.formatResultSelection
      });

 private formatResult(item) {
    // show 'Searching...' text while loading
    if (item.loading) {
      return item.text;
    }

    const markup =
      '<table class="dataTable">' +
      '<tbody>' +
      '<tr>' +
      '<td>' + item.code + '</td>' +
      '<td>' + item.title + '</td>' +
      '<td>' + item.unitOfMeasure.title + '</td>' +
      '<td>' + item.price+ '</td>' +
      '</tr >' +
      '</tbody>' +
      '</table>';

    return markup;
  }

  private formatResultSelection(item) {
    return !!(item.id) === true ? item.title : item.text;
  }

It works fine, results are loaded as it should in a table columns and selection works by selecting item’s title. But there is still one problem . . .

Problem:
Select2 is now using item’s id as a value, and item title as a visible text.
I need to work with objects and not with Ids, so I need to transform (map) select2 id and text to be something like:

const item = {
      id: JSON.stringify(obj),
      text:obj.title
};

… and once I have id as stringified object, I can use ‘change’ event to handle selection, parse stringified object and emit it to component - something like:

$('#myDropdown).on('change', (e: any) => {
      const value = $(e.target).val();
      let obj: any;
        try {
          obj = JSON.parse(value);
        } catch (e) {
          obj = JSON.parse(null);
        }
      }
      this.ngModelChange.emit(obj); // change emitter
}

Question:

  • How to transform/map recieved ajax-remote-data to stringify each object as id so that selecting my table row will send stringified object as a .val() in ‘change’ event !?

Please help me solve this :wink:

:: cheers ::

You can use the Array.prototype.map function to do it:

// ...
return {
    results: data.map(function(obj) {
        return { 'id': JSON.stringify(obj),
                 'text': obj.title
               };
    });
// ...

However, I’m not sure I’d stringify the objects as the Select2 id values, unless the objects are quite small; otherwise you’re potentially using a lot of memory. Select2 data objects must include at least the id and text properties, but can also contain any other properties you want (in other words, the other properties of your object). Here’s how you can retrieve the Select2 data object for the selected value in the widget: https://select2.org/programmatic-control/retrieving-selections . So you should be able to leave your returned data alone and just grab the data object for the selected option and then load that data into your table.

1 Like

Thanks John30013,

I know about the map method, but I don’t know when/where to call it?

For example, this is the way to call it in a normal, non-remote dropdown:

$('#myDropdown').select2({
      data: $.map(this.items, (obj: any) => {
        const item = {
          id: JSON.stringify(obj),
          text: obj.title
        };
      return item;
    }),
});
  • And I was wondering how to get mapping items this way ?

  • Ajax-remote-template-dropdown has few properties/callbacks where we could probably handle this mapping, but in which one to use:

    • processResults:
    • templateResult:
    • templateSelection

Any help would be great !!

Thanks

Hi again !!

Thanks to John30013 I manage to solve data mapping in ‘processResults’ calback:

    processResults: (data, params) => {
        // parse the results into the format expected by Select2
        // since we are using custom formatting functions we do not need to
        // alter the remote JSON data, except to indicate that infinite
        // scrolling can be used
        params.page = params.page || 1;

        // change search offset
        this.remoteSearchOptions.offset = data.length;

        // mapping data's id (stringified object) and text (visible text value) properties 
        data = data.map((obj) => {
          return {
            'id': JSON.stringify(obj),
            'text': obj.title
          };
        });

        return {
          results: data,
          pagination: {
            more: (params.page * this.remoteSearchOptions.pageSize) <= data.length
          }
        };
      },

# NEXT PROBLEM:
I have edit component which has to load previously saved dropdown value.
On component load, I need to fetch binded dropdown value remotely and load it to dropdown!

How to do that !?

  • Before this implementation, I had an items array and my dropdown was generated from that array. Then, on component load I called http get by id and push the item in array + select it. That way I manage to solve this problem. But how to do that with ajax-remote-data-template-dropdown?
1 Like

Finaly solved !!

# EDIT component:
On edit we need to get previously saved dropdown object by ID from database.
Once we fetch binded object, we need to append <option> to the dropdown and select that value.

I made small method for that:

# Docs:

# Method:

  appendSelectedValue() {
     const newOption = new Option(this.editArticle.title, JSON.stringify(this.editArticle), true, true);
     $('#myDropdown').empty().append(newOption).trigger('change');
  }

You can call this method after object is fetched from database successfully.

Good luck !!

:: cheers ::

Hi, Josip–

I’m glad you were able to solve your problems. If my suggestions helped you, would you mind clicking the “heart” icon next to one of my responses? It helps my reputation on this forum.

Thanks again!

–John

1 Like