Saturday, February 16, 2013

Angularjs & Yeoman - Part 3

Intro

In the first post of this series, we had a look at features that we found impressive in Angular. In the second post we created a really simple edit in place "app" using the basics of Angular. This post takes things further, and, even though the app doesn't do much apart from displaying a list of genres / subgenres of TV programmes, it is well scaffolded and gives a good look at why Angular + Yeoman is awesome for creating web apps.
The code for this can be found on bitbucket: git@bitbucket.org/mchauvinc/genrespanel.git

Goal

The app will have 2 views, a "Control panel" view which holds links to the other views, and a Genres view, which displays a list of genres and subgenres which can be edited, added, removed.

Testing

Though it is highly encouraged to do so, we have not yet succeeded in getting the Jasmine tests running with dependency injection to fake the API calls. So skipping this at the moment. Any help is welcome :)

Scaffolding

As mentioned previously, one of the greatest things we found about Angular is it's scaffolding companion Yeoman. 
Yeoman is a command line tool that scaffolds your app (it can do several types of apps, but we're mostly interested in its Angular abilities), creates controllers, routes, views, services, includes corresponding js files into your main html file, helps you run tests, and more. It is an awesomely powerful tool, which should remind you of RoR scaffolding.

Furthermore, it includes a web server which watches for changes on your files and reloads automatically, a pretty neat feature.

Assuming that Yeoman is fully installed, creating an app is as simple as typing this in a console:

yeoman init angular

After answering a few questions, Yeoman creates a whole bunch of files. You can actually at this stage run

yeoman server

and view your app on port 3501 by default.
But going back to the console, we can actually scaffold a few more things.

Route


Using

yeoman init angular:route genres

creates the corresponding controller, html view, adds the route to the app, and the scripts to our HTML page. Isn't that awesome? Try browsing to http://localhost:3501/#/genres

Service


The service is the link to the backend API. You can have different types of definition of a service, for example one that simply returns a value (value definition), one that behaves like a constructor (service definition) or one that returns an object (factory definition). All services are singletons, and the factory ensures separation of concerns (other parts of the app don't need to know how the API connection is made). A good description of the difference between service and factory can be found here.
We will use a factory: type

yeoman init angular:service genresService --factory

and the genres service has been added to the app. We will get into the actual service in the next section.

Getting data

The service that was created by Yeoman currently only holds a someMethod function which returns 42. Yeoman has a good sense of humor. Let's wire it instead to grab data from an API. (app/scripts/services/genres/genresService.js)

If you try to save the above in the service and reload your page, you will see that the script fails. This is because $resource isn't available. $resource is an Angular service which lets you interact with RESTful APIs easily. Alternatively, you could use $http which handles all forms of AJAX requests (not limited to CRUD objects). Since $resource is not built in, you need to

  1.  add the file angular-resource.js to your scripts. You can
    yeoman install angular-resource
    to get the latest from git. Somehow, in my case, the file did not get added to the html though, so I've added it manually in index.html using <script src="components/angular-resource/angular-resource.js"></script>
  2. Tell angular that the app will be using $resource by adding 'ngResource' to the (currently empty) array of "imports" in the app.js definition:
    var nooApp = angular.module('nooApp', ['ngResource'])
Once this is done, $resource becomes available in any parts of your application, if you request it using $resource in the function parameters. This is handled by dependency injection.

Because we are calling an external API, JSONP is required. We can override the default http methods for each action of the $resource object when initializing the method as seen above. An awesome video by John Lindquist explains almost all you need to know about $resource. Just be careful when overriding the method for "query" to include "isArray" as true again if your query returns an array of objects (it does in our case). "JSON_CALLBACK" lets Angular keep track of which padding function to use.

So, what happens is that when the service is initialized, we create a $resource object with correct API url, and make it available to any part of the app that requests genresService using dependency injection.

Controller(s)

Though we have a main controller, it isn't really being used. So let's focus on the genres controller. It actually does 4 things only.
  1. Query the API for a list of genres / subgenres
  2. Add a subgenre (check that it has a unique id for given genre)
  3. Remove a subgenre
  4. Prepare to save (we won't implement save)
A pretty neat feature here, is that when query() is called on the genresService, an empty array is immediately returned, so that our table simply looks empty while loading.

When an ng-click event is fired, the context ("this") is the Angular enhanced DOM element. That element is bound to its subgenre and even one level up to its genre. This is how we handle deleting subgenres, by going from the DOM element to the Genre object's list of subgenres (and filtering out the model corresponding to the element we clicked on)

View

The genres view will show a table of genres, with their corresponding list of editable subgenres. To that effect we will use two levels of repeaters. How repeaters work is explained in the basic examples of angularjs.org.
Also used in the view are:
  • ng-click directives on Add and remove buttons
  • An input per genre to create a new subgenre. Note that ng-model is set to genre.newKey because each input "belongs" to a genre
  • A hidden error message which will be shown if the key is already in use, using ng-show="genre.invalidKey". Again, showing the error message depends on each genre so we watch for genre.invalidKey
  • Subgenre title is editable via an input bound to ng-model subgenre.title
  • doSave does not actually save, only because the API point has not been implemented, but if you look at your network activity in Chrome dev tools, you'll see the call with all the JSON encoded data.

Conclusion

The list of genres and subgenres gets populated via the API, in a really easy manner.

Removing and adding elements to the array of subgenres automatically takes care of rendering and the final data can be read from the models (instead of having to go through the rows and input elements to read the values). I think this is absolutely awesome. Not one bit of DOM manipulation required. So we can focus on the logic instead.

No comments:

Post a Comment