Angular without its module system

I'm going to demonstrate a way I've used Angular that allows you to swap its module system for other, better ones.

Ripping it out

Angular's module system is integral to Angular. Your application is booted up by creating an injector with your boot module(s), and that's used to prime the compiler with the directives and controllers it needs, not to mention routing etc if you've included it.

The module system creates a large blocker for lot of non-standard use cases - lazy-loading being an incredibly important one: if you're not inside that injector once it's created, you can't get at the same instances of the injector singletons the compiler uses: $directiveProvider, $controllerProvider and $routeProvider. A booted application can evidently only have one of these singletons, so it's hard to incrementally add controllers/directives/services.

So all non-standard uses of Angular's module system follow the same pattern:

CommonJS

I'll demo the approach with CommonJS - the module system used by Node and browserify. Rather than defining angular modules, we'll create standard CommonJS modules that expose Angular functionality like controllers, directives and routes:

// user.js
exports.controllers = {
  UserCtrl: UserCtrl,
};

exports.routes = {
  "/users": {
    template: require("./views/user/index.html"),
  },
  "/users/:id": {
    template: require("./views/user/show.html"),
  },
};

exports.directives = {
  userProfile: userProfile
};

To allow this we'll need a couple of tools: browserify and stringify. Browersify turns a set of CommonJS modules into a single file ready for use in the browser, with source-maps. stringify allow us to directly require() our templates, including them in the build distribution file. So to install we'll run:

npm install --save-dev browserify stringify

Loading

Now we've defined our modules, what does application boot look like? Time for the 'ripping modules out of Angular' algorithm:

// module that'll store our app singletons
var ng = require("../ng");

// our code that exposes controllers, directives etc
var deps = [
  require("./user"),
  require("./homepage"),
];

// the sole time we call 'angular.module' in our app
angular.module("app", ["ngRoute"])
.config(function($controllerProvider, $compileProvider, $routeProvider) { 

  // expose all of the providers so we can register functionality later
  _.extend(ng, {
    $controllerProvider: $controllerProvider,
    // ... the rest
  });

  deps.forEach(function(module) {
    _.each(module.controllers, function(controller, name) {
      $controllerProvider.register(name, controller);
    });

    _.each(module.directives, function(directive, name) {
      $compileProvider.directive(name, directive);
    });

    _.each(module.routes, function(route, path) {
      $routeProvider.when(path, route);
    });
  });

}) .run(function($rootScope, $rootElement, $timeout, $location, $q) { _.extend(ng, { $rootScope: $rootScope, // ... the rest }); });

Build and run

To build our app we run:

browserify app/boot.js -o dist/app.js

If you'd like to see this in action, here's a live demo and the source code.

Next time

In the next posts I'll address lazy-loading, dependency injection, and consider what we've gained and lost by jettisoning Angular's module system.