Angularize Your Rails App Part Three: Getting Data
In the previous post in this series I explained, with sample code, how to separate your AngularJS front end from your Ruby on Rails back end. In this post I will explain how to reconnect them for the purposes of getting data from the server.
Sharing data in AngularJS is done via services. You’re probably familiar with a few of the built in services provided by AngularJS, such as $locationProvider and $routeProvider, from the AngularJS routing post I wrote a couple weeks ago. Well, you can create your own services for use in your application. Let’s look at an example:
Here we have defined two new services, Task and Tasks. These use the AngularJS ngResource service, which must be declared as a dependency and included in your source files (downloadable here for your chosen release as angular-resource.js). The ngResource service provides us an easy way to send requests to our API and automatically load the data on receiving a reply.
As you may have guessed, the singular service (Task) is responsible for interactions with single, specific task objects. The plural service (Tasks) is used for interactions with multiple or non-specific objects. The url passed as the first argument to the $resource function must match the routes on your Rails API. Note that we’ve added the JSON extension since that is the format of response we want. Each of these services has multiple functions associated with it; these functions each have their own HTTP method (and potentially other variables) which will be received by Rails and used to interpret the request.
In order to use these services we must first declare the services module as a dependency for our main AngularJS module as follows:
From here we then declare the services themselves as a dependency anywhere we wish to use them, such as in a controller:
Now let’s retrieve a list of tasks for the index:
Wait a minute. Where’s the callback function? Isn’t that how JS works? You send a request, then handle it with a callback function. Actually you can do that by passing in a function as the second argument (after the data as the first) to the service function. In this case, however, that would be overkill. AngularJS knows your intention here is to wait for the request to be complete then assign $scope.tasks to the return value of that call, so that’s exactly what it does for you. When the response is parsed the value will be updated and your views will be re-rendered accordingly. It’s that easy to make API calls.
The Rails Side
There’s only one problem. Anyone following along and running this code is (likely) going to get an error response from their Rails application. Why? By default, Rails doesn’t respond to JSON format. We have to go into all our controllers and add that response type. Here’s an example from the tasks controller:
This basically says “If you received this request with a JSON extension then respond with the contents of the @tasks variable rendered in JSON format”. You’ll add a similar line in every one of your controllers which responds with data. You might add another response format if, for example, you wished to support XML. What about a simple “destroy” command which doesn’t need to respond with data beyond a report of success? I’m glad you asked:
This little snippet will result in no content but only a successful response code which AngularJS interprets. You can switch this to any of the available Rails symbols which map to HTTP response codes (an unofficial list is available here or by installing the cheat gem and running the command cheat status_codes). I’ve found these really handy in designing the API for our application.
There’s another problem, though. Let’s assume that each of our tasks has comments associated with it which we’d like to display. In our Rails views we’d call task.comments and it would give us the list of comments. The same is true AngularJS. So what’s the problem? It turns out that Rails, in an attempt to be efficient, is lazy about loading those comments. It doesn’t do so until you actually request them.
One solution would be to make another API call for each task, requesting the associated comments. You might choose this way if you only wanted to display the comments at user request (thus saving the transfer of those comments which are not to be displayed). If you intend to display all comments, a better way is to specify to Rails that you’d like to include them in the main tasks list. Here’s how:
In the first line we’re telling Rails that the query to load our requested task should include the associated comments. Rails might handle this with a single query or multiple queries depending on what it deems efficient. Then, in the third line, we’re telling Rails to include the associated comments when it renders the JSON response. Now let’s get back to displaying our content.
The Angular Side
When last we left our client it had assigned $scope.tasks to the result of our API call. This should now give you a real list of tasks. You can inspect them in the console or you can display them using code like this:
Then, in /templates/tasks/show.html:
And finally, in /templates/comments/show.html:
There you have it! You’ve successfully loaded your data from the Rails API and displayed it in your AngularJS application. Congratulations! Join me next week when we’ll be going the other way and using services to send data back to the Rails server. Update: The next post is now available here.