Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Karsten Pearce
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Karsten Pearce@vexeddeveloper )

Class Attribute Interpolation Is Safer In AngularJS 1.2 And Newer

By Ben Nadel on

NOTE: If you started using AngularJS with 1.2 or later, this post will only have academic value for you.

As of AngularJS 1.4, you can now mix Array and Object notation in the ngClass directive. This would enable you to pass in both dynamic class names as well as conditional classes. But, this got me thinking: why do I have a vague negative feeling when it comes to using class-attribute interpolation to generate dynamic class names? After digging through the code, I remembered that in older versions of AngularJS (pre-1.2), it could be unsafe to use the class interpolation in certain rare edge-cases. But, as of AngularJS 1.2, those edge-cases have been solved.

There have always been two ways to add dynamic classes to an element in AngularJS. You can use attribute interpolation to dynamically change the class attribute value:

class="widget widget-{{ item.subclass }}"

Or, you can use the ngClass directive to conditionally add or remove classes:

ng-class="{ active: item.isActive, expired: item.isExpired }"

In addition to these declarative approaches, you can also use a custom directive to add and remove classes from within the directive's link function. This is what the ngHide and ngShow directives do, conditionally adding and removing the ".ng-hide" class. But, in AngularJS 1.0.8 (and earlier), this is where we could run into problems with edge-cases in class attribute interpolation.

Before AngularJS 1.2, when animations were introduced, class attribute interpolation was just like any other attribute interpolation. Under the hood, it used $observe() to watch the class expression; then, when the expression result changed, it would overwrite the class attribute. This overwrite action would override any class-based changes that sibling directives may have made.

So for example, in a workflow like this, you end up losing some state:

  1. User goes to sort an element with class attribute interpolation.
  2. Sort plugin adds new class "X" during sort operation.
  3. During sort, the interpolation result changes.
  4. The attribute interpolation directive updates the class attribute, removing "X".

As of AngularJS 1.2, however, class attribute interpolation is now a special case. If you look at the interpolation directive, the logic now branches for "class" and "everything else." If the interpolation is being applied to a class attribute, the class attribute value is never overridden in totality. Instead, the changes in the class expression are used to conditionally add and remove specific classes using the $animate service. This means that, as of AngularJS 1.2, class attribute interpolation can safely live alongside other directives, like ngShow and ngHide, which mutate element classes.

To see this in action, I've put together a small demo which uses both class attribute interpolation and the ngClass directive to define the classes for items in an ngRepeat list:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Class Attribute Interpolation Much Safer In AngularJS 1.2 And Newer
  • </title>
  •  
  • <style type="text/css">
  •  
  • li.friend {
  • border: 1px solid #CCCCCC ;
  • margin-bottom: 10px ;
  • padding: 10px 10px 10px 10px ;
  • }
  •  
  • li.friend-nth0 {
  • border-width: 1px ;
  • }
  •  
  • li.friend-nth1 {
  • border-width: 4px ;
  • }
  •  
  • li.friend-nth2 {
  • border-width: 8px ;
  • }
  •  
  • li.best-friend {
  • border-color: gold ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • Class Attribute Interpolation Much Safer In AngularJS 1.2 And Newer
  • </h1>
  •  
  • <ul ng-controller="AppController">
  • <!--
  • Notice that are using two different sources of class names: we're using the
  • attribute interpolation on [class] to build dynamic class names and we're
  • using [ng-class] to toggle conditional class names.
  • -->
  • <li
  • ng-repeat="friend in friends"
  • class="friend friend-nth{{ $index }}"
  • ng-class="{ 'best-friend': friend.isBFF }">
  •  
  • {{ friend.name }}
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope ) {
  •  
  • // I am the collection of friends being rendered.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Frannie",
  • isBFF: false
  • },
  • {
  • id: 2,
  • name: "Joanna",
  • isBFF: false
  • },
  • {
  • id: 3,
  • name: "Kim",
  • isBFF: true
  • }
  • ];
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Notice that we are using class attribute interpolation to dynamically generate the index-based class name. And, we're using the ngClass directive to dynamically add or remove the "best-friend" class. And, when we run this, both the approaches work quite nicely together:


 
 
 

 
 Using both class attribute interpolation and the ngClass directive to generate dyanmic class state. 
 
 
 

Now, just to be clear, class attribute interpolation and the ngClass directive have always been able to work well together. Even in AngularJS 1.0.8, the ngClass directive uses the $observe() method to make sure that its classes weren't clobbered by attribute interpolation. The above demo was just to showcase the two approaches working together, not to demonstrate a change in behavior.

Chances are, you didn't even know about this edge-case regarding attribute interpolation. And, if you started using AngularJS after 1.0.8, then it was never even an issue for you. But, if nothing else, it's just nice to see how the underlying framework evolves to deal with a more comprehensive set of use-cases.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.