Firebase
Build a Real-Time Slack Clone using AngularFire

Storing User Profiles in Firebase

PRO
Outline

Now that we're able to authenticate users, let's create the ability for users to have custom display names to use in our app (rather than showing the user's email or uid)

Create a factory Users in app/users/users.service.js. This factory will depend on $firebaseArray and $firebaseObject.
angular.module('angularfireSlackApp')
  .factory('Users', function($firebaseArray, $firebaseObject){

    var Users = {};

    return Users;
  });

The purpose of this factory is to provide us with the ability to get either a specific user's data, or to get a list of all of our users. Note that while Firebase provides us with a means of authentication, all of the authentication data is separate from our Firebase data and can't be queried. It is up to us to store any custom user data within Firebase manually.

Create a reference to the users node
angular.module('angularfireSlackApp')
  .factory('Users', function($firebaseArray, $firebaseObject){
    var usersRef = firebase.database().ref('users');

    var Users = {};

    return Users;
  });

Data in Firebase is stored in a tree structure and child nodes can be referenced by passing a string to the ref() function, so firebase.database().ref('users') refers to the users node.

Read the Firebase documentation on understanding data to learn about how data is stored in Firebase, along with the limitations and restrictions.
Create a $firebaseArray using the reference we just created.
angular.module('angularfireSlackApp')
  .factory('Users', function($firebaseArray, $firebaseObject){
    var usersRef = firebase.database().ref('users');
    var users = $firebaseArray(usersRef);

    var Users = {};

    return Users;
  });

It's also good to know that $firebaseArray will return a pseudo-array, meaning it will act a lot like an array in javascript, however, methods like splice(), push(), pop() will only affect data locally and not on the Firebase. Instead, $firebaseArray provides methods named $add and $remove to provide similar functionality while keeping your data in sync. Read the $firebaseArray Documentation For a complete understanding of how $firebaseArray should be used.

Set Users to the following object:
var Users = {
  getProfile: function(uid){
    return $firebaseObject(usersRef.child(uid));
  },
  getDisplayName: function(uid){
    return users.$getRecord(uid).displayName;
  },
  all: users
};

getProfile(uid) allows us to get a $firebaseObject of a specific user's profile, while all returns a $firebaseArray of all the users. getDisplayName(uid) is a helper function that returns a user's displayName when given a uid. We will be keying our data by the uid that comes back from our Firebase auth data, so data in our Firebase will look similar to:

{
  "users": {
    "userid1":{
      "displayName": "Blake"
    },
        "userid2":{
          "displayName": "Marie"
        }
  }
}

Now that our Users service is created, let's create a controller for updating a user's profile. First we'll need to create a new state in app/app.js to resolve a couple dependencies. We want to have the user's auth data and their profile available to us before our controller is instantiated.

Create a new state for profile:
.state('profile', {
  url: '/profile',
  resolve: {
    auth: function($state, Users, Auth){
      return Auth.$requireSignIn().catch(function(){
        $state.go('home');
      });
    },
    profile: function(Users, Auth){
      return Auth.$requireSignIn().then(function(auth){
        return Users.getProfile(auth.uid).$loaded();
      });
    }
  }
})

We left the controller and templateUrl properties out of the state configuration temporarily because we haven't created them yet. The auth dependency is similar to the requireNoAuth dependency we created for login and register, except it does the inverse, where the user is redirected to the home state if they're not authenticated. The .catch function is a shorthand for handling promises if we don't want to provide a success handler. The profile dependency also ensures authentication, but resolves to the user's profile using the getProfile function we created in our Users service. $loaded is a function provided by both $firebaseObject and $firebaseArray that returns a promise that gets resolved when the data from Firebase is available locally.

Create ProfileCtrl in app/users/profile.controller.js, injecting $state, md5, auth, and profile
angular.module('angularfireSlackApp')
  .controller('ProfileCtrl', function($state, md5, auth, profile){
    var profileCtrl = this;
  });

We'll be using Gravatar to get profile picture functionality in our application. Gravatar is a service that provides us with a user's profile picture when given an email, however the email needs to be md5 hashed. Luckily, there are many modules available that can do this for us, and angular-md5 was already included in our seed codebase.

Set profile on the controller to the one that was resolved by the router
profileCtrl.profile = profile;
Create the following updateProfile function on our controller:
profileCtrl.updateProfile = function(){
  profileCtrl.profile.emailHash = md5.createHash(auth.email);
  profileCtrl.profile.$save();
};

Here we're getting the current user's email from the auth data that was resolved from our router, hashing it and setting to emailHash on profile. displayName will be set from the template we'll be creating next using ng-model.

Add a helper function getGravatar to the Users service that will return the url to a user's gravatar image when provided a uid.
getGravatar: function(uid){
  return '//www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
},
In app/index.html, include the user service and profile controller
<script src="auth/auth.service.js"></script>
<script src="users/users.service.js"></script>
<script src="users/profile.controller.js"></script>
Set controller and templateUrl in the profile state to the following:
url: '/profile',
controller: 'ProfileCtrl as profileCtrl',
templateUrl: 'users/profile.html',
Create the edit profile template in users/profile.html with the following code:
<div class="page-wrapper">

  <div class="page-header">
    <h1>Edit Profile</h1>
  </div>

  <form ng-submit="profileCtrl.updateProfile()">
    <p ng-hide="profileCtrl.profile.displayName">
      You'll need a display name before you can start chatting.
    </p>

    <div class="input-group">
      <input required type="text" class="form-control" placeholder="Display Name" ng-model="profileCtrl.profile.displayName">
    </div>
    <input type="submit" class="btn btn-default" value="Set Display Name">
  </form>

</div>

We should now be able to navigate to http://localhost:4000/#/profile, specify a display name for our user, submit the form and it should persist when we refresh the page.

Check your work

You can view the completed & working code for this tutorial here:

 

I finished! On to the next chapter