6

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');
        });
      });
    }
  };
}
ghiscoding
  • 12,308
  • 6
  • 69
  • 112

2 Answers2

9

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);
}
caseyjhol
  • 3,090
  • 2
  • 19
  • 23
  • Very interesting concept, this gave me the idea of using the refresh without even the need of an attribute. Since we already have the object name directly inside the `ngOptions` you could do it by only modifying the directive itself and adding something like the following: `if (attrs.ngOptions && / in /.test(attrs.ngOptions) { scope.$watch(attrs.ngOptions.split(' in ')[1], refresh, true); }` ... edit your answer with this option, or better, and I'll accept your answer... thanks a lot :) – ghiscoding Feb 07 '15 at 00:42
  • This is a great solution for me, Thanks! – WiseGuy Oct 25 '16 at 18:26
0

Well, this is an old one... But I had to use it. This is what I added in the link(..) function of the directive:

            scope.$watch(
            _ => element[0].innerHTML,
            (newVal, oldVal) => {
                if (newVal !== oldVal)
                {
                    element.selectpicker('refresh');
                }
            }
            )
Jelmer Jellema
  • 1,072
  • 10
  • 16