Bootstrap "buttons-radio" toggle with AngularJS

All we need is an easy explanation of the problem, so here it is.

I made a small angular module to integrate with the bootstrap “buttons-group” (‘s plus a bit of javascript to make them work like radio buttons. I made it on this module to do the same for checkboxes: http://jsfiddle.net/evaneus/z9rge/

My code is at http://jsfiddle.net/askbjoernhansen/YjMMD/

My questions:

1) Is this the right approach?

2) Will the model be watched three times, or does $scope.$watch() figure out that it’s the same model and just do it once? It seems that way.

3) Is it “correct” to muck around with the DOM in the $watch function like I do? It feels “dirty”, but I guess that’s what I am asking for when I am adding angular to something that’s not angularjs compatible natively. Or?

4) Is there a way to put the ng-model attribute on the btn-group instead of on each button? That’d make it appear a lot cleaner.

You can see it at my jsfiddle above, or the code is here, first the html:

 <!-- to test the two-way model -->
  <input name="test" type="radio" ng-model="myModel['A']" value="left"> Left<br>
  <input name="test" type="radio" ng-model="myModel['A']" value="middle"> Middle<br>
  <input name="test" type="radio" ng-model="myModel['A']" value="right"> Right<br>


myModel A {{myModel['A']}}<br/>

<div class="btn-group" data-toggle="buttons-radio">
  <button type="button" buttons-radio=""
    ng-model="myModel['A']" value="left"   class="btn">Left</button>
  <button type="button" buttons-radio=""
    ng-model="myModel['A']" value="middle" class="btn">Middle</button>
  <button type="button" buttons-radio=""
    ng-model="myModel['A']" value="right"  class="btn">Right</button>
</div>

And the javascript:

angular.module('buttonsRadio', []).directive('buttonsRadio', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function($scope, element, attr, ctrl) {
            element.bind('click', function() {
                $scope.$apply(function(scope) {
                    ctrl.$setViewValue(attr.value);
                });
            });

            // This should just be added once, but is added for each radio input now?
            $scope.$watch(attr.ngModel, function(newValue, oldValue) {
                element.parent(".btn-group").find('button').removeClass("active");
                element.parent(".btn-group")
                .find("button[value='" + newValue + "']").addClass('active');
            });
        }
    };
});​

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

You are invoking the directive three times, once for each button. AFAIK this means the linking function is called three times, binding events and watching for changes. added a little more on this below.

It’s fine to change the DOM when models change, but what you are doing – changing classes and values – can be done automatically by angular using bi-directional data-binding and native directives such as ng-class

You can create a directive that renders buttons given a list of options. For example if you have this model and these options

$scope.myOptions = ["Left", "Middle", "Right"];
$scope.myModel = "Left"

You can create a buttons-radio directive like this

App.directive('buttonsRadio', function() {
    return {
        restrict: 'E',
        scope: { model: '=', options:'='},
        controller: function($scope){
            $scope.activate = function(option){
                $scope.model = option;
            };      
        },
        template: "<button type='button' class='btn' "+
                    "ng-class='{active: option == model}'"+
                    "ng-repeat='option in options' "+
                    "ng-click='activate(option)'>{{option}} "+
                  "</button>"
    };
});​

which can be invoked from your HTML

<buttons-radio class="btn-group" data-toggle="buttons-radio" 
               model='myModel'    
               options='myOptions'>
</buttons-radio>

Things to notice:

  1. The template uses an ng-repeat directive which goes through all options and compiles buttons accordingly.
  2. The directive has its own controller and isolated scope with model and options to accept bi-directional binding, so angular does its magic when the directive changes their values.
  3. If the option matches the current value of the model, then the button is assigned an active class.
  4. When a button is pressed, the method activate (from the directives controller) is called, and it changes the value of the model.

Heres a JSFiddle http://jsfiddle.net/jaimem/A5rvg/4/


EDIT

I wasn’t completely sure about this, but yes, your model will have three different watchers, one for every directive invocation. If you are using Batarang you can see all the watched expressions from the Performance tab and the AngularJS Properties panel. Batarang also exposes an $scope convenience property, so you can look for your model’s watch objects by running the following from the console

$scope.$$watchers.filter(function(w){return w.exp ==="myModel['A']"}); 

Method 2

this is simple

<html>
    <div class="btn-group" data-toggle="buttons">
      <span class="btn btn-primary" ng-click="worktype=1" ng-class="worktype==1?'active':''">
        <input type="radio"   value="1">全职
      </span>
      <span class="btn btn-primary" ng-click="worktype=2" ng-class="worktype==2?'active':''">
        <input type="radio"   value="2">兼职
      </span>
    </div>
</html>

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply