Firebase data normalized. How should I fetch a collection based on this structure?

angularfire, angularjs, angularjs-service, firebase

Solution

Begin by updating to angularFire 0.6. This looks 0.3.*ish. angularFire has been changed to `$firebase` and has a much more powerful and simplified interface.

Vanilla Firebase

I'll do this the hard way first as I think there is great value in understanding the underlying principle here. It's fairly complex, and I'll only cover the essentials. There are a lot of tiny edge cases to be handled as well:

angular.module('app', [])
    .controller('UsersController', function($scope, $firebase, $timeout, $routeParams){
      var userId = $routeParams.userId;
      $scope.user = $firebase(new Firebase('URL/user/'+userId));

      // or, for 3-way binding and automatic writes back to Firebase
      var userRef = $firebase(new Firebase('URL/users/'+userId)).$bind($scope. 'user');

      // grab this users' books using Firebase (the hard way)
      $scope.books = {};
      var booksRef = new Firebase('URL/books/');

      // fetch the user's book list dynamically because it may change in real-time
      var indexRef = new Firebase('URL/user/'+userId+'/books');

      // watch the index for add events
      indexRef.on('child_added', function(indexSnap) {
         // fetch the book and put it into our list
         var bookId = indexSnap.name();
         booksRef.child(bookId).on('value', function(bookSnap) {
            // trigger $digest/$apply so Angular syncs the DOM
            $timeout(function() {
               if( snap.val() === null ) {
                  // the book was deleted
                  delete $scope.books[bookId];
               }
               else {
                  $scope.books[bookId] = snap.val();
               }
            });
         });
      });

      // watch the index for remove events
      indexRef.on('child_removed', function(snap) {
         // trigger $digest/$apply so Angular updates the DOM
         $timeout(function(snap) {
            delete $scope.books[snap.name()];
         });
      });
});

Then the HTML (this will be the same for the other examples below):

<div data-ng-repeat="(bookId, book) in books">
   {{bookId}}: {{book.title}}
</div>

Some of the edge cases not fully covered here:

- data is not sorted by priority ordering

- when a record is deleted from index, should call off() on data paths

- changes in ordering of the index won't change order of data records

- value of the index is not stored anywhere for reference (if it matters)

FirebaseIndex

FirebaseIndex is a simple utility that takes an index like your book list and manages the code we just created above in a bit more sophisticated manner.

Unfortunately, FirebaseIndex doesn't support `value` events, so it can't be used with angularFire after 0.5.0 because of a change to angularFire's internal loading mechanisms. So it's not quite as short and sweet as it used to be.

angular.module('app', [])
.controller('UsersController', function($scope, $firebase, $timeout){
   var userId = $routeParams.userId;
   $scope.user = $firebase(new Firebase('URL/user/'+userId));

   var fb = new Firebase(URL);
   var index = new FirebaseIndex( fb.child('user/'+userId+'/books') );
   $scope.books = {};

   // almost magic
   index.on('child_added', function(snap) {
      $timeout(function() { $scope.books[snap.name()] = snap.val(); });
   });

   index.on('child_removed', function(snap) {
      $timeout(function() { delete $scope.books[snap.name()]; });
   });
});

Firebase.util.join

Firebase-util is a much more powerful and sophisticated library for normalizing paths. Because it returns an object that works just like a regular Firebase reference, it can also be used seamlessly with angularFire 0.5 and above.

angular.module('app', [])
.controller('UsersController', function($scope, $firebase){
   var userId = $routeParams.userId;
   $scope.user = $firebase(new Firebase('URL/user/'+userId)); 

   var fb = new Firebase(URL);
   var ref = new Firebase.util.intersection( fb.child('user/'+userId+'/books'), fb.child('books') );

   // magic!
   $scope.books = $firebase(ref);
});

Problem

I think I am getting close, I am able to print out the ID of books belonging to a user but have been trying unsuccessfully to fetch the list of books belonging to a user, from the firebase books reference. I'm following loosely the tutorial here: http://www.thinkster.io/pick/eHPCs7s87O/angularjs-tutorial-learn-to-rapidly-build-real-time-web-apps-with-firebase#item-526e9330d90f99661f00046c and also reading the documentation about denormalizing data here: https://www.firebase.com/blog/2013-04-12-denormalizing-is-normal.html How should I go about it if I want to display the user in a page, followed by all its books? firebase structure ``` FB | --user | | | --user1 | | | --name: "test name" | --email: "test@test.com" | --books | | | "-JFZG3coHOAblHZ7XSjK": true | "-KJKJASDIUOPIWE9WEeJ": true | "-YtUTRGJLNL876F3SSwS": true | --books | --"-JFZG3coHOAblHZ7XSjK" | | | --title: "book title 1" | --ownerId: "user1" | --"-KJKJASDIUOPIWE9WEeJ" | | | --title: "book title 2" | --ownerId: "user1" | --"-YtUTRGJLNL876F3SSwS" | | | --title: "book title 2" | --ownerId: "user1" ``` View ``` <div data-ng-controller="UsersController" data-ng-init="findOneUser()"> <h2>Profile</h2> <img class="image_preview" data-ng-src="{{user.photoUrl}}"> <p>Name: {{ user.name }}</p> <p>Name: {{ user.email }}</p> <a data-ng-href="#/users/{{ userId }}/edit">Edit</a> <h2>Coffee Blends</h2> <div data-ng-repeat="book in user.books"> <p>---</p> <p>{{user.books}}</p> </div> <!--<div data-ng-controller="BooksController" data-init="">--> <!--</div>--> </div> ``` Controller ``` 'use strict'; angular.module('ccApp.controllers.users', ['ccApp.services.users']) .controller('UsersController', ['$scope', '$routeParams', '$location', 'angularFire', 'Users', function($scope, $routeParams, $location, angularFire, Users){ $scope.user = {}; $scope.userId = $routeParams.userId; $scope.findOneUser = function(userId){ if (!!$scope.userId){ angularFire(Users.find($routeParams.userId), $scope, 'user'); } }; $scope.updatePhotoUrl = function(url, user){ $scope.fileUrl = url; console.log($scope.fileUrl[0].url); user.photoUrl = $scope.fileUrl[0].url; }; $scope.findUsers = function(){ $scope.users = Users.collection(); }; $scope.findWholesalers = function(){ $scope.wholesalers = Users.collection(); }; }]); ``` Service ``` 'use strict'; angular.module('ccApp.services.users', ['ccApp.services.firebaseRefs']) .factory('Users', ['angularFireCollection', 'FireRef', function(angularFireCollection, FireRef){ return{ collection: function(cb){ return angularFireCollection(FireRef.users(), cb); } , find: function(userId){ return FireRef.users().child('/'+userId); } }; }]); ```

Original source