ui-select multiselect is very slow in displaying the choices
Asked Answered
C

4

20

I ran into this problem, and I don't know how to solve it. I have used a ui-select multiselect in my page. First, a http.get request is made to a url which gets the data, then the ui-select choices are populated. The data is big - the length of the data is 2100. This data is to be shown as choices. (The data is fetched at the beginning during the loading of the page and is stored in an array)

But the problem is each time I click on the multiselect to select a choice, it takes 4-5 seconds to populate the list and the page becomes very slow. What do I do to reduce this time?

The choices data is stored in an array, the datatype is array of strings.

  <ui-select multiple ng-model="selectedFields.name"  style="width: 100%;">
    <ui-select-match placeholder="Select fields...">{{$item}}</ui-select-match>
    <ui-select-choices repeat="fields in availableFields | filter:$select.search">
      {{fields}}
    </ui-select-choices>
  </ui-select>

in the controller,

$scope.selectedFields = {};
$scope.selectedFields.name = [];

$scope.init = function() {

    $http.get(url)
        .success( function(response, status, headers, config) {
            availableFields = response;
        })
        .error( function(err) {
        });
};

$scope.init();

If not this way, is there any other options/choice I can work with which doesn't delay showing the select-choices?

Certainty answered 14/7, 2015 at 11:43 Comment(0)
C
17

This is a known issue in ui-select. I tried the following ways, both work

1) There is a workaround for this - use

| limitTo: 100

This limits the choice display to 100 but all the choices can be selected. Look at this thread for more details.

2) Since some of the time, there is a need to display the entire list in the choices, 1) is not a viable option. I used a different library - selectize.js. Here's a plunker demo given in the page

Certainty answered 16/7, 2015 at 4:54 Comment(0)
C
9

Here is complete solution that decorates uiSelectChoices directive.

Items are populated progressively as the user scrolls.

Also takes care of searches in the scrolls.

Also works for all values of position={auto, up, down}

Example

    <ui-select-choices 
         position="up" 
         all-choices="ctrl.allTenThousandItems"  
         refresh-delay="0"
         repeat="person in $select.pageOptions.people | propsFilter: {name: $select.search, age: $select.search} ">
      <div ng-bind-html="person.name | highlight: $select.search"></div>
      <small>
         email: {{person.email}}
         age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
       </small>
   </ui-select-choices>

Working Plnkr Also with With v0.19.5

