Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

JavaScript Arrays, References and Databinding in Angular


:P
On this page:

I ran into a little snag in a small demo application I'm building last night. I've been using Angular and as part of the app I'm adding an object from the $scope to an array that also lives on the $scope instance. Seems straight forward enough, but as it turns out there was a snag with the last addition of the object to the array basically resetting all items in the array to the same value as the last one added.

For some context I have an Angular $scope that looks like this:

$scope.locationData = {        
    Name: "",
    Address: "",
    Usage: "",
    Description: "",
    Longitude: 0,
    Latitude: 0
};    
$scope.locationHistory = [];

The locationData object is bound via Angular bindings to a few textbox controls that are updated from the UI. Then the user clicks a Save Location button and it should add the new locationData item into the locationHistory:

this.saveLocationData = function() {
    $scope.locationData.Entered = new Date();
    $scope.locationData.Updated = new Date();               
                
    $scope.locationHistory.splice(0, 0,$scope.locationData);
    localStorage.setItem("locationScope", JSON.stringify($scope.locationHistory));

    self.showPage();         

};

The logic works fine on the first pass. A new locationData item is added to the locationHistory array.

However, on the second (and any subsequent) pass, the locationHistory array is changed completely - every item in the array is updated with the last item added. In other word on the second pass the first and second item are exactly the same.

Even weirder I can do something like this in the Console window:

bc.$scope.locationData.Name = "New Name"

and it will change the name property in all the locationData objects in the array

Hold on a Sec!

When I first ran this, it took me quite a while to track this down. I was looking for a logic error, thinking I was adding the wrong item to the array. So off I went debugging with a bunch of console.log() statements echoing out the items that were added - and oddly enough it all looked right. I was adding the right items and console.log() seemed to show the right items being passed and added. Not until I looked more closely at the array, did I realize that all items in the array were always the same as the last item added.

My first reaction - of course was: WTF is Angular doing here mainly because there were actually only some items in the array changing. Only the ones added in the current session changed. Since I'm storing the list data to localStorage, some items are 'persistent' and some weren't. It took a minute to realize this however, especially if I only added 2 items - it would just seem that I added the same item twice (or the last item was overwritten with the current one). It wasn't until I added a whole bunch of entries that I realized that this must be something else: A reference problem.

After some more experimenting it turns out this isn't an Angular problem at all, but really a JavaScript reference issue. What's happening here is that there's only a single instance of a locationData object. And even though the values of that object are changed the reference of that single object is all that Angular uses for data binding. When I change a textbox value Angular updates that objects matching bound value in that single instance.

When I add that object to the array, I am in effect always adding the same object - by reference. A pointer is passed and added. So all the objects are pointers to the same instance! Change one, change all!

Using data binding sort of obfuscates that simple fact and it's easy to miss. Similar issues will creep up with other binding frameworks like Knockout.js and Ember.js etc. It's a simple problem to solve, once you know what the problem is.

Watch those References with Arrays

To demonstrate the issue more simply here's a small example completely outside of Angular (jsFiddler here):

    var arr = [];
    var obj = { name: "rick" };

    arr.push(obj);

    obj.name = "markus";
    arr.push(obj);

    for (var i = 0; i < 2; i++) {
        // both entries print 'markus'
        console.log(arr[i].name);
    }

When you run this you'll find that both objects print "markus". Both objects point to the same instance.

In this code the problem is much more obvious though because you can actually see that the same object being referenced. But in effect, Angular's data binding is doing exactly the same thing: It's updating a single instance with values that are changed in the UI. There's only one locationData object instance.

I threw this out on Twitter and got a bunch of responses back.

De-referencing a JavaScript Object

The easiest solution for me was to use an Angular helper function: angular.copy() which is a deep object copy function (ie. it copies all properties down the hierarchy). It basically copies an object and creates a new instance, which effectively de-references the original object. Now when I add the copied locationData I have a new instance that's being written out.

Here's what the splice operation that adds the locationData to the array looks like:

$scope.locationHistory.splice(0, 0, angular.copy($scope.locationData));

This is nice and simple and because of the deep copy should work reliably with most objects and arrays.

If you're not using Angular, you can also use jQuery's $.extend() method to do the same thing. You can do both shallow and deep copies with:

// Shallow copy
var n1 = $.extend({}, old);

// Deep copy
var n2 = $.extend(true, {}, old);

Extend adds properties of the second object to the first and if you pass an empty object in it effectively creates a copied object.

Resetting the Reference

Another approach to the de-referencing issue is to re-create a new instance of the object after it's been assigned to the array. Rather than leaving the single instance of this object instance around and live, deleting the original reference by creating a new reference effectively de-references the object as well. So I can simply set the object to null or {} to de-reference.

When using Angular this isn't a good idea, especially if items are bound. So in this case I'd have to recreate an empty object and rebind it. To do this a factory method for the base object is in order:

 

$scope.createLocationData = function() {
    return {        
        Name: "",
        Address: "",
        Usage: "",
        Description: "",
        Longitude: 0,
        Latitude: 0
    };    
};

$scope.locationData = $scope.createLocationData();
$scope.locationHistory = [];

The same function can then be used when saving to recreate the object:

$scope.locationHistory.splice(0, 0, $scope.locationData);
$scope.locationData = $scope.createLocationData();
//$scope.$apply();

This is an easy solution to this problem and it doesn't require any special libraries. Simply recreating the object is efficient and it also provides a nice way to reset the object after it's been saved to show an empty form if no location data is actually loaded.

JavaScript 101

Yes I realize that reference objects is a pretty basic JavaScript 101 issue, but it's one that's becoming more common in light of the various databinding frameworks. The issue totally makes sense, but when I looked at it originally I was scratching my head for a while, trying to understand why it seemed like my data was being changed by the mere act of databinding. It turns out it's nothing more than simple JavaScript logistics that has a few simple workarounds. I hope some of you might find this useful when running into a similar issue.

Thanks to Ben Maddox and Nick Berardi for their help on Twitter.

Posted in JavaScript  Angular  


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024