Most of the time, each ngRepeat directive in AngularJS renders a single, homogeneous set of data. But sometimes, the user interface (UI) of your application requires a list to contain several commingled sets of data. In such cases, not only do the individual subsets of data contain potentially-overlapping unique identifiers, they are also likely to require different rendering templates. The ngRepeat directive can handle this, as long as you take a little extra care in preparing your View Model (VM).
When you need to render several commingled sets of data using a single ngRepeat directive in AngularJS, you have to worry about two things:
- Making sure each item can be tracked by a unique identifier.
- Making sure each item has a "type" indicator for differential rendering.
Technically, you can probably get away without the first consideration (a set-wide unique identifier); but, I would recommend this approach - it's good to be able to use the "track by" expression as it will cut down on unnecessary DOM (Document Object Model) node destruction and re-creation, which is a nice performance boost.
The second consideration - a type indicator - can be used inside the ngRepeat template to define individual sub-templates for each data type. While AngularJS 1.2 doesn't like using two different types of transclusion on the same element, there are some exceptions. Luckily, the ngRepeat-ngSwitch combination is one of those exceptions and we can use ngSwitch to handle the template rendering.
To see this in action, I've created a demo that lists out the "people" on your team. This list is composed of both real users and pending invitations (to the team). You will see that when I create the data set used in the ngRepeat directive, I am taking care to create both a unique identifier - uid - as well as a type differentiator - isInvitation.
My biggest concern with this approach is that I believe that the sub-templates keep getting re-compiled every time the ngRepeat element is cloned. While I am not exactly sure how AngularJS is working under the hood, I do know that the ngSwitchWhen directive uses "transclude:element"; this means that the HTML is pulled out of the DOM and the directive is provided with a linking function that can clone the extracted DOM element(s).
According to the AngularJS documentation:
transclude: Extract the contents of the element where the directive appears and make it available to the directive. The contents are compiled and provided to the directive as a transclusion function.
From the above documentation, it sounds like the ngSwitchWhen directives are going to be recompiled every time the ngRepeat element is cloned, injected into the DOM, and then linked to the child scope. This is less than ideal. What we'd really like is for the ngSwitchWhen to be compiled once, along with the ngRepeat directive, and then simply cloned and linked with each ngRepeat element. Unfortunately, that kind of a solution will take a little bit more "noodling" on my part.
Now, you could forego the compiling and the transclusion by changing the ngSwitchWhen directives to simply be ngHide/ngShow. But, in that case, you're trading in one performance problem (repeat compiling) for another performance problem (a higher number of data bindings that add overhead to each each digest). Plus, I don't love the idea of having hidden data bindings that I know are causing suppressed exceptions.
While there might be some aspects of this approach that are sub-optimal, I have to say that it has been quite successful for me. Personally, I'm not crazy about the idea of having a user interface that commingles different sets of data in a single list. But, if that's what the product team wants, well then, that's what they get.
Want to use code from this post? Check out the license.
I took a stab at refactoring this example to use a custom directive that precompiles the sub-templates instead of using the standard ngSwitch and ngSwitchWhen directives:
Granted, the tests are simplistic (me looking at Chrome Dev tools), but the results seem to be pretty solid - 30-40% increase in performance and 40% less memory usage (according to the Snapshot tool in the Profile).
Its actually kinda weird that your experiment worked, given the following bug that was fixed in 1.3:
Can you explain how you got ng-switch/ng-repeat combination working?
I had to read that example a couple of times to figure out what was going on. It looks like that user (in Stack Overflow) was using "Switch" to figure out which "ngRepeat" to run. I am doing the opposite - I am using the ngSwitch *inside* the ngRepeat.
So, basically, I am doing [ ngRepeat --> ngSwitch ] ... and that use was doing [ ngSwitch --> ngRepeat ].
Looking at the AngularJS source code, for AngularJS 1.3, it looks like that priority of ngSwitch is still 0 (zero), but ngSwitchWhen was raised to 1200. So, we now have (in 1.3), the following priorities:
For my example, this has no bearing since the ngSwitchWhen doesn't actually compete with any other directive for priority. However, for the user on Stack Overflow, this will make a difference as the ngSwitchWhen will link before the ngRepeat, which why they will be in good shape.
Thanks for pointing this out - I didn't realize this change was made to the source code. Very cool!
Thanks again for pointing out the change in ngSwitchWhen behavior. I added a follow-up post for this AngularJS 1.3 update:
Totally cool, Ill give it a read. Thanks