The title of this post may be a little misleading, as this post is really more about finding a way out of being stuck in asynchronous code purgatory. But the title is inspired by Google’s omniscient autocomplete search suggestion.
While picking up some legacy code as a part of a hackathon, I was having trouble getting ng-repeat
to display anything helpful on the view. While searching for answers, Google kept suggesting the rather vague search phrase “ng-repeat doesn’t work”. In this post, I am going to explore why ng-repeat
did not work in my particular case with using Firebase.
Refactoring the View
The reason for using ng-repeat
was to reduce the redundancy in the view. There was a giant <table>
for layout (!) with each section iterating through repeated properties of the model. This lead to a very large view with about 330 lines of hard-coded html that could be significantly reduced by about 90% by using ng-repeat
to DRY it up.
However, when I applied ng-repeat
, I could not get any of the properties of the model to display. For example this would produce nothing:
1 2 3 4 5 6 |
<tr ng-repeat="item in object"> {{ item.property }} </tr> // Blank. Empty. Nada. Do not pass Go. Etc. |
But this would show the entire Firebase object:
1 2 3 4 5 6 |
<tr ng-repeat="item in object"> {{ object }} </tr> // Deluge of object properties |
My First Clue
Angular was also throwing an error about not being able to call $apply
before a previous invocation of $apply
had resolved.
I took a look in the controller to see how we were getting the data, where I found multiple nuggets like this every time Firebase is queried or updated:
1 2 3 4 5 6 7 8 9 |
var fb = new Firebase('...Firebase URL...'); fb.on("value", function(snapshot) { $scope.$apply(function(){ $scope.object = snapshot.val(); }); }); |
I realized that with multiple $scope.$apply
references tied to fb.on("value", ...)
all in the same controller, we were likely experiencing an asychronous issue. The second $scope.$apply
was getting fired before the first one resolved with data returned from Firebase.
Simplify
I first commented out all those Firebase calls to just one call for the model I was trying to use with ng-repeat
. Unfortunately, still no luck. I could load the main model, but no properties on the model. In fact, nothing was repeating at all. I actually had the ng-repeat
on a
The thought occurred to me that this may still be an asynchronous issue. Maybe the html and ng-repeat
was being loaded before Firebase came back with any data. If ng-repeat
sees an empty object when it first loads, it won’t repeat any elements. Though I would assume that the view would update itself once the model updated because of Angular automagical data binding, asynchronous code can cause odd behavior.
AngularFire
First thing: let’s get rid of those $scope.$apply
methods. There happens to be a lovely library called AngularFire that will sync data between your model and a Firebase instance.
After installing AngularFire with Bower and including the <script>
, syncing the model and Firebase became simple and paved my path out of asynchronous code purgatory:
1 2 3 4 5 6 7 8 9 10 |
var ref = new Firebase('/*... Firebase URL ...*/'); var sync = $firebase(ref); // sync as an array for ng-repeat $scope.object = sync.$asArray(); // or as object $scope.object = sync.$asObject(); |
Success! Oh, wait …
Cool, now my ng-repeat
is actually showing and repeating the properties of the model like I would expect…
… except that something is still not right. Some things repeat and some things don’t. After some frustrating experimentation, I found that this wasn’t working:
1 2 3 4 5 6 7 |
<tr ng-repeat="item in object"> {{ item.property }} </tr> // Tables bad! // :( |
But this was working:
1 2 3 4 5 6 7 |
<li ng-repeat="item in object"> {{ item.property }} </li> // Lists good! // :) |
After some digging, I found that there seemed to be various reports of issues with using ng-repeat
on a <tr>
, like this one: https://github.com/angular/angular.js/issues/1459.
Okay, so that issue is two years old, and probably not applicable to my situation … but that still makes a lovely excuse to change all those <table>
s to <div>
s anyway! Bootstrap was already present, so it was easy enough to apply a grid.
After that I was able to compress the ~330 lines of html in the view to little more than the following by using a nested ng-repeat
(some classes/styles and other stuff removed for clarity):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div ng-repeat="item in object" class="col-sm-4"> <h4>{{ item.sectionNumber }}</h4> <div ng-style="section"> <div ng-repeat="property in item" class="col-sm-6 {{ property.propertyNumber }}"> <button type="button" ng-click="update(item.sectionNumber, $event)"> <img ng-show="item.name" ng-src="{% item.img %}&s=110"> </button> <span class="label">{% item.name %}</span> </div> </div> </div> |