Rendering DOM Elements With ngRepeat In AngularJS

Posted January 16, 2013 at 9:44 AM by Ben Nadel

Tags: Javascript / DHTML

When it comes to compiling and rendering DOM elements, AngularJS is definitely a little bit magical. Things just work! And, they work really well. Especially when it comes to syncing the DOM with the view model (ie. $scope) shared by the various Controllers. At first, they mysterious nature of the syncing is delicious; but, when your application starts to use things like caching, localStorage, directives, and heavy client-side calculations, understanding how AngularJS renders DOM elements in an ngRepeat can be essential to providing a fast, smooth user experience.


 
 
 

 
  
 
 
 

The ngRepeat directive provides a way to render a collection of items given a template. To do this, AngularJS compiles the given template and then clones it for each unique item in the collection. As the collection is mutated by the Controller, AngularJS adds, removes, and updates the relevant DOM elements as needed.

But, how does AngularJS know which actions to perform when? If you start to test the rendering, you'll discover that AngularJS doesn't brute force DOM creation; that is, it doesn't recreate the DOM for every rendering. Instead, it only creates a new DOM element when a completely new item has been introduced to the collection. If an existing item has been updated, AngularJS merely updates the relevant DOM properties rather than creating a new DOM node.

It does this by injecting expando properties into the DOM element as well as into the individual items of your collection. At the time of this writing, the expando property injected into your collection items is called "$$hashKey" (though all $$ variables are subject to change). During the execution of the ngRepeat, AngularJS then maps the $$hashKey onto the expando property injected into the DOM; if the two line up, AngularJS does not create a new DOM node.

NOTE: jQuery also uses expando properties. This is a tried and true approach to linking.

