Search code examples
angularjstwitter-bootstrap-3angularjs-directivebootstrap-multiselect

How to invoke a JQuery method after Angular select directive applies a model change?


I am trying to change the selection of David Stutz's bootstrap-multiselect via Angular ng-model:

<select ng-model="selection" multiple="multiple" id="my-example">
  <option value="cheese">Cheese</option>
  <option value="tomatoes">Tomatoes</option>
  <option value="mozarella">Mozzarella</option>
  <option value="mushrooms">Mushrooms</option>
  <option value="pepperoni">Pepperoni</option>
  <option value="onions">Onions</option>
</select>

The changes to the model only get applied to the underlying select element, but the bootstrap-multiselect doesn't get updated automatically. Looking at its documentation, this is expected: you are required to call multiselect('refresh') afterwards to propagate the changes:

$('#my-example').multiselect('refresh');

My question is:

How to invoke this method when the model changes after Angular is done updating the select element?

Since I need to access the element, I assume directives are the way to go. I was looking at decorators, which in theory I could use to modify the behavior of the built-in select directive, but I don't know how to get my code invoked at the right moment.

I've prepared a plunk to demo the issue:

  • I have two multiselects bound to the same model: a bootstrap-multiselect and a plain one
  • I initialize both with a default selection
  • The first button changes the selection. The plain multiselect is updated immediatelly, but the bootstrap-multiselect appears unchanged.
  • The second button shows the current model value in an alert.
  • The third button calls refresh on bootstrap-multiselect, which causes it to update. This is what I would like to get called automatically by Angular.

Solution

  • In the end I managed to solve my problem with a decorator. I based it on the Directive Decorator Example in AngularJS documentation.

    Unlike ngHrefDirective from the example, selectDirective defines both preLink and postLink, therefore the compile override must also return both. I only needed to change postLink though, where $render is defined. In my version of the method I simply invoked the original method, which updates the select element, and called multiselect('refresh') afterwards, which was my original requirement:

    app.config(['$provide', function($provide) {
      $provide.decorator('selectDirective', ['$delegate', function($delegate) {
        var directive = $delegate[0];
    
        directive.compile = function() {
    
          function post(scope, element, attrs, ctrls) {
            directive.link.post.apply(this, arguments);
    
            var ngModelController = ctrls[1];
            if (ngModelController) {
              originalRender = ngModelController.$render;
              ngModelController.$render = function() {
                originalRender();
                element.multiselect('refresh');
              };
            }
          }
    
          return {
            pre: directive.link.pre,
            post: post
          };
        };
    
        return $delegate;
      }]);
    }]);