@script annotation and Angular.js

Note, this post has been edited on 12/11/2014 and 01/12/2015. These changes were made to keep the code base changes to handle routing and directives.

A previous post talked about a general idea behind data annotation and the ability to mark up classes. But the example was far from being real world or useful. Today we are going to explore the way that data annotation can be used to decouple business logic with angular bindings. We are in search of a simpler solution to ES6 modules or RequireJS and Angular modules.

Module Pattern and Angular

ES6 brings us modules and the "import" keyword for better composition management, which basically means that this code:

import {ngBase} from './ngBase';

transforms into RequireJS code that creates a dependency and on './ngBase

define(['./ngBase'], function($__0) {
  "use strict";
  if (!$__0 || !$__0.__esModule)
  $__0 = {default: $__0};
  var ngBase = $__0.ngBase;
});

Nicely enough, ngBase becomes the variable into which the import is piped, scoped to the wrapper function. So how does this play with something like Angular which has its module system. A quick look online brings about a general consensus that the module pattern is good for business logic, but the angular bootstrapping should take place once the require js modules are loaded.

Each of these post talks about creating non-angular aware modules that are later bootstrapped in another bootstrapping file. In my mind, this is a bit messy. When anything changes, you must change the bootstrapping file and the implementation file logic. But data annotation solves this problem. We can defer bootstrapping by marking our classes with all required data annotations to complete the late bootstrapping.

Annotate it All

So data annotation lets us create classes that understand their own purpose. Ideally, anything in Angular can be broken down into a class constructor function with annotation that defines the class function.

We annotate the controller/factory/directive name and dependency list into the ngController/ngFactory/ngDirective and ngInject annotation.

Annotation Classes

The magic behind this type of annotation is a collection of annotation classes that can register the class it annotates against an angular module.

ngBase class is an abstract that all Angular-based annotations derive from. We do have a useful wrap function, which allows us to wrap the Angular array notation for dependency injection. The ngBase class is common ground for all Angular annotations from Angular registering functions (ngController/ngFactory/ngDirective).

ngController class is an actual annotation class. It describes controllers to angular modules. The register function, which is aware of annotation on the class, can work in combination with other annotations like route and inject. The controller annotation simply takes the name.

@ngController('main')
@ngInject(['$scope', 'data'])
@ngRoute('/Home', { templateUrl : 'home.html' });

export class main {
  constructor($scope, data) {
    $scope.data = data();
  }
}

ngFactory annotation class for creating factories, the Angular version of a singleton. Like ngController, it also works in combination with

@ngFactory('data')

export class data {
  constructor() {
    return () => "test";
  }
}

ngDirective annotation class for creating directives has some additional logic. Like the other ngBase classes the name of the directive is the first argument of the annotation constructor and the directive property object as the second argument. The constructor of the annotated class automatically becomes the link function on the directive object.

@ngDirective('hello', {
  scope : true,
  template : '<div> hello {{data}}</div>',
  restrict : 'AE'
})
@ngInject(['data'])
export class hello {
  constructor(data) {
    return (scope, element, attr) => {
      scope.data = data();
    }
  }
}

The Bootstrapper

Annotations can bind behavior to a class, and we can process the behavior by looking at injectable dependencies. The ngBootstrap class processes a list of imports. In our bootstrapping process, this is denoted by arguments based on how Tracuer transpiles this code.

import {ngBootstrap} from './lib/ngBootstrap';
import {data} from './data';
import {main} from './main';
import {hello} from './hello';

var app = ngBootstrap(document, angular.module('test', []), arguments);

Transpiled, we get this code, which explains the argument's object.

define(['./lib/ngBootstrap', './data', './main', './main2', './hello'], function($__0,$__2,$__4,$__6,$__8) {
  "use strict";
  if (!$__0 || !$__0.__esModule)
  $__0 = {default: $__0};
  if (!$__2 || !$__2.__esModule)
  $__2 = {default: $__2};
  if (!$__4 || !$__4.__esModule)
  $__4 = {default: $__4};
  if (!$__6 || !$__6.__esModule)
  $__6 = {default: $__6};
  if (!$__8 || !$__8.__esModule)
  $__8 = {default: $__8};
  var ngBootstrap = $__0.ngBootstrap;
  var data = $__2.data;
  var main = $__4.main;
  var main2 = $__6.main2;
  var hello = $__8.hello;
  var app = angular.module('test', ['ngRoute']);
  ngBootstrap(document, app, arguments);
  return {};
});

Conclusion

You can find running code examples here

This post gave a real world example of the usefulness of data annotation. But there is a lot more. There are benefits to decoupling the application from Angular. The annotation can be quickly re-written to bind a different framework to your current classes. There are benefits to unit testing a directive without needing to bind to angular.

Overall, data annotation is a tool for the tool box, and more posts will come on how this tool is useful.

Related Books