This is the first post in what I hope to be many in my series "The RAP Stack". Each post will detail something I discovered/implemented using Rails, Angular, and/or Postgres. Each post is inspired by code we implemented at Pathgather. Want to join us and use Angular in your every day work? We're hiring! Feel free to reach out to me at email@example.com
We want to allow a logged-in user to follow/unfollow another user, on any page. If it's a list of users, each user should have a follow button (if we aren't already following them) or an unfollow button (if we are already following them).
- We'll create a model (service) for the current logged-in user. Every service, controller, and directive will have to inject our model to gain access to the current user.
- We'll create another service that stores a local cache of the users we are already following. Following & unfollowing users will update this cache.
- We'll create a directive that renders the appropriate follow/unfollow button for each displayed user.
Creating our logged-in user's model
When first learning Angular, it seemed that most tutorials I read only covered creating models with static JSON data stored directly on the client, which is something you'll likely only ever implement in 'throw-away' projects. And I've yet to come across a tutorial that provided a way to represent the current user, who lives behind a login, with an Angular model.
It seemed like a trivial issue on the surface: the user was logged-in, and I needed the user in a variety of directives, services, etc. Sometimes, I needed to do something as simple as render the user's name or image, other times I wanted to use Restangular (a library you should check it if you haven't yet) to send a request to the server off the current user (say the user wrote a review, and we needed to POST that review to the server). However, I had to fetch the user from the server first, wait for the request to return, and then do whatever I needed. Also, since any number of services/directives/controllers may need the current user, we want to be sure to send only one server request for it, even if there are 10 directives simultaneously asking for it.
In Angular, services are singletons with initialization that occurs once the app loads. With our current user model, we'll want to get a fresh instance of the current user from the server the first time. Services, controllers, etc, will need to ensure the server request has been resolved before trying to access a property off the current user. This is the approach I finally settled on:
Here, we send an initial request to the server for the current user. The service returns, at the moment, one function to get the current user. Since another component may very well ask for our current user before the server request returns, the getUser function always returns a promise, one of which resolves immediately with the current user if it exists. If a controller, directive, etc., needs to know something about the current user, they must inject our service and use it like this:
CurrentUser.getUser().then (current_user) ->
# Promise is resolved and we are free to use the current user now
A service to encapsulate our following functionality
Now, let's go back to our scenario where we want to have the appropriate follow/unfollow button next to a user, no matter the page they show up on.
To do this, we need to know if the current user is already following a certain user. At Pathgather, users appear in bunches, and I'd prefer to not hit the server for each user to ask it if the user is already being followed. So, we'll do something similar to above, but this time in another service:
This service differs a bit from the CurrentUser one in that we don't immediately send a request for the list of users being followed. Why not? Eh, mainly because many places in our app won't ever need those users. So, the first request hits the server for the followed users, and subsequent requests do not. Then, when we want to know if the current user is following someone, we use a little helper method that makes sure we have a list of followed users to test against. When we do, we simply search through it (with Underscore) for the given user. Now, we'll use this helper to dynamically render the right follow/unfollow button.
Using our services plus a directive to load the right action
With the above, we are now ready to render the appropriate follow/unfollow button for any user. We can use a directive to encapsulate all the functionality in one reusable component.
Here, we've built a directive that injects our CurrentFollowedUsers service and creates its own (isolate) scope so that we don't accidentally mess with another scope's data. Isolate scopes can be a pain sometimes, but I find them to be entirely appropriate for reusable widgets like our follow button. Since we'll be using the directive in many different places, an isolate scope guarantees that we won't stomp on anything outside of our directive.
We have 2 functions: one that dynamically creates a follow button and the other an unfollow button. The directive can be used like this:
<div ng-repeat="user in users">
The directive then waits (watches) for the user to bind, and once it does, we use our CurrentFollowedUsers service to determine if we are already following the given user. If we are, then we'll load an unfollow button. Otherwise, we'll load the follow one.
And finally, we now need to actually follow/unfollow the user when the button is clicked. Since this will affect the current followed users, we'll use our CurrentFollowedUsers service.
You can probably see what's happening here. We first make sure we have access to our current user, as we'll use the functionality behind Restangular to communicate with our server. When we do, we shoot a POST to our server to follow the given user. If that's successful, we add the user to our local cache of our followed users so that subsequent requests to followingUser will return the right result. Unfollowing a user does the exact opposite.
Wrap it up!
Well, that's about it for my first 'RAP Stack' post. I'd love to hear other people's thoughts, but I really like this implementation. We send the absolute minimum amount of server requests possible, and work off of local caches after the first requests. Even the requests we do send our bite-sized, so they should return pretty quickly. Plus, functionality is encapsulated pretty well: the current user is limited to a single service, and the current user's following in another. At Pathgather, we have many services like our CurrentFollowedUsers one: CurrentUserReviews, CurrentUserCourses, etc. Plus, functionality provided by our follow/unfollow button is contained inside one small, reusable directive, so putting it on any page is extremely trivial. Let me know your thoughts!
This post was inspired by functionality implemented at Pathgather. Want to join us and use Angular in your every day work? We're hiring! Feel free to reach out to me at firstname.lastname@example.org