Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write angular directive that toggles classes by selector

I'm trying to write a directive that toggles classes based on a selector condition:

<label class-when="{'is-checked': ':has(input:checked)', 'is-disabled': ':has(input:disabled)'}">
    <input type="checkbox">
    Example checkbox
</label>

I need to somehow watch for DOM changes on the element and its descendents but I'm getting an ng:areq error. How can I do this?

define(function (require) {
    var _ = require('lodash');

    return {
        restrict: 'A',
        scope: {
            object: '@classWhen'
        },
        link: function (scope, element) {
            scope.$watchCollection(function() {
                return element.find('*').add(element);
            }, function () {
                _.forOwn(scope.object, function (test, classes) {
                    test = typeof test === 'boolean' ? test : element.is(test);
                    element.toggleClass(classes, test);
                });
            });
        }
    };
});
like image 793
ryanve Avatar asked Oct 06 '16 20:10

ryanve


People also ask

What is the difference between @component and @directive in Angular?

Component is used to break up the application into smaller components. But Directive is used to design re-usable components, which is more behavior-oriented. That is why components are widely used in later versions of Angular to make things easy and build a total component-based model.

What is Ngclass in Angular?

AngularJS ng-class Directive The ng-class directive dynamically binds one or more CSS classes to an HTML element. The value of the ng-class directive can be a string, an object, or an array. If it is a string, it should contain one or more, space-separated class names.

Which directive is used for controller in Angular?

AngularJS ng-controller Directive The ng-controller directive adds a controller to your application. In the controller you can write code, and make functions and variables, which will be parts of an object, available inside the current HTML element. In AngularJS this object is called a scope.

How do I create a custom directive?

To create a custom directive we have to replace @Component decorator with @Directive decorator. So, let's get started with creating our first Custom Attribute directive. In this directive, we are going to highlight the selected DOM element by setting an element's background color. Create an app-highlight.


2 Answers

Okay, after reading your bounty comment, I understand that you want this to be controller independent. I spent some time completely reworking my solution, and I think I have finally figured out a way to accomplish what you want.

It really comes down to 2 things:

1) Detecting a change on the checkbox :checked status, and

2) Detecting a change on the checkbox :disabled status

Detecting 1) was easy, as you can use a simple jQuery change handler, but detecting 2) took a bit more research. It requires the use of scope.$watch on the child ng-disabled attribute.

Here is a demo of how this would work:

var app = angular.module("myApp", [])
  .directive("classWhen", function() {
    function setClasses(classWhen, $element, $input) {
      Object.keys(classWhen).forEach(function(key) {
        var test = classWhen[key];
        // .toggleClass("className", true) === .addClass("className")
        // .toggleClass("className", false) === .removeClass("className")
        $element.toggleClass(key, $element.is(test));
      });
    }
    return {
      restrict: 'A',
      link: function link (scope, element, attrs) {
        var classWhen = JSON.parse(attrs.classWhen);
        var $element = $(element);
        $element.find("*").each(function (index, elem) {
          var $elem = $(this);
          // namespace the change event so we can easily .off() it
          $elem.off("change.classWhen");
          $elem.on("change.classWhen", function () {
            setClasses(classWhen, $element, $elem);
          });
          // watch child ng-disabled attribute
          scope.$watch($elem.attr("ng-disabled"), function (val) {
            setClasses(classWhen, $element, $elem);
          });
        });
      }
    };
  });
.is-checked {
  background-color: yellow;
}
.is-disabled {
  background-color: lightblue;
}
<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/2.1.1/jquery.min.js"></script>
<div ng-app="myApp">
  <label>
    <input type="checkbox" ng-model="disableAll" />Disable All</label>
  <br>
  <br>
  <label class-when='{"is-checked": ":has(input:checked)", "is-disabled": ":has(input:disabled)"}'>
    <input type="checkbox" ng-disabled="disableAll">Example checkbox 1
  </label>
  <br>
  <label class-when='{"is-checked": ":has(input:checked)", "is-disabled": ":has(input:disabled)"}'>
    <input type="checkbox" ng-disabled="disableAll">Example checkbox 2
  </label>
  <br>
  <label class-when='{"is-checked": ":has(input:checked, .test)", "is-disabled": ":has(input:disabled)"}'>
    <input type="text" ng-disabled="disableAll" ng-class="testingClass" ng-model="testingClass"/>
    <br>
    <input type="checkbox" ng-disabled="disableAll">
    Example checkbox 3
  </label>
  <br>
  <label class-when='{"is-checked": ":has(input:checked)", "is-disabled": ":has(input:disabled)"}'>
    <input type="checkbox" ng-disabled="disableAll">Example checkbox 4
  </label>
</div>
like image 146
mhodges Avatar answered Oct 06 '22 02:10

mhodges


Here's my attempt to solve your problem in generic way: https://plnkr.co/edit/kVyJQClmQhF3FeBTmKaX?p=preview

Directive code:

app.directive('classWhen', () => (scope, element, attrs) => {
  scope.$watchCollection(getClasses, setClasses)

  function getClasses() {
    // We have to evaluate expression in attrs.classWhen so that we get object
    const rawClasses = scope.$eval(attrs.classWhen)
    const classes = {}

    // then we normalize it so that strings are treated as jquery selectors
    Object.keys(rawClasses).forEach((className) => {
      const expr = rawClasses[className]
      classes[className] = typeof expr === 'string'
        ? element.is(expr)
        : !!expr // we normalize falsy values to booleans
    })

    // then we return it to $watchCollection
    return classes
  }

  /**
   * will be called whenever any of the classes changes
   */
  function setClasses(classes) {
    Object.keys(classes).forEach((className) => {
      element.toggleClass(className, classes[className])
    })
  }
})
like image 39
Bartosz Gościński Avatar answered Oct 06 '22 01:10

Bartosz Gościński