AngularJS Conventions

This document accompanies the UI Coding Conventions. It gives specific guidance for AngularJS modules, controllers, directives, and filters.

Modules

Modules in angular should describe and bind together a small unit of functionality. The OpenLMIS-UI build process should construct larger module units from theses small units.

Documentation

Docs for modules must contain the module name and description. This should be thought of as an overview for the other objects within the module, and where appropriate gives an overview of how the modules fit together.

/**
 * @module module-name
 *
 * @description
 * Some module description.
 */

Controller

Controllers are all about connecting data and logic from Factories and Services to HTML Views. An ideal controller won’t do much more than this, and will be as ‘thin’ as possible.

Controllers are typically specific in context, so as a rule controllers should never be reused. A controller can be linked to a HTML form, which might be reused in multiple contexts — but that controller most likely wouldn’t be applicable in other places.

It is also worth noting that John Papa insists that controllers don’t directly manipulate properties in $scope, but rather the ControllerAs syntax should be used which injects the controller into a HTML block’s context. The main rationale is that it makes the $scope variables less cluttered, and makes the controller more testable as an object.

Conventions

  • Should be only object changing application $state
  • Is used in a single context
  • Don’t use the $scope variable EVER
  • Use ControllerAs syntax
  • Don’t $watch variables, use on-change or refactor to use a directive to watch values

Unit Testing

  • Set all items that would be required from a route when the Controller is instantiated
  • Mock any services used by the controller

Documentation

The only difference between controllers and other components is the ‘.controller:’ part in the @name annotation. It makes controller documentation appear in controllers section. Be sure to document the methods and properties that the controller exposes.

/**
 * @ngdoc controller
 * @name module-name.controller:controllerName
 *
 * @description
 * Controller description.
 */

Directive

Directives are pieces of HTML markup that have been extended to do a certain function. This is the only place where it is reasonable to manipulate the DOM.

Make distinction between directive and component – components use E tag and isolate scope, directive use C and never isolate scope

Conventions

  • Restrict directives to only elements or attributes
  • Don’t use an isolated scope unless you absolutely have to
  • If the directive needs external information, use a controller — don’t manipulate data in a link function

Unit Testing

The bit secret when unit testing a directive is to make sure to use the $compile function to return an element that is extended with jQuery. Once you have this object you will be able to interact with the directive by clicking, hovering, or triggering other DOM events.

describe('SampleDirective', function() {

	it('gets compiled and shows the selected item name', function($compile, $rootScope) {
		var scope = $rootScope.$new();
		scope['item'] = {
			name: "Sample Title"
		};
		var element = $compile("<sample-directive selected='item'></sample-directive>")(scope);

		expect(element.text()).toBe("Sample Title");
	});

	it('responds to being clicked', function($compile, $rootScope) {
		var element = $compile("<sample-directive selected='item'></sample-directive>")($rootScope.$new());

		// check before the action
		expect(element.text()).toBe("No Title");

		element.click();
		// check to see the results of the action
		// this could also be looking at a spy to see what the values are
		expect(element.text()).toBe("I was clicked");
	});

});

Documentation

Directive docs should have well described ‘@example’ section.

Directive docs should always have ‘@restrict’ annotation that takes as a value one of: A, E, C, M or any combination of those. In order to make directive docs appear in directives section there needs to be ‘.directive:’ part in @name annotation.

/**
 * @ngdoc directive
 * @restrict A
 * @name module-name.directive:directiveName
 *
 * @description
 * Directive description.
 *
 * @example
 * Short description of how to use it.
 * ```
 *   <div directiveName></div>
 * ```
 * Now you can show how the markup will look like after applying directive code.
 * ```
 * <div directiveName>
 *     <div>something</div>
 * </div>
 * ```
 */

Extending a Directive

You can extend a directive by using AngularJS’s decorator pattern. Keep in mind that a directive might be applied to multiple places or have multiple directives applied to the same element name.

angular.module('my-module')
    .config(extendDirective);

extendDirective.$inject = ['$provide'];
function extendDirective($provide) {
  
  // NOTE: This method has you put 'Directive' at the end of a directive name
  $provide.decorator('OpenlmisInvalidDirective', directiveDecorator);
}

directiveDecorator.$inject = ['$delegate'];
function directiveDecorator($delegate) {
  var directive = $delegate[0], // directives are returned as an array
      originalLink = directive.link;

  directive.link = function(scope, element, attrs) {
    // do something
    originalLink.apply(directive, arguments); // do the original thing
    // do something after
  }

  return $delegate;
}

Filters

Use an AngularJS filter if:

  • You need to do complex formatting
  • You need to render value in HTML, and it doesn’t make sense to include in a controller.

Documentation

Filter docs should follow the pattern from example below:

/**
 * @ngdoc filter
 * @name module-name.filter:filterName
 *
 * @description
 * Filter description.
 *
 * @param   {Type} input     input description
 * @param   {Type} parameter parameter description
 * @return  {Type}           returned value description
 *
 * @example
 * You could have short description of what example is about etc.
 * ```
 * <div>{{valueToBeFiltered | filterName:parameter}}</div>
 * ```
 */

It is a good practice to add example block at the end to make clear how to use it. As for parameters the first one should be describing input of the filter. Please remember of ‘.filter:’ part. It will make sure that this one will appear in filters section.