Article

Latest articles →

· Max Wheeler

An all-in-one directive-controller with AngularJS

This week the kind folks at Good Films hosted the second ngMelb — the AngularJS Melbourne meetup — at their offices in Fitzroy and I gave a little demo of a technique I’ve used recently for wrapping up some complex behaviour into a simple directive. The idea being that we can replace a single element with a much larger widget that is populated with data from an external API. In this (somewhat contrived) example we’re going to retrieve all the public repositories that Angular have available on Github, as well as the latest public repositories to have any activity.

You can view the entire example on JSFiddle. The implementation there is in CoffeeScript though, so if you prefer plain ol’ JavaScript keep reading below.

The code is made up of three parts: a directive for building up the boilerplate template for our widget, a controller inside that directive that handles a little logic for its behaviour, and a factory that provides a consistent interface to the Github API.

<div github-repos github-user='angular'></div>
<div github-repos></div>

The directive matches any element with an attribute of github-repos and replaces it with contents of the postsTemplate variable. That template contains a pretty standard ng-repeat directive for iterating over a set of data. This is where the magic of AngularJS starts to show: you don’t have to do anything special to bind the directives in the newly inserted template. Angular sees the new directives automatically and handles all the data bindings for you.

var github = angular.module('github', []);

github.directive('githubRepos', function() {
  var postsTemplate = "<div class='github-repos ng-cloak'>" +
    "<h1>{{ title }}</h1>" +
    "<a class='github-repo' href='{{ repo.url }}' ng-repeat='repo in repos'>" +
    "<h3 class='github-repo__name'>{{ repo.name }}</h3>" +
    "<h4 class='github-repo__full-name'>{{ repo.full_name }}</h4>" +
    "<div class='github-repo__description' ng-show='repo.description'>" +
    "<p>{{ repo.description }}</p>" +
    "</div>" +
    "<time class='github-repo__created'>{{ repo.created_at | date:'mediumDate' }}</time>" +
    "</a>" +
    "</div>";
  return {
    restrict: 'A',
     // Replace the div with our template
    replace: true,
    template: postsTemplate,
    scope: {
     // Pass `github-user` through from the the attributes, we need to create
     // a separate scope for each widget.
      githubUser: '@'
    },
    // Specify a controller directly in our directive definition.
    controller: function($scope, githubFactory) {
      $scope.repos = [];
      if ($scope.githubUser) {
        return githubFactory.userRepos($scope.githubUser).success(function(rsp) {
          return $scope.repos = rsp.data;
        });
      } else {
        return githubFactory.repos().success(function(rsp) {
          return $scope.repos = rsp.data;
        });
      }
    }
  };
});

In a normal app we’d define a controller function somewhere to handle the logic and behaviour of our widget. The behaviour we want is pretty simple though so there’s no point creating a separate global function, especially when it would mean separating such tightly coupled code — luckily we don’t have to. Angular lets you specify a controller function directly in the definition of a directive. That function will get all the normal benefits of Angular’s dependency injection system and so all we need to do is pass in the $scope injectable and we can pass data back and forth from our directive.

If you’ve been paying attention you’ll have noticed that there’s another dependency that we’re injecting into our controller function. The githubFactory is a custom factory created to encapsulate the interface for talking to the Github API. That factory looks something like this:

github.factory('githubFactory', function($http) {
  var domain, endpoint, params, path, protocol, repos, userRepos;
  protocol = "https://";
  domain = 'api.github.com';
  path = '';
  endpoint = function() {
    return "" + protocol + domain + path;
  };
  params = function() {
    return "?callback=JSON_CALLBACK";
  };
  userRepos = function(user) {
    return $http.jsonp("" + endpoint() + "/users/" + user + "/repos" + params());
  };
  repos = function() {
    return $http.jsonp("" + endpoint() + "/repositories" + params());
  };
  return {
    userRepos: userRepos,
    repos: repos
  };
});

We’re using Angular’s factory function to create our factory as it offers a little more flexibility than a service. Setting it up like this means it automatically becomes available to Angular’s dependency injection. We can simply pass githubFactory as an argument to our directive controller and it’ll be resolved for us, saving us the hassle of manually managing the load order for common bits of functionality like this. It’s important to remember that our factory (like all services and providers) is a Singleton, which may or may not be what you’re after in some cases.

The actual structure of the factory is pretty simple. We pass in the $http service so we can make AJAX calls, normalise some of the specifics about talking to Github, and then return an API that defines the interface we want. The neat thing about the $http service is that it creates a deferred with a promise that resolves as either .success() or .error() once the AJAX call is complete. This allows us to simply return the $http object from our factory and let whatever we’re passing it to handle the callback. In this case we just attached the response data to our $scope and the ng-repeat we injected earlier will automatically iterate over the data and output each template.

What we end up with is a really simple way of including our widget anywhere across out site: add a single <div github-repos> and it’ll just appear with the latest data and all its template structure. And, by structuring our code into separate components, we also get a consistent interface to the Github API that we can automatically inject into any places we need it.

This is part of the reason I’ve become enamoured of Angular: it encourages you to write things The Right Way™ and makes it harder to do stupid things. I’ll take all the help I can get.