0

I want to set css classes to items of a list depending of subelements matches a certain criterion or not. The structure is like in the following example:

<ul ng-controller="Navigation">
    <li><a href="#">Category A</a>
        <ul>
            <li><a href="a1.html">a1</a></li>
            <li><a href="a2.html">a2</a></li>
        </ul>
    </li>
    <li><a href="#">Category B</a>
        <ul>
            <li><a href="b1.html">b1</a></li>
            <li><a href="b2.html">b2</a></li>
        </ul>
    </li>
    <li><a href="contact.html">contact</a></li>
</ul>

My model is the current page, say a2.html. If a link has the same href attribute as the model value, it should have a certain css class (active). This could be done with this expression:

<a href="a1.html" ng-class="{'active': currentPage == 'a1.html'}>

But this is a bit inelegant, because I have to repeat the file name (a1.html). Would it be possible to pass the current element to a function? Something like this: ng-class="getClass(currentElement)"

The next problem is, that I want to select parent elements depending on whether a child has the class active or not. If <a href="a1.html">a1</a> in my above example is selected, then Category A should get the class active too.

Conclusion

'stewie's solution works, but I came to the conclusion that Angular is not the best tool for this job. It is not a web 'application' (the domain of Angular) but static html which should be enriched a bit.

This simple jQuery snippet does the job:

var activeLink = $("a").filter(function() {
  return $(this).attr("href") == currentPage();
});
activeLink.addClass("active");
activeLink.parents("li").children("a").addClass("active");
deamon
  • 89,107
  • 111
  • 320
  • 448
  • What happens when the currentPage() changes? Wouldnt you have to keep monitoring the currentPage() and then run this again ? – ganaraj Mar 24 '13 at 14:53

2 Answers2

5

It can be done by using a custom directive on your UL element, which would traverse the list whenever the model is changed and set the appropriate 'active' class on matching items. See this Plunker as an example. Please note that the directive can be further optimized. It's only a demonstration.

HTML:

<ul menu ng-controller="Navigation">
  <li><a href="#">Category A</a>
    <ul>
      <li><a href="a1.html">a1</a></li>
      <li><a href="a2.html">a2</a></li>
    </ul>
  </li>
  <li><a href="#">Category B</a>
    <ul>
      <li><a href="b1.html">b1</a></li>
      <li><a href="b2.html">b2</a></li>
    </ul>
  </li>
  <li><a href="contact.html">contact</a></li>
</ul>

JS:

var app = angular.module('plunker', []);
app.controller('Navigation', 
    function($scope) {}
  );

app.directive('menu',
  function(){
    return {
      link: function ($scope, $element) {
        var link, li;
        $scope.$watch('currentPage', function(page){
          activate(page);
        });

        function activate(page){
          angular.forEach($element.find('li'), function(elm){
            li = angular.element(elm);
            link = li.find('a');
            if(link.attr('href') === $scope.currentPage){
              li.addClass('active');
              li.parents('li').addClass('active');
              return;
            }
            li.removeClass('active');
          });
        }
      }
    };
  }
);
Stewie
  • 60,366
  • 20
  • 146
  • 113
  • Thanks, it works. But you have to use jQuery (and not only jqlite as part of Angular) to be able to use `parents`. Angular provides only `parent` what is not sufficient in this case. – deamon Feb 28 '13 at 20:11
  • Yes, but question did not make jQlite a requirement. For that matter, this could be done in plain javascript as well. – Stewie Feb 28 '13 at 21:08
  • You're right, I didn't mention it and actually it is not a requirement at all. I mentioned the need of jQuery only for other readers. – deamon Mar 01 '13 at 21:24
  • Regarding your conclusion: you still need to use that jQuery logic inside the directive, if you want to keep everything [the Angular way](http://stackoverflow.com/a/15012542/1095616) – Stewie Mar 14 '13 at 23:51
0

I had a look at @Stewei 's the solution for this. But writing up the solution using a directive for this just did not make sense to me. I think that the directives are only meant to be written when you are creating a component or perhaps when you are stamping out templates. Even for stamping out templates I would suggest people to use ng-include or something like unless you want the semantics that a custom element provides.

But, I believe the Angular way of doing this is to totally separate out the behavioral logic from the 'App specific' logic. I would argue that you write directives in such a way that they are independent of the controller around it.

Here is a plunkr that actually solves this, hopefully in the most "Angular way" possible. This can be further decoupled, if you could use some kind of a data model for all the link's and could possibly ng-repeat over that model. But that is a choice upto the developer. Since you mentioned that it is a static page, this solution probably suits it best.

http://plnkr.co/edit/DjaYi1ofpFKgKfhtakub?p=preview

In your original question, you did ask if you could pass a currentElement to the getClass method. I have asked this before and the answer is NO. The only place where you could pass a reference to the currentElement is in a directive controller, but that is currently out of topic.

Here is the code that achieves the task at hand.

HTML :

<div ng-controller="NavController">
  <ul >
    <li ng-class="getCategoryClass('A')">
        <a href="#">Category A</a>
        <ul>
            <li ng-class="getClass('a1.html','A')"><a href="a1.html">a1</a></li>
            <li ng-class="getClass('a2.html','A')"><a href="a2.html">a2</a></li>
        </ul>
    </li>
    <li ng-class="getCategoryClass('B')">
      <a href="#">Category B</a>
        <ul>
            <li ng-class="getClass('b1.html','B')"><a href="b1.html">b1</a></li>
            <li ng-class="getClass('b2.html','B')"><a href="b2.html">b2</a></li>
        </ul>
    </li>
    <li ng-class="getClass('contact.html','contact')"><a href="contact.html">contact</a></li>
  </ul>

  <input ng-model="currentPage" />
  <br>
  currentPage: {{currentPage}}
</div>

JS :

app.controller('NavController', 
    function($scope) {
      $scope.currentPage = "";
      $scope.currentCategory = "";
      $scope.pageActive = false;
      $scope.getClass = function(page,category){

        if( page === $scope.currentPage ) {
          $scope.pageActive = true;
          $scope.currentCategory = category;
          return "active";
        }
        else{
          return "";
        }
      };

      $scope.$watch('currentPage',function(val){
        $scope.pageActive = false;
      });

      $scope.onLinkClick = function($event){
        $event.preventDefault();  
      };

      $scope.getCategoryClass = function(category){
        return $scope.currentCategory === category && $scope.pageActive ? "active" : "";
      };
    }
  );
ganaraj
  • 26,841
  • 6
  • 63
  • 59