Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andrew Dixon
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andrew Dixon@aandrewdixon )

Understanding "Object Identity" With ngFor Loops In Angular 2 Beta 3

By Ben Nadel on

According to the ngFor documentation, "Angular uses object identity to track insertions and deletions within the iterator and reproduce those changes in the DOM." Call me naive, but I didn't understand what the Angular 2 documentation meant by "object identity." In AngularJS 1.x, I used to use "track by" to track objects based on a given property. As such, I didn't know if "object identity" referred to some special injected property, like the $$hashKey in NG1; or, if it was maybe referring to something like its location in memory. I thought a quick exploration was in order.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To test the meaning of "object identity," I put together a demo with two lists and a "shuffle" link. When the user clicks the shuffle link, both collections are reversed. In the first list, all I'm doing it creating a new collection but maintaining the same item objects. In the second list, I'm creating both a new collection and a new object for each item in the list. In this way, we can see what role object references play in the tracking of an "object identity."

I'm also using the new ngForTrackBy property that was added in Beta 3 to track objects in the second collection (the one that is using all new object references). If you watch the video, you can see how adding and removing the "trackBy" micro-syntax changes the way the DOM (Document Object Model) elements are tracked.

In both cases, each item in the list also gets a logging directive - nodeLogger - that logs the creation and update of the DOM node. This way, we can see if existing DOM elements are destroyed and re-created or simply moved to another location in the DOM tree.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Understanding "Object Identity" With ngFor Loops In Angular 2 Beta 3
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Understanding "Object Identity" With ngFor Loops In Angular 2 Beta 3
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-polyfills.min.js"></script>
  • <!-- CAUTION: Some fatures does no work well with the minified UMD code in Beta 3. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ "AppComponent" ],
  • function run( AppComponent ) {
  •  
  • ng.platform.browser.bootstrap( AppComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root App component.
  • define(
  • "AppComponent",
  • function registerAppComponent() {
  •  
  • var NodeLogger = require( "NodeLogger" );
  •  
  • // Configure the app component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ NodeLogger ],
  •  
  • // In this demo, we have two different collections that are
  • // being rendered using ngFor. Notice that the second one uses
  • // a custom trackBy: [ngForTrackBy] property which uses an
  • // identity function to help determine item equality rather than
  • // using directive object references (like the first ngFor loop
  • // is doing). This is akin to the "track by" syntax in NG1.
  • // --
  • // NOTE: Each LI element also has a [nodeLogger] directive that
  • // will track (via console.log()) when new DOM nodes are created.
  • template:
  • `
  • <p>
  • <a (click)="shuffleCollections()">Shuffle collections</a>.
  • </p>
  •  
  • <h3>
  • Friends
  • </h3>
  •  
  • <ul>
  • <li
  • *ngFor="#friend of friends ; #index = index"
  • nodeLogger="{{ friend.name }} at index {{ index }}.">
  •  
  • {{ friend.name }}
  •  
  • </li>
  • </ul>
  •  
  • <h3>
  • Enemies
  • </h3>
  •  
  • <ul>
  • <li
  • *ngFor="#enemy of enemies ; #index = index ; trackBy:personIdentity"
  • nodeLogger="{{ enemy.name }} at index {{ index }}.">
  •  
  • {{ enemy.name }}
  •  
  • </li>
  • </ul>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // Build the collections.
  • vm.friends = buildCollection( "Fiona", "Fay", "Franny", "Francis", "Fifi" );
  • vm.enemies = buildCollection( "Ella", "Erin", "Eva", "Ester", "Eveline" );
  •  
  • // Expose the public API.
  • vm.personIdentity = personIdentity;
  • vm.shuffleCollections = shuffleCollections;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I provide a custom track-by function that tracks a person based on
  • // the ID, not the object identity (aka object reference).
  • function personIdentity( index, person ) {
  •  
  • console.log( "TrackBy:", person.name, "at index", index );
  •  
  • return( person.id );
  •  
  • }
  •  
  •  
  • // I shuffle (reverse) each of the people collection.
  • function shuffleCollections() {
  •  
  • console.log( "- - - - - - - - - - - - - - - - -" );
  • console.log( "Shuffling both collections." );
  • console.log( "- - - - - - - - - - - - - - - - -" );
  •  
  • // When we reverse the friends collection, notice that we are
  • // returning a new array; however, the actual items within that
  • // array are the same OBJECT REFERENCES that they were in the
  • // previous array - we are only affecting the collection.
  • vm.friends = vm.friends.slice().reverse();
  •  
  • // When we reverse the enemies collection, on the other hand, we
  • // are creating both a NEW ARRAY and creating NEW ITEM references
  • // within that array.
  • vm.enemies = vm.enemies.reverse().map(
  • function operator( enemy ) {
  •  
  • // CAUTION: Creating a new item object.
  • return({
  • id: enemy.id,
  • name: enemy.name
  • });
  •  
  • }
  • );
  •  
  • // Log out the collections so that we can inspect the objects
  • // for any mutations applied by Angular.
  • console.info( "Friends:", vm.friends );
  • console.info( "Enemies:", vm.enemies );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I build a collection of People from the given names. Each person
  • // contains a unique id and a name.
  • function buildCollection( /* ...names */ ) {
  •  
  • var collection = Array.prototype.slice.call( arguments ).map(
  • function operator( name, i ) {
  •  
  • return({
  • id: ( i + 1 ),
  • name: name
  • });
  •  
  • }
  • );
  •  
  • return( collection );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a directive that logs the input message, differentiating between
  • // DOM (Document Object Model) node instantiation and updates.
  • define(
  • "NodeLogger",
  • function registerNodeLogger() {
  •  
  • // Configure the NodeLogger directive definition.
  • ng.core
  • .Directive({
  • selector: "[nodeLogger]",
  • inputs: [ "nodeLogger" ]
  • })
  • .Class({
  • constructor: NodeLoggerController,
  •  
  • // Define the directive life-cycle methods on the prototype
  • // so that they'll be picked up at runtime.
  • ngOnChanges: function noop() {}
  • })
  • ;
  •  
  • return( NodeLoggerController );
  •  
  •  
  • // I control the NodeLogger directive.
  • function NodeLoggerController() {
  •  
  • var vm = this;
  •  
  • // Expose the public methods.
  • vm.ngOnChanges = ngOnChanges;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle input changes.
  • function ngOnChanges( event ) {
  •  
  • // If this is the first change of this value, it means that this
  • // is the initial binding of the input after the directive has
  • // just been instantiated for this DOM node.
  • if ( event.nodeLogger.isFirstChange() ) {
  •  
  • console.log( "Node logger ** instantiated **:", vm.nodeLogger );
  •  
  • } else {
  •  
  • console.log( "Node logger __ updated __:", vm.nodeLogger );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

So, as you can see in the code, the first ngFor is relying on "object identity" and the second ngFor is relying on the custom ngForTrackBy function, personIdentity(). After each shuffle operation, I'm logging out the collections so that we can inspect them to see if Angular 2 has injected any secret properties (such as a $$hashKey).

When we run the code and shuffle the collections a few times, we get the following page output:


 
 
 

 
 Understanding  
 
 
 

As you can see, after a few shuffle operations, the DOM nodes that are used to render the first ngFor loop are only ever moved to a new location in the DOM tree - they are not being destroyed and re-created. As such, I think it's fair to conclude that the term "object identity" refers to "object equality;" or, that which is the same according to the "===" identity operator.

In retrospect, developers who have a better "academic" understanding of the JavaScript language probably thought this was completely obvious. If you understand the language very deeply, "object identity" is probably just a thing you know. But, for me, especially in the context of AngularJS 1.x and the magic $$hashKey values, the meaning of "object identity" was one worth exploring.




Reader Comments

I still don't get it...

My understanding is also that object identity is like the tripple equals operator, but 1x's equals() is exactly working around the fact that two identical objects are NOT the same identity.
As in
{name:'ben'} === {name:'ben'} => false

So angular2 still has do some deep equality checking? or how else do they know it the objects are identical?

Reply to this Comment

@Filip,

Angular 2 is not doing deep-checking. It is only checking refrences. So, as you say, two _instances_ of {name:"Ben"} are NOT the same identity and are therefore not the "same" in an ngFor loop. However, if you need to be able to change the way it is tracking, you can use the "trackBy:" attribute.

If you look in the above demo, the second list uses:

trackBy:personIdentity

... this is a way to tell Angular that the "identity" is not the actual object reference but is, instead, the result of that method. So, while:

{name:"Ben"} !== {name:"Ben"}

.... this IS true:

personIdentity( {name:"Ben"} ) === personIdentity( {name:"Ben"} )

.... because the result of the function is what Angular is using when associating DOM (Document Object Model) nodes with an existing item in the view-model.

Reply to this Comment

@Ben,

Thanks for clearing that out. I totally get it now.
(obviously I hadn't read your code well enough, and saw personIdentity as an object-key like in angular1, which obviously didn't make sense.

Thanks for yet another great post... however, I wish you would use the typescript class syntax - I know its more or less a religious question, but I would prefer using the same style as the angular docs does. Getting to know a framework, the mental-overhead of another syntax is making it a little trickier... either way I appreciate all your writings!

Reply to this Comment

@Filip,

My pleasure sir. And, I do get a lot of push-back against the use of ES5. I'll see if there's anything I can do. I like to keep things simple, which may not be possible with TypeScript.

Reply to this Comment

trackBy tells angular how to determine what elements are the same, when a list is being altered...
So if we have a list like
[{
id:1,
name:'john'
},{
id:2,
name:'paul'
},{
id:3,
name:'john'
}]

we can use the 'id' field to trackby. It has to be unique though, so if two names are the same, you cant use name to trackBy. If you dont specify trackBy the default is a strickt comparison, and since {name:'john'} is stricktly not the same as an identical {name:'john'} object, angular will see them as different

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.