To see this in action, take a look at the following demo. We're going to render a collection of items using the ngRepeat directive. One item in the collection will be passed around by-reference. The other item in the collection will be passed around by-value. You'll see that the by-value item precipitates new DOM element creation because the $$hashKey fails to persist across $digest cycles.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="DemoController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>Rendering DOM Elements With ngRepeat In AngularJS</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Rendering DOM Elements With ngRepeat In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="rebuild()">Rebuild Friends</a>
  • </p>
  •  
  • <div
  • ng-repeat="friend in friends"
  • bn-log-dom-creation>
  •  
  • {{ friend.id }}. {{ friend.name }}
  •  
  • </div>
  •  
  •  
  • <!-- Load AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var Demo = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the root-level controller for the application.
  • Demo.controller(
  • "DemoController",
  • function( $scope ) {
  •  
  •  
  • // I create and return an array of the friends. One
  • // of the friends is passed by reference; one of the
  • // friends is passed by value.
  • function build() {
  •  
  • var friends = [
  • tricia,
  • angular.copy( joanna ) // NOTE: By-value.
  • ];
  •  
  • return( friends );
  •  
  • }
  •  
  •  
  • // I re-assemble the collection of friends.
  • $scope.rebuild = function() {
  •  
  • $scope.friends = build();
  •  
  • };
  •  
  •  
  • // ---
  •  
  •  
  • var tricia = {
  • id: 1,
  • name: "Tricia"
  • };
  •  
  • var joanna = {
  • id: 2,
  • name: "Joanna"
  • };
  •  
  • $scope.friends = build();
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I log the invocation of the LINK function. Since the Link
  • // function is invoked whenever its contextual DOM element is
  • // created, this actually logs DOM creation.
  • Demo.directive(
  • "bnLogDomCreation",
  • function() {
  •  
  •  
  • // I link the DOM element to the view model.
  • function link( $scope, element, attributes ) {
  •  
  • console.log(
  • "Link Executed:",
  • $scope.friend.name,
  • $scope.friend
  • );
  •  
  • }
  •  
  •  
  • // Return directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the template for the ngRepeat directive also has a custom directive, bnLogDomCreation. This directive simply logs a message to the console whenever it is linked. And, since it is linked whenever a new DOM element (in the ngRepeat) is created, it conveniently serves to log DOM creation events.

When we ask the Controller to rebuild the friend collection, you can see that one item is passed by reference (tricia); and, one item is passed by value (joanna). Since the second item fails to persist the $$hashKey across $digest cycles, AngularJS creates a new DOM node for it... again... and again... and again.

Multiple requests to the rebuild() method result in the following console output:


 
 
 

 
 AngularJS and rendering DOM elements inside an ngRepeat directive. 
 
 
 

As you can see, AngularJS recreates the "joanna" DOM node and re-links the bnLogDomCreation directive for every update to the collection.

Now that you know this, you can work with AngularJS to make rendering faster and more efficient [in use-cases where the return-on-investment seems valuable]. For example, if you wanted to replace a cached collection item with a live collection item, you may choose to simply copy the non-AngularJS properties rather than completely overwriting the object:

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="DemoController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>Rendering DOM Elements With ngRepeat In AngularJS</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Rendering DOM Elements With ngRepeat In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="rebuild()">Rebuild Friends</a>
  • </p>
  •  
  • <div
  • ng-repeat="friend in friends"
  • bn-log-dom-creation>
  •  
  • {{ friend.id }}. {{ friend.name }}
  •  
  • </div>
  •  
  •  
  • <!-- Load AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var Demo = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the root-level controller for the application.
  • Demo.controller(
  • "DemoController",
  • function( $scope ) {
  •  
  •  
  • // I create and return an array of the friends. One
  • // of the friends is passed by reference; one of the
  • // friends is passed by value.
  • function build() {
  •  
  • var friends = [
  • tricia,
  • angular.copy( joanna ) // NOTE: By-value.
  • ];
  •  
  • return( friends );
  •  
  • }
  •  
  •  
  • // I re-assemble the collection of friends.
  • $scope.rebuild = function() {
  •  
  • // Update the ID so we can see *some* change.
  • joanna.id++;
  •  
  • // Update-Copy the joanna friend back into the
  • // collection.
  • update( $scope.friends[ 1 ], joanna );
  •  
  • };
  •  
  •  
  • // I update the values in the destination object
  • // using the values in the source object, but I
  • // ignore any proprietary keys used by AngularJS.
  • function update( destination, source ) {
  •  
  • var angularJSKeyPattern = /^\$\$/i;
  •  
  • for ( var name in source ) {
  •  
  • if (
  • source.hasOwnProperty( name ) &&
  • destination.hasOwnProperty( name ) &&
  • ! angularJSKeyPattern.test( name )
  • ) {
  •  
  • destination[ name ] = source[ name ];
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // ---
  •  
  •  
  • var tricia = {
  • id: 1,
  • name: "Tricia"
  • };
  •  
  • var joanna = {
  • id: 2,
  • name: "Joanna"
  • };
  •  
  • $scope.friends = build();
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I log the invocation of the LINK function. Since the Link
  • // function is invoked whenever its contextual DOM element is
  • // created, this actually logs DOM creation.
  • Demo.directive(
  • "bnLogDomCreation",
  • function() {
  •  
  •  
  • // I link the DOM element to the view model.
  • function link( $scope, element, attributes ) {
  •  
  • console.log(
  • "Link Executed:",
  • $scope.friend.name,
  • $scope.friend
  • );
  •  
  • }
  •  
  •  
  • // Return directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

In this example, the "joanna" item is copied originally by-value, just as in the first example. However, this time, rather than completely overwriting the "joanna" object on rebuild(), we copy the "Safe" keys from the core "joanna" object into the collection. This allows AngularJS to update the existing DOM elements without creating any new ones.

This is a lot of hoops to jump through and will definitely make your code more complex. As such, I would never advocate thinking this way until you actually need to. When you first get into AngularJS, just let it work its beautiful magic. Only worry about rendering when performance actually becomes an issue.




Reader Comments

Jan 16, 2013 at 3:00 PM // reply »
3 Comments

Ben, I'm really enjoying your Angular posts; I appreciate your insights. I've just started using Angular in production and am loving it. Keep 'em coming!


Jan 16, 2013 at 3:08 PM // reply »
11,238 Comments

@Matt,

Awesome my man! Thanks! I'll try to keep it going. If there's anything in AngularJS that you'd like me to explore, just let me know.


Jan 16, 2013 at 3:17 PM // reply »
63 Comments

Re: "If there's anything in AngularJS that you'd like me to explore, just let me know."

How about pushstate :=)


Jan 24, 2013 at 12:35 AM // reply »
11,238 Comments

@Edward,

Ha ha, one of these days.


Apr 8, 2013 at 1:55 AM // reply »
9 Comments

extremely useful.


Apr 18, 2013 at 9:53 AM // reply »
11,238 Comments

@Aladdin,

Thanks! Glad you found this helpful!


May 7, 2013 at 10:02 AM // reply »
11,238 Comments

@All,

I re-examined this scenario (overwriting locally-cached data with live data) from a slightly different point of view:

http://www.bennadel.com/blog/2472-HashKeyCopier-An-AngularJS-Utility-Class-For-Merging-Cached-And-Live-Data.htm

This time, instead of copying over ALL the data, I'm simply copying over the $$hashKey expando property from the local data structures into the live data structures.

I created a utility class, HashKeyCopier, that will perform this operation in painless way.


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 21, 2013 at 7:46 PM
Using Plupload For Drag & Drop File Uploads In ColdFusion
No luck. At least I have uncovered the cause, URLScan 3.1. Here is what I see in the IIS log when a file is over 30mb. 2013-05-21 23:29:05 10.105.45.128 GET /plupload/assets/jquery/jquery-1.8. ... read »
May 21, 2013 at 6:12 PM
Using Plupload For Drag & Drop File Uploads In ColdFusion
Ben, I did not see you after Pete Freitag's Lockdown session at cfObjective but he said that IIS sets file size limits at 30MB by default which just happened to be the threshold for file size when ... read »
May 21, 2013 at 11:51 AM
Ask Ben: Parsing Very Large XML Documents In ColdFusion
Looking at my first ever XML document that I have to parse and put into MS SQL 2000 with CF8. I get it to list the desired Field name, many times over, and have a long list of this field name displa ... read »
May 21, 2013 at 9:25 AM
Turning Off and On Identity Column in SQL Server
you are awesome..i am lucky to get this blog between such a garbage one....Thanks, Prashant ... read »
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
May 20, 2013 at 4:29 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, That's if you're trying to reference a specific row. In this case, we're trying to reference the entire query column as one cohesive value. So, you are correct that if you wanted to output a ... read »
May 20, 2013 at 4:24 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I thought when you used array notation to reference queries you always had to have the row or it would throw a similar error as well? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools