Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Bootstrap-Select timing issue to refresh

I love Bootstrap-Select and I am currently using it through the help of a directive made by another user joaoneto/angular-bootstrap-select and it works as intended except when I try to fill my <select> element with an $http or in my case a dataService wrapper. I seem to get some timing issue, the data comes after the selectpicker got displayed/refreshed and then I end up having an empty Bootstrap-Select list.. though with Firebug, I do see the list of values in the now hidden <select>. If I then go in console and manually execute a $('.selectpicker').selectpicker('refresh') it then works.
I got it temporarily working by doing a patch and adding a .selectpicker('refresh') inside a $timeout but as you know it's not ideal since we're using jQuery directly in an ngController...ouch!

So I believe the directive is possibly missing a watcher or at least something to trigger that the ngModel got changed or updated.

Html sample code:

<div class="col-sm-5">
    <select name="language" class="form-control show-tick" 
        ng-model="vm.profile.language" 
        selectpicker data-live-search="true"
        ng-options="language.value as language.name for language in vm.languages">
    </select>
    <!-- also tried with an ng-repeat, which has the same effect -->
</div>

then inside my Angular Controller:

// get list of languages from DB
dataService
    .getLanguages()
    .then(function(data) {  
        vm.languages = data;

        // need a timeout patch to properly refresh the Bootstrap-Select selectpicker 
        // not so good to use this inside an ngController but it's the only working way I have found
        $timeout(function() {
            $('.selectpicker, select[selectpicker]').selectpicker('refresh');
        }, 1);
    }); 

and here is the directive made by (joaoneto) on GitHub for Angular-Bootstrap-Select

function selectpickerDirective($parse, $timeout) {
  return {
    restrict: 'A',
    priority: 1000,
    link: function (scope, element, attrs) {
      function refresh(newVal) {
        scope.$applyAsync(function () {
          if (attrs.ngOptions && /track by/.test(attrs.ngOptions)) element.val(newVal);
          element.selectpicker('refresh');
        });
      }

      attrs.$observe('spTheme', function (val) {
        $timeout(function () {
          element.data('selectpicker').$button.removeClass(function (i, c) {
            return (c.match(/(^|\s)?btn-\S+/g) || []).join(' ');
          });
          element.selectpicker('setStyle', val);
        });
      });

      $timeout(function () {
        element.selectpicker($parse(attrs.selectpicker)());
        element.selectpicker('refresh');
      });

      if (attrs.ngModel) {
        scope.$watch(attrs.ngModel, refresh, true);
      }

      if (attrs.ngDisabled) {
        scope.$watch(attrs.ngDisabled, refresh, true);
      }

      scope.$on('$destroy', function () {
        $timeout(function () {
          element.selectpicker('destroy');
        });
      });
    }
  };
}
like image 893
ghiscoding Avatar asked Feb 05 '15 04:02

ghiscoding


1 Answers

One problem with the angular-bootstrap-select directive, is that it only watches ngModel, and not the object that's actually populating the options in the select. For example, if vm.profile.language is set to '' by default, and vm.languages has a '' option, the select won't update with the new options, because ngModel stays the same. I added a selectModel attribute to the select, and modified the angular-bootstrap-select code slightly.

<div class="col-sm-5">
    <select name="language" class="form-control show-tick" 
        ng-model="vm.profile.language" 
        select-model="vm.languages"
        selectpicker data-live-search="true"
        ng-options="language.value as language.name for language in vm.languages">
    </select>
</div>

Then, in the angular-bootstrap-select code, I added

if (attrs.selectModel) {
    scope.$watch(attrs.selectModel, refresh, true);
}

Now, when vm.languages is updated, the select will be updated too. A better method would probably be to simply detect which object should be watched by using ngOptions, but using this method allows for use of ngRepeat within a select as well.

Edit:

An alternative to using selectModel is automatically detecting the object to watch from ngOptions.

if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
    scope.$watch(attrs.ngOptions.split(' in ')[1], refresh, true);
}

Edit 2:

Rather than using the refresh function, you'd probably be better off just calling element.selectpicker('refresh'); again, as you only want to actually update the value of the select when ngModel changes. I ran into a scenario where the list of options were being updated, and the value of the select changed, but the model didn't change, and as a result it didn't match the selectpicker. This resolved it for me:

if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
    scope.$watch(attrs.ngOptions.split(' in ')[1], function() {
        scope.$applyAsync(function () {
            element.selectpicker('refresh');
        });
    }, true);
}
like image 107
caseyjhol Avatar answered Sep 20 '22 01:09

caseyjhol