The directive

  app.directive('uiSelectChoices', ['$timeout', '$parse', '$compile', '$document', '$filter', function($timeout, $parse, $compile, $document, $filter) {
    return function(scope, elm, attr) {
    var raw = elm[0];
    var scrollCompleted = true;
    if (!attr.allChoices) {
      throw new Error('ief:ui-select: Attribute all-choices is required in  ui-select-choices so that we can handle  pagination.');
    }

    scope.pagingOptions = {
      allOptions: scope.$eval(attr.allChoices)
    };

    attr.refresh = 'addMoreItems()';
    var refreshCallBack = $parse(attr.refresh);
    elm.bind('scroll', function(event) {
      var remainingHeight = raw.offsetHeight - raw.scrollHeight;
      var scrollTop = raw.scrollTop;
      var percent = Math.abs((scrollTop / remainingHeight) * 100);

      if (percent >= 80) {
        if (scrollCompleted) {
          scrollCompleted = false;
          event.preventDefault();
          event.stopPropagation();
          var callback = function() {
            scope.addingMore = true;
            refreshCallBack(scope, {
              $event: event
            });
            scrollCompleted = true;

          };
          $timeout(callback, 100);
        }
      }
    });

    var closeDestroyer = scope.$on('uis:close', function() {
      var pagingOptions = scope.$select.pagingOptions || {};
      pagingOptions.filteredItems = undefined;
      pagingOptions.page = 0;
    });

    scope.addMoreItems = function(doneCalBack) {
      console.log('new addMoreItems');
      var $select = scope.$select;
      var allItems = scope.pagingOptions.allOptions;
      var moreItems = [];
      var itemsThreshold = 100;
      var search = $select.search;

      var pagingOptions = $select.pagingOptions = $select.pagingOptions || {
        page: 0,
        pageSize: 20,
        items: $select.items
      };

      if (pagingOptions.page === 0) {
        pagingOptions.items.length = 0;
      }
      if (!pagingOptions.originalAllItems) {
        pagingOptions.originalAllItems = scope.pagingOptions.allOptions;
      }
      console.log('search term=' + search);
      console.log('prev search term=' + pagingOptions.prevSearch);
      var searchDidNotChange = search && pagingOptions.prevSearch && search == pagingOptions.prevSearch;
      console.log('isSearchChanged=' + searchDidNotChange);
      if (pagingOptions.filteredItems && searchDidNotChange) {
        allItems = pagingOptions.filteredItems;
      }
      pagingOptions.prevSearch = search;
      if (search && search.length > 0 && pagingOptions.items.length < allItems.length && !searchDidNotChange) {
        //search


        if (!pagingOptions.filteredItems) {
          //console.log('previous ' + pagingOptions.filteredItems);
        }

        pagingOptions.filteredItems = undefined;
        moreItems = $filter('filter')(pagingOptions.originalAllItems, search);
        //if filtered items are too many scrolling should occur for filtered items
        if (moreItems.length > itemsThreshold) {
          if (!pagingOptions.filteredItems) {
            pagingOptions.page = 0;
            pagingOptions.items.length = 0;
          } else {

          }
          pagingOptions.page = 0;
          pagingOptions.items.length = 0;
          allItems = pagingOptions.filteredItems = moreItems;

        } else {
          allItems = moreItems;
          pagingOptions.items.length = 0;
          pagingOptions.filteredItems = undefined;
        }


      } else {
        console.log('plain paging');
      }
      pagingOptions.page++;
      if (pagingOptions.page * pagingOptions.pageSize < allItems.length) {
        moreItems = allItems.slice(pagingOptions.items.length, pagingOptions.page * pagingOptions.pageSize);
      }

      for (var k = 0; k < moreItems.length; k++) {
        pagingOptions.items.push(moreItems[k]);
      }

      scope.calculateDropdownPos();
      scope.$broadcast('uis:refresh');
      if (doneCalBack) doneCalBack();
    };
    scope.$on('$destroy', function() {
      elm.off('scroll');
      closeDestroyer();
    });
  };
}]);
Chlorate answered 28/6, 2016 at 13:18 Comment(21)
Hi, i tried this. it works. but i've got a problem. if i select the items in 9k+ below. the ui select choice won't open anymore. What could be the possible problem? ThanksBoynton
plnkr.co/edit/oRmKRV?p=preview is using 9500 items. Woks fine. Can you reproduce modifying that plnkr ?Chlorate
I tried inspecting the ui-select and found the div of the choices opacity was set to 0. i checked the select_0_17.js and change this line from 0 to 1. // Reset the position of the dropdown. #1333 dropdown[0].style.opacity = 1; I'm using it in chrome app btw.Boynton
What is $select.pageOptionsin repeat="person in $select.pageOptions.people?Celestina
It is for the internal purposes of the directive. The template is passing nothing in here. It just needs to be some object on $select which is in the isolated scope. Ideally the directive could generate the repeat. My local implementation has much diverged from this answer but ideally this feature needs to be implemented in the u-select itself. But it work for now. If anyone is interested I can get rid of that 'pagingOptions`Chlorate
Thanks for the explanation and for the code. Your solution does indeed work. If you have the time you should try and open a PR for the ui-select team to consider.Celestina
Could this solution work with "group-by" option? I tried but I got errors.Nitroso
@Nitroso group-by with paging gets tricky as you have slide the page window across the pages - where we could be displaying just a partial group. Will try to update the plnkr.Chlorate
@JimmyBob will get to a PR soonChlorate
@bhantol: Yes, I see. Beside of that, your code is not compatible with new version of ui-select (v0.19.5). Could you please try with new version? Thanks so muchNitroso
@envy Try plnkr.co/edit/GXkvtT?p=preview for grouping - seems to work just fine.Chlorate
We have one bug: If the data (which got from the server) in att all-choices is returned after ui-select rendered, we will received the error: Cannot read property 'length' of undefined at line pagingOptions.page * pagingOptions.pageSize < allItems.length. I works if I use ng-if for this case. Do you have better suggestion for me?Nitroso
You can watch all-choices and perform the initialization there only when the all-choices return something.Chlorate
@Envy, I discovered the same problem with multi-select. It's actually possible to reproduce it on the Plnkr, all you need to do is to remove every item from the initial list and try to search again, it'll throw this error message.Egide
a PR to ui-select repo would be great!Astaire
how can all-choices be watched inside the directive?Placentation
@VladSlobodkin You can try something like scope.$watchCollection(attr.allChoices , function(newChoices, oldChoices){ });Chlorate
ui-select is taking too much time in internet explorer but works fine in other browsers. Can you provide the solution?Mercuri
@Chlorate How do you pre-select the array item that matches the ngModel value?Homeopathist
This doesn't load the residual records if the total records length % page size is not equal to 0. For ex if there is total 105 records then only 100 records are displayed. Also search by particular property is not working.Casimiracasimire
what is $select.pageOptions.people what is the replacement of people? may sound stupid, unable to figure thisMoller
M
4

