AngularJS, Filters and $index

Posted by & filed under , .

AngularJS logoI came across an issue with AngularJS in one of the projects I worked on which I thought was worth signalling. It all stems back from the usage of $index inside a ng-repeat group — in the context of using a filter in ng-repeat, and as such it took us a bit to identify the issue.

The crux of the issue is the fact that if you use $index and apply a filter in your ng-repeat, your $index refer to the index in the filtered array — not the initial array. That might seem like a no issue, but here’s a simplified version of the issue I was faced with to exemplify why this can become a problem:

Let’s say I am dealing in my Angular app with a few backend systems and I’m correlating data from these sources on the front end. For the purpose of this example let’s consider that I am writing an app for the company intranet where I am presenting information for the company employees, I am retrieving the names, date of birth and email from a HR database, then I go and fetch the employee photo from another system. (For instance, since a lot of companies are using Google for Business for their emails, I might want to go and fetch the photo from their Google profile.)

Now that means I end up in my JavaScript with 2 arrays:

  • one with the employee details
  • one with the employee images

Let’s say these 2 are linked by the email address. To simplify, I would be dealing with this sort of structures:

Employee:
{
   "name": "Liviu Tudor",
   "dob": "14-Feb-1975",
   "department" : "Engineering",
   "email": "my@email"
}
Profile photo:
{
   "email": "my@email",
   "bigPhoto": "http://some.url/image_800x800.png",
   "smallPhoto": "http://other.url/image_128x128.png"
}

To correlate these 2 arrays I have (at least) 2 options:

  1. I can attach the bigPhoto and smallPhoto to the Employee instances
  2. I can keep them as 2 separate arrays but make sure they are in the same order — such that photos[index] correspond to employee[index] (in other words photos at position index in photos array correspond to employee at the same position in employees array).

First approach would definitely be preferred — however, that presents challenges if I have to save back instances from the employees array: by attaching those properties to an instance of employees array, I’m creating practically 2 new properties (bigPhoto and smallPhoto) on that object which might create problems with the backend system handling employees when I need to save them; as such before sending any Employee instance to the backend for saving I have to strip it back of those properties.

So I decide instead to just keep the arrays synced up in the same order — after all I read them just once on page load, so it’s a small hit to take to sort the photos array in the order needed, just the once.

Now I can set up a table which shows all of my employees and their photos using Angular’s ng-repeat:

<tr ng-repeat="employee in employees">
   <td>{{employee.name}}</td>
   <td>{{employee.dob}}</td>
   <td>{{employee.department}}</td>
   <td>{{employee.email}}</td>
 
   <!-- this is the culprit -->
   <td><a href="{{photos[$index].bigPhoto}}"><img src="{{photos[$index].smallPhoto}}"/></a></td>
</tr>

All good and well until someone comes and say “can I have a dropdown so I can filter the employees by department?”. You think for a second and since you know of Angular’s filtering capabilities you say “Sure, it will only take a minute”.

So you go and implement a dropdown with all the possible values for department and assign the model for that to a scope variable then implement a quick filter function which checks against it. Then simply change your ng-repeat to this:

<tr ng-repeat="employee in employees:filterCheck($employee.department);">

Done, right?

Well, if you decide to go ahead with that like I saw in one of our projects, you will notice soon that the entry for Liv starts showing pictures of Justin Bieber and probably viceversa. And we both know that one of them is more handsome than the other and as such that’s unfair 😉

What gives? Well, the problem as I said is the usage of $index in the context of angular filters!

Let’s say that to start with you have this array of employees:

[
{
   "name": "Liviu Tudor",
   "dob": "14-Feb-1975",
   "department" : "Engineering",
   "email": "my@email"
},
{
   "name": "Donald Duck",
   "dob": "01-Jan-1934",
   "department" : "Sailing",
   "email": "donald@duck"
},
{
   "name": "Liviu Tudor",
   "dob": "01-Jan-1928",
   "department" : "Entertainment",
   "email": "mickey@mouse"
}
]

This corresponds to this array of photos:

[
{
   "email": "my@email",
   "bigPhoto": "http://some.url/image_800x800.png",
   "smallPhoto": "http://other.url/image_128x128.png"
},
{
   "email": "donald@duck",
   "bigPhoto": "http://some.url/donald_800x800.png",
   "smallPhoto": "http://other.url/donald_128x128.png"
},
{
   "email": "mickey@mouse",
   "bigPhoto": "http://some.url/mickey_800x800.png",
   "smallPhoto": "http://other.url/mickey_128x128.png"
}
]

If you are to trace the value of $index during the ng-repeat cycle would be something like this if we were to not use any filtering.

  • $index = 0 –> employee = “Liviu Tudor, photos[$index] = “photos for Liviu Tudor”
  • $index = 1 –> employee = “Donald Duck”, photos[$index] = “photos for Donald Duck”
  • $index = 2 –> employee = “Mickey Mouse”, photos[$index] = “photos for Mickey Mouse”

Now let’s apply a filter and only show the employees in “Sailing”. At this point, because of the filter, the array that ends up being traversed by ng-repeat is this:

[
{
   "email": "donald@duck",
   "bigPhoto": "http://some.url/donald_800x800.png",
   "smallPhoto": "http://other.url/donald_128x128.png"
}
]

And if we are to trace the values of $index during the ng-repeat cycle it would be this:

  • $index = 0 –> employee = “Donald Duck”, photos[$index] = “photos for Liviu Tudor” !!??!!

WTF? It’s obvious what’s happening, the $index is the index in the filtered array, not the original one, as such now we are paying the price for our approach as the indexes are out of order in our ng-repeat directive.

To fix it, we have to find the index of the employee in the original employees array and then use that index in the photos array. Which sounds complicated — but it’s actually as easy as using indexOf() method on the employees array:

<tr ng-repeat="employee in employees | filter:filterCheck($employee.department)">
   <td>{{employee.name}}</td>
   <td>{{employee.dob}}</td>
   <td>{{employee.department}}</td>
   <td>{{employee.email}}</td>
 
   <!-- this is the culprit -->
   <td><a href="{{photos[employees.indexOf(employee)].bigPhoto}}"><img src="{{photos[employees.indexOf(employee)].smallPhoto}}"/></a></td>
</tr>

And voila, order restored! If you trace this down for the above case when we are filtering by department “Sailing”, we get this:

  • $index = 0 –> employee = “Donald Duck”, employees.indexOf(employee) = 1, photos[1] = “photos for Donald Duck”

Phew! 🙂

Moral of the story I guess is that this approach is far from ideal (using 2 parallel arrays I mean) — but if you have to use it and you are throwing in Angular filtering capabilities too, then scrap the $index and opt instead for array.indexOf().