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?

akashrajkn
  • 2,295
  • 2
  • 21
  • 47

4 Answers4

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

akashrajkn
  • 2,295
  • 2
  • 21
  • 47
8

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();
    });
  };
}]);
bhantol
  • 9,368
  • 7
  • 44
  • 81
  • 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? Thanks – Binsoi Jul 14 '16 at 13:04
  • http://plnkr.co/edit/oRmKRV?p=preview is using 9500 items. Woks fine. Can you reproduce modifying that plnkr ? – bhantol Jul 14 '16 at 13:11
  • 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. – Binsoi Jul 14 '16 at 13:30
  • 2
    What is `$select.pageOptions`in `repeat="person in $select.pageOptions.people`? – Jimmy Dec 19 '16 at 14:09
  • 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` – bhantol Dec 19 '16 at 14:13
  • 1
    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. – Jimmy Jan 05 '17 at 14:38
  • Could this solution work with "group-by" option? I tried but I got errors. – Envy Feb 06 '17 at 06:35
  • @Envy `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. – bhantol Feb 06 '17 at 16:26
  • @JimmyBob will get to a PR soon – bhantol Feb 06 '17 at 16:27
  • @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 much – Envy Feb 08 '17 at 01:57
  • 1
    @envy Try http://plnkr.co/edit/GXkvtT?p=preview for grouping - seems to work just fine. – bhantol Feb 08 '17 at 03:12
  • 1
    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? – Envy Feb 09 '17 at 09:01
  • You can watch all-choices and perform the initialization there only when the all-choices return something. – bhantol Feb 11 '17 at 19:10
  • @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. – Remiz Feb 14 '17 at 19:54
  • 1
    a PR to ui-select repo would be great! – burak emre Mar 03 '17 at 03:57
  • how can all-choices be watched inside the directive? – Vlad Slobodkin Jul 08 '17 at 04:17
  • @VladSlobodkin You can try something like `scope.$watchCollection(attr.allChoices , function(newChoices, oldChoices){ });` – bhantol Jul 10 '17 at 14:37
  • ui-select is taking too much time in internet explorer but works fine in other browsers. Can you provide the solution? – Prasanna Jul 31 '17 at 11:13
  • @bhantol How do you pre-select the array item that matches the ngModel value? – Tawani Sep 27 '17 at 13:41
  • 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. – Farhan Ghumra Dec 12 '17 at 11:36
  • 1
    what is `$select.pageOptions.people` what is the replacement of `people`? may sound stupid, unable to figure this – Zameer Fouzan Feb 12 '18 at 07:33
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.

BuddhistBeast
  • 2,652
  • 2
  • 21
  • 29
  • 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 unresponsive – akashrajkn Jul 17 '15 at 08:45
  • 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. – BuddhistBeast Jul 17 '15 at 15:21
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.

d3orn
  • 188
  • 1
  • 8
  • @bhantol 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.people` – Tawani Sep 27 '17 at 13:34