Firebase
Build a Real-Time Slack Clone using AngularFire

Creating the Channels Sidebar

PRO

Learn how to use ui-router to manage states and create an interface for making chat channels

Now that our Users can authenticate and have profiles, we can finally start to create the functionality for Channels. We're going to start by creating the left sidebar for listing channels and display the current user's profile.

Create a service Channels in app/channels/channels.service.js that will return a $firebaseArray at the channels node.
angular.module('angularfireSlackApp')
  .factory('Channels', function($firebaseArray){
    var ref = firebase.database().ref('channels');
    var channels = $firebaseArray(ref);

    return channels;
  });
Create a state for channels with the following code:
.state('channels', {
  url: '/channels',
  resolve: {
    channels: function (Channels){
      return Channels.$loaded();
    },
    profile: function ($state, Auth, Users){
      return Auth.$requireSignIn().then(function(auth){
        return Users.getProfile(auth.uid).$loaded().then(function (profile){
          if(profile.displayName){
            return profile;
          } else {
            $state.go('profile');
          }
        });
      }, function(error){
        $state.go('home');
      });
    }
  }
})

templateUrl and controller are temporarily omitted since we haven't created them yet. We're resolving two dependencies here: channels, which is promising our $firebaseArray of channels, and profile, which is a lot like the profile dependency in the profile state, but we're ensuring that the user already a displayName set, otherwise they're taken to the profile state, and if they're not authenticated, they get sent to the home state.

Create ChannelsCtrl in app/channels/channels.controller.js, injecting $state, Auth, profile, and channels.
angular.module('angularfireSlackApp')
  .controller('ChannelsCtrl', function($state, Auth, Users, profile, channels){
    var channelsCtrl = this;
  });
Set channels and profile to the resolved dependencies from the router
channelsCtrl.profile = profile;
channelsCtrl.channels = channels;
Set getDisplayName and getGravatar to the respective functions on the Users service.
channelsCtrl.getDisplayName = Users.getDisplayName;
channelsCtrl.getGravatar = Users.getGravatar;
Create a logout function that will allow our users to log out, returning them to the home state.
channelsCtrl.logout = function(){
  Auth.$signOut().then(function(){
    $state.go('home');
  });
};
In app/index.html, include the channels service and controller
<script src="users/profile.controller.js"></script>
<script src="channels/channels.controller.js"></script>
<script src="channels/channels.service.js"></script>
Create the template for our channels sidebar in app/channels/index.html with the following code:
<div class="main">
  <div class="sidebar">

    <div class="slack-name">
      <h2>FireSlack</h2>
    </div>

    <div class="channel-list">
      <div class="list-head">Channels</div>
    </div>

    <div class="my-info">
      <img class="user-pic" ng-src="{{ channelsCtrl.getGravatar(channelsCtrl.profile.$id) }}" />
      <div class="user-info">
        <div class="user-name">
          {{ channelsCtrl.profile.displayName }}
        </div>

        <div class="options">
          <a ui-sref="profile">edit profile</a>
          /
          <a href="#" ng-click="channelsCtrl.logout()">logout</a>
        </div>
      </div>
    </div>

  </div>
</div>
Set the controller and templateUrl in channels state to the following:
url: '/channels',
controller: 'ChannelsCtrl as channelsCtrl',
templateUrl: 'channels/index.html',
Add the following resolve to the home state:
resolve: {
  requireNoAuth: function($state, Auth){
    return Auth.$requireSignIn().then(function(auth){
      $state.go('channels');
    }, function(error){
      return;
    });
  }
}
Update the updateProfile function in ProfileCtrl to send the user to the channels state after a successful save.
profileCtrl.updateProfile = function(){
  profileCtrl.profile.emailHash = md5.createHash(auth.email);
  profileCtrl.profile.$save().then(function(){
    $state.go('channels');
  });
};

This requireNoAuth dependency is a lot like the one on login and register but it sends the user to the channels state. If you want, you can change the requireNoAuth dependency on login and register to also send the user to the channels state as well. Now when we're logged in and visit http://localhost:4000 we should be sent to the channels state. In that state, we can see the sidebar we just created for our application. There should be the logged in user's name and Gravatar at the bottom of the sidebar, and an edit profile and logout link next to it. We're using the ui-sref directive that comes with ui-router to specify what state we should navigate to on click. If we click on edit profile, we can update the user's displayName and it should send us back to the channels state when we submit the form. The logout link should log us out and send us back to the home state. Now let's create the view to create channels.

Create a new state channels.create:
.state('channels.create', {
  url: '/create',
  templateUrl: 'channels/create.html',
  controller: 'ChannelsCtrl as channelsCtrl'
})

This state is a child state of the channels controller (the dot notation in the state name specifies parentState.childState). This state will also use ChannelsCtrl. We'll want to render our child states to the right of the sidebar. We need to declare another ui-view tag in order for our child states to appear. You can read more about ui-router's nested states at this Github Wiki.

Add the message-pane div with a ui-view tag in app/channels/index.html. This should be at the end of the main div after the sidebar div.
<div class="message-pane">
  <ui-view></ui-view>
</div>
Add a newChannel object on ChannelsCtrl with a blank name
channelsCtrl.newChannel = {
  name: ''
};
Create a createChannel function on ChannelsCtrl.
channelsCtrl.createChannel = function(){
  channelsCtrl.channels.$add(channelsCtrl.newChannel).then(function(){
    channelsCtrl.newChannel = {
      name: ''
    };
  });
};

The $add function on the channels $firebaseArray provides similar functionality to the .push() function on a Javascript Array, but keeps our data in sync with Firebase, while returning a promise. Once the new channel is created we'll need to clear out the newChannel object.

Create the a form in app/channels/create.html for creating a new channel
<div class="header">
  <h1>Create a channel</h1>
</div>

<form ng-submit="channelsCtrl.createChannel()">
  <div class="input-group">
    <input type="text" class="form-control" placeholder="Channel Name" ng-model="channelsCtrl.newChannel.name">
  </div>
  <input type="submit" class="btn btn-default" value="Create Channel">
</form>
Create the channels listing in app/channels/index.html using the following markup in the channel-list div below the list-head div:
<div class="channel-list">
  <div class="list-head">Channels</div>
  <div class="channel" ng-repeat="channel in channelsCtrl.channels">
    <a># {{ channel.name }}</a>
  </div>
</div>

We're using the ng-repeat directive to iterate over our array of channels.

Create a link to the channels.create state below the channel listing:
<div class="channel-list">
  <div class="list-head">Channels</div>
  <div class="channel" ng-repeat="channel in channelsCtrl.channels">
    <a># {{ channel.name }}</a>
  </div>

  <div class="channel create">
    <a ui-sref="channels.create">+ create channel</a>
  </div>
</div>

We're now able to click on the create channel link and start creating channels!

Check your work

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