1

I'm trying to build a custom checkbox directive (to be able to style it as I like) not using <input type="checkbox">. It should work as one would expect; updating the model should update the view, clicking the box should update the model, it should listen to the required directive (validation should work).

I currently have the following directive, working exactly as intended:

'use strict';

angular.module('directives.checkbox', [])
  .directive('checkbox', [function () {
    return {
      restrict: 'A',
      require: 'ngModel',
      replace: true,
      template: '<span class="checkbox"></span>',
      link: function (scope, elem, attrs, ctrl) {
        elem.bind('click', function () {
          scope.$apply(function () {
            elem.toggleClass('checked');
            ctrl.$setViewValue(elem.hasClass('checked'));
          });
        });

        ctrl.$render = function () {
          if (!elem.hasClass('checked') && ctrl.$viewValue) {
            elem.addClass('checked');
          } else if (elem.hasClass('checked') && !ctrl.$viewValue) {
            elem.removeClass('checked');
          }
        };
      }
    }
  }]);

It's just a <span> with a CSS class checkbox and another CSS class checked for the checked state. However, it does not seem very angular-like (or best practice) to use jQuery to toggle the class and update the view like I do. I would rather use the ng-class and ng-click directives, but that implies I need to use an isolated scope (to avoid scope state clashes when using multiple checkboxes on the same page). For some reason, the isolated scope makes angular stop calling $render() on ctrl.

Does anyone know if this is the correct way, or if there's a more angular-like approach which would still solve the requirements I have?

3 Answers 3

1

First of all I believe that if styling is your problem you could definitely get away with customizing the input through css rather than creating a directive that has to mimic the input checkbox.

That being said, the angular way of adding and removing classes is to actually include then in your template either as expression or as arguments to ngClass. Also you can avoid the bind by including an ngClick into your template.

app.directive('myCheckbox', function() {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    template: '<span class="checkbox" ng-class="{checked: isChecked}" ng-click="toggleMe()"></span>',
    scope: { isChecked: '=?'},
    link: function(scope, elem, attrs) {
      scope.isChecked = true;

    scope.toggleMe = function() {
        scope.isChecked = !(scope.isChecked);
    }
 }
 });

Here is a little plunk to explain what I mean

Sign up to request clarification or add additional context in comments.

6 Comments

I really don't understand why I was down voted. I provided a working solution with an example without only using angular and no jquery. In any case a down vote should be justified with a comment.
I didn't downvote you, but the problem with your solution is that multiple checkboxes can not be on the same page, they would share the isChecked scope variable.
Hi, thank you for the comment. I didn't use an isolated scope in order to prevent adding to the confusion. I just wanted to demonstrate how to manipulate the dom without using jQuery. I have now update my answer (and the plunker) to feature an isolated scope. The binding is also optional in case you don't want to bind to something in the outer scope. It could also be done with a dependency on ngModel value.
Thank you, but this still does not fully answer my question. I am aware of how scope and isolate scope works, but my question is regarding ngModel and $ctrl.render() which does not seem to work well with isolated scopes. Your solution does not work with ngModel (and the validation directives that comes with it such as required).
That's a good idea. Post it as an answer and I'll accept it :)
|
0

$render function is not invoked for checkbox. Instead of providing render function, we can watch for model change and check the element state and update the UI accordingly.

Below is the directive code.

var app = angular.module('RadioOrCheck', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
});

app.directive('radioOrCheck', radioOrCheckDirective);

function radioOrCheckDirective() {
  return {
    require: 'ngModel',
    restrict: 'A',
    scope: false,
    link: link
  };

  function link(scope, element, attrs) {
    element.addClass('hidden-input').wrap('<label/>');
    element.parent().addClass(attrs.type).append('<span/>');

    scope.$watch(attrs.ngModel, function updateUI() {
      element.parent().toggleClass('checked', jQuery(element).is(':checked'));
    });

    attrs.$observe('label', function(newLabel) {
      element.parent().find('span').text(newLabel);
    });
  }
}
.hidden-input {
  border: 0;
  clip: rect(0 0 0 0);
  height: 0;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 0;
}
.radio {
  display: block;
  cursor: pointer;
  color: red;
}
.radio.checked {
  color: green;
}
.checkbox {
  display: block;
  cursor: pointer;
  color: red;
}
.checkbox.checked {
  color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<input radio-or-check="" type="checkbox" ng-model="cbModel" data-label="CB Label" />
<br/>
<input radio-or-check="" type="checkbox" ng-model="cbModel" data-label="CB Label" />
<br/>
<input radio-or-check="" type="checkbox" ng-model="cbModel" data-label="CB Label" />
<br/>
<br/>
<input radio-or-check="" type="radio" ng-model="rModel" data-label="R Label" value="1" />
<br/>
<input radio-or-check="" type="radio" ng-model="rModel" data-label="R Label" value="2" />
<br/>
<input radio-or-check="" type="radio" ng-model="rModel" data-label="R Label" value="3" />
<br/>

Here is the working plunker. Input fields here are mocking the default ones, CSS can be tweaked to your need.

Comments

0

$render function is not invoked for checkbox. Instead of providing render function, we can watch for model change and check the element state and update the UI accordingly.

Below is the directive code.

var app = angular.module('RadioOrCheck', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
});

app.directive('radioOrCheck', radioOrCheckDirective);

function radioOrCheckDirective() {
  return {
    require: 'ngModel',
    restrict: 'A',
    scope: false,
    link: link
  };

  function link(scope, element, attrs) {
    element.addClass('hidden-input').wrap('<label/>');
    element.parent().addClass(attrs.type).append('<span/>');

    scope.$watch(attrs.ngModel, function updateUI() {
      element.parent().toggleClass('checked', jQuery(element).is(':checked'));
    });

    attrs.$observe('label', function(newLabel) {
      element.parent().find('span').text(newLabel);
    });
  }
}
.hidden-input {
  border: 0;
  clip: rect(0 0 0 0);
  height: 0;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 0;
}
.radio {
  display: block;
  cursor: pointer;
  color: red;
}
.radio.checked {
  color: green;
}
.checkbox {
  display: block;
  cursor: pointer;
  color: red;
}
.checkbox.checked {
  color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app="RadioOrCheck">
<input radio-or-check="" type="checkbox" ng-model="cbModel" data-label="CB Label" />
<br/>
<input radio-or-check="" type="checkbox" ng-model="cbModel" data-label="CB Label" />
<br/>
<input radio-or-check="" type="checkbox" ng-model="cbModel" data-label="CB Label" />
<br/>
<br/>
<input radio-or-check="" type="radio" ng-model="rModel" data-label="R Label" value="1" />
<br/>
<input radio-or-check="" type="radio" ng-model="rModel" data-label="R Label" value="2" />
<br/>
<input radio-or-check="" type="radio" ng-model="rModel" data-label="R Label" value="3" />
<br/>
</div>

Here is the working plunker. Input fields here are mocking the default ones, CSS can be tweaked to your need.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.