As stated, ui-select is having quite a few performance issues, but there is a workaround for the limit issue.

If you follow akashrajkn's approach then you will notice that it will actually cut out important pieces of data because it will only render 100 at a time. There is a fix that has passed the unit tests and it can be found on the thread here:

https://github.com/angular-ui/ui-select/pull/716

Basically, if you are storing the javascript file locally, then you can adjust the unminified version. All you need to do is implement the changes he made in the pull request and it should help out significantly. In order to apply the limiting factor, take a look at the below, modified example:

<ui-select multiple ng-model="selectedFields.name" limit = "10"  style="width: 100%;">
    <ui-select-match placeholder="Select fields...">{{$item}}</ui-select-match>
    <ui-select-choices repeat="fields in availableFields | filter:$select.search | limitTo:$select.limit ">
      {{fields}}
    </ui-select-choices>
</ui-select>

The above will limit your data in the drop down while also maintaining the level of consistency needed.

Merited answered 16/7, 2015 at 18:41 Comment(2)
I tried this, but it still poses the same problem, display of choices takes almost 5 seconds every time and during that time, the page becomes unresponsiveCertainty
Right, and that is going to happen because of the performance issues with ui-select. The only way around that is to actually go in the file and remove the majority of useless code that is centered around the keydown/keyup events (useless unless you need them of course). There are other select2 plugins for angular but it seems like they are not as supported at ui-select. And, of course, you could always try to rewrite the functionality you need.Merited
B
4

Because I cannot leave a comment (not enough rep) I write this as an answer and I am sorry it is no answer for the problem.

@bhantol I changed the following line of code to your solution which is working perfectly for me so far

for (var k = 0; k < moreItems.length; k++) {
  pagingOptions.items.push(moreItems[k]);
}

for (var k = 0; k < moreItems.length; k++) {
  if (pagingOptions.items.indexOf(moreItems[k]) == -1){
    pagingOptions.items.push(moreItems[k]);
  }
}

This prevents duplicated items from showing up if the user is starting to write a filter and then deletes it.

Also I just figured out that if the list is smaller than 20 items it will not work so I changed:

if (pagingOptions.page * pagingOptions.pageSize < allItems.length) {
  moreItems = allItems.slice(pagingOptions.items.length, pagingOptions.page * pagingOptions.pageSize);
}

to:

if (pagingOptions.page * pagingOptions.pageSize < allItems.length) {
  moreItems = allItems.slice(pagingOptions.items.length, pagingOptions.page * pagingOptions.pageSize);
}
else{ moreItems = allItems;}

Maybe this will help you somehow and sorry again for not answering the question.

Barca answered 13/7, 2016 at 9:4 Comment(1)
@Chlorate the list does not preselect the selected item if the ngModel value is passed from a query string. or If the user hits the "reload" page button. person.id as person in $select.pageOptions.peopleHomeopathist

© 2022 - 2024 — McMap. All rights reserved.