Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Kev McCabe
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Kev McCabe@bigmadkev )

Mixing Data And Templates Using A Single ngRepeat In AngularJS

By Ben Nadel on

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).


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

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.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Mixing Data And Templates Using A Single ng-repeat In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • .invitation {
  • color: #AAAAAA ;
  • font-style: italic ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Mixing Data And Templates Using A Single ng-repeat In AngularJS
  • </h1>
  •  
  • <h2>
  • Your Team &mdash;
  • {{ users.length }} users and
  • {{ invitations.length }} outstanding invitations.
  • </h2>
  •  
  • <ul>
  • <!--
  • When we use the ngRepeat directive, notice that we are tracking the DOM/object
  • relationship using the UID that was derived during the list merge. This allows
  • us to make sure we don't destroy and re-build DOM elements when we start to
  • introduce Cached vs. Live data.
  •  
  • Then, for each item, we are "switching" the rendering template based on the
  • "isInvitation" flag that was injected into each item.
  • -->
  • <li
  • ng-repeat="person in people track by person.uid"
  • ng-switch="person.isInvitation">
  •  
  • <!-- BEGIN: Template for Invitation. -->
  • <div ng-switch-when="true" class="invitation">
  •  
  • {{ person.email }}
  • ( <a ng-click>Resend</a> or <a ng-click>Cancel</a> invitation )
  •  
  • </div>
  • <!-- END: Template for Invitation. -->
  •  
  • <!-- BEGIN: Template for User. -->
  • <div ng-switch-when="false" class="user">
  •  
  • {{ person.name }} &mdash; {{ person.email }}
  •  
  • </div>
  • <!-- END: Template for User. -->
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I hold the collection of pending invitations to people who will one
  • // day become users.
  • // --
  • // NOTE: This data is persisted in a completely different data-table
  • // (from users) on the back-end and has its own unique set of IDs.
  • $scope.invitations = [
  • {
  • id: 1,
  • email: "kim@bennadel.com"
  • },
  • {
  • id: 2,
  • email: "sarah@bennadel.com"
  • },
  • {
  • id: 3,
  • email: "tina@bennadel.com"
  • }
  • ];
  •  
  • // I hold the collection of active users.
  • // --
  • // NOTE: This data is persisted in a completely different data-table
  • // (from invitations) on the back-end and has its own unique set of IDs.
  • $scope.users = [
  • {
  • id: 1,
  • name: "Anna",
  • email: "anna@bennadel.com"
  • },
  • {
  • id: 2,
  • name: "Kit",
  • email: "kit@bennadel.com"
  • },
  • {
  • id: 3,
  • name: "Tricia",
  • email: "tricia@bennadel.com"
  • },
  • {
  • id: 4,
  • name: "Yuu",
  • email: "yuu@bennadel.com"
  • }
  • ];
  •  
  • // I hold the co-mingled collection of active users and pending
  • // invitations. Since this list is the aggregate of two different and
  • // unique sets of data, this collection has its own unique identifier
  • // - uid - injected into each item.
  • $scope.people = buildPeople( $scope.invitations, $scope.users );
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I merge the given invitations and users collections into a single
  • // collection with unique ID (generated from the collection type and
  • // the ids of each item).
  • function buildPeople( invitations, users ) {
  •  
  • var people = sortPeople( invitations.concat( users ) );
  •  
  • for ( var i = 0 ; i < people.length ; i++ ) {
  •  
  • var person = people[ i ];
  •  
  • // I determine if the given item is an invitation or a user.
  • person.isInvitation = ! person.hasOwnProperty( "name" );
  •  
  • // I build the unique ID for the item in the merged collection.
  • person.uid = ( ( person.isInvitation ? "invitation-" : "user-" ) + person.id );
  •  
  • }
  •  
  • return( people );
  •  
  • }
  •  
  •  
  • // I sort the collection based on either name or email. Since I am
  • // sorting a mixed-collection, I am expecting not all elements to have
  • // a name; BUT, I am expecting all elements to have an email.
  • function sortPeople( people ) {
  •  
  • people.sort(
  • function comparisonOperator( a, b ) {
  •  
  • var nameA = ( a.name || a.email ).toLowerCase();
  • var nameB = ( b.name || b.email ).toLowerCase();
  •  
  • if ( nameA < nameB ) {
  •  
  • return( -1 );
  •  
  • } else if ( nameA > nameB ) {
  •  
  • return( 1 );
  •  
  • } else {
  •  
  • return( 0 );
  •  
  • }
  •  
  • }
  • );
  •  
  • return( people );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

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.




Reader Comments

@All,

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:

http://www.bennadel.com/blog/2702-creating-an-optimized-switch-directive-for-use-with-ngrepeat-in-angularjs.htm

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).

Reply to this Comment

@Boris,

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:

ngRepeat: 1,000
ngSwitch: 0
ngSwitchWhen: 1,200

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!

Reply to this Comment

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.