Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Latest commit

 

History

History
698 lines (529 loc) · 14.5 KB

angular-style-guide.adoc

File metadata and controls

698 lines (529 loc) · 14.5 KB

Hawkular UI - Angular Style Guide

Tip
For language level ES6/Typescript style guide please use: Air Javascript Style Guide (ES6)

This Angular Style Guide is based on: Opinionated AngularJS style guide for teams by @toddmotto and modified for usewith Hawkular and typescript instead of javascript.

Tip
This is by no means an Angular or Typescript tutorial. For tutorial resources please see: Angular/Typescript Resources

Modules

*

Definitions: Declare modules without a variable using the setter and getter syntax

// avoid
var app = angular.module('app', []);
app.controller();
app.factory();

// recommended
angular
 .module('app', [])
 .controller()
 .factory();

*

Note: Using angular.module('app', []); sets a module, whereas angular.module('app'); gets the module. Only set once and get for all other instances.

*

Methods: Pass functions into module methods rather than assign as a callback

// avoid
angular
 .module('app', [])
 .controller('MainController', function MainController () {

})
 .service('SomeService', function SomeService () {

});

// recommended
function MainController () {

}
function SomeService () {

}
angular
 .module('app', [])
 .controller('MainController', MainController)
 .service('SomeService', SomeService);

*

This aids with readability and reduces the volume of code "wrapped" inside the Angular framework

Tip
Since our controllers will be Typescript, we will just use Typescript Classes and not functions to package the controllers, services, etc…​

Controllers

*

Controller names should always end in *Controller (not *Ctrl or anything else)

controllerAs syntax: Controllers are classes, so use the controllerAs syntax at all times

<!-- avoid -->
<div ng-controller="MainController">
 {{ someObject }}
</div>

*

In the DOM we get a variable per controller, which aids nested controller methods, avoiding any $parent calls

*

The controllerAs syntax uses this inside controllers, which gets bound to $scope

// avoid
function MainController ($scope) {
 $scope.someObject = {};
 $scope.doSomething = function () {

};
}

// recommended
class MainController {
  public someObject :any;
  constructor() {
    this.someObject = {};
  }
  public doSomething ():void {

  };
}

*

Only use $scope in controllerAs when necessary; for example, publishing and subscribing events using $emit, $broadcast, $on or $watch. Try to limit the use of these, however, and treat $scope as a special use case

*

controllerAs 'vm': Capture the this context of the Controller using vm, standing for ViewModel

// avoid
function MainController () {
 var doSomething = function () {

};
 this.doSomething = doSomething;
}

// recommended
function MainController () {
 var vm = this;
 var doSomething = function () {
 };
 vm.doSomething = doSomething;
}

Why? : Function context changes the`this`value, use it to avoid`.bind()` calls and scoping issues

*

Presentational logic only (MVVM): Presentational logic only inside a controller, avoid Business logic (delegate to Services)

// avoid
function MainController () {
 var vm = this;

$http
 .get('/users')
 .success(function (response) {
 vm.users = response;
 });

vm.removeUser = function (user, index) {
 $http
 .delete('/user/' + user.id)
 .then(function (response) {
 vm.users.splice(index, 1);
 });
 };

}

// recommended
class MainController  {
  constructor(private $scope :any,
              private UserService :IUserService) {
    $scope.vm = this;
  }

  public getUsers() {
  UserService.getUsers()
  .then(function (response) {
    this.users = response;
  });
 }

 public removeUser (user :string, index :number) :void  {
    UserService.removeUser(user)
    .then(function (response) {
      this.users.splice(index, 1);
  });
 };

}

Why? : Controllers should fetch Model data from Services, avoiding any Business logic. Controllers should act as a ViewModel and control the data flowing between the Model and the View presentational layer. Business logic in Controllers makes testing Services impossible.

Tip
Please try to always provide a type to the response objects returned from an external call — otherwise, we have to goto the external source to find out what the schema is.

Services and Factory

  • All Angular Services are singletons, using .service() or .factory() differs the way Objects are created.

Services: act as a constructor function and are instantiated with the new keyword. Use this for public methods and variables

```javascript
function SomeService () {
  this.someMethod = function () {

  };
}
angular
  .module('app')
  .service('SomeService', SomeService);
```

Factory: TIP: Factories are not really useful with Typescript like they are in javascript, use Services instead.

// avoid
function AnotherService () {
 var AnotherService = {};
 AnotherService.someValue = '';
 AnotherService.someMethod = function () {

};
 return AnotherService;
}
angular
 .module('app')
 .factory('AnotherService', AnotherService);
// recommended
 export class SomeService implements ISomeService {

    public static $inject = ['$log', 'toastr'];

    constructor(private $log: ng.ILogService,
                private toastr: any) {
    }

    public doSomeThing(message: string): void {
      this.toastr.info(message, 'info');
    }

}

  _module.service('SomeService', SomeService);

Directives

*

Declaration restrictions: Only use custom element and custom attribute methods for declaring your Directives ({ restrict: 'EA' }) depending on the Directive’s role

<!-- avoid -->

<my-directive></my-directive>
<div my-directive></div>

*

Comment and class name declarations are confusing and should be avoided. Comments do not play nicely with older versions of IE. Using an attribute is the safest method for browser coverage.

*

DOM manipulation: Takes place only inside Directives, never a controller/service

// avoid
function UploadController () {
 $('.dragzone').on('dragend', function () {
 // handle drop functionality
 });
}
angular
 .module('app')
 .controller('UploadController', UploadController);

// recommended
function dragUpload () {
 return {
 restrict: 'EA',
 link: function (scope, element, attrs) {
 element.on('dragend', function () {
 // handle drop functionality
 });
 }
 };
}
angular
 .module('app')
 .directive('dragUpload', dragUpload);

*

Naming conventions: Never ng- prefix custom directives, they might conflict future native directives, instead for Hawkular use hk- so its easy to tell that it came from our project. [Also, don’t use data-my-directive, it is just not necessary].

// avoid
// <div ng-upload></div>
function ngUpload () {
 return {};
}
angular
 .module('app')
 .directive('ngUpload', ngUpload);

// recommended
// <div hk-drag-upload></div>
function dragUpload () {
 return {};
}
angular
 .module('app')
 .directive('hkDragUpload', dragUpload);

*

Directives and Filters are the only providers that have the first letter as lowercase; this is due to strict naming conventions in Directives. Angular hyphenates camelCase, so dragUpload will become <div drag-upload></div> when used on an element.

*

controllerAs: Use the controllerAs syntax inside Directives as well

// avoid
function dragUpload () {
 return {
 controller: function ($scope) {

[source]
----
}
----

 };
}
angular
 .module('app')
 .directive('dragUpload', dragUpload);

// recommended
function dragUpload () {
 return {
 controllerAs: 'vm',
 controller: function () {

[source]
----
}
----

 };
}
angular
 .module('app')
 .directive('dragUpload', dragUpload);

Filters

*

Global filters: Create global filters using angular.filter() only. Never use local filters inside Controllers/Services

// avoid
function SomeController () {
 this.startsWithLetterA = function (items) {
 return items.filter(function (item) {
 return /^a/i.test(item.name);
 });
 };
}
angular
 .module('app')
 .controller('SomeController', SomeController);

// recommended
function startsWithLetterA () {
 return function (items) {
 return items.filter(function (item) {
 return /^a/i.test(item.name);
 });
 };
}
angular
 .module('app')
 .filter('startsWithLetterA', startsWithLetterA);

*

This enhances testing and reusability

Routing resolves

*

Promises: Resolve Controller dependencies in the $routeProvider (or $stateProvider for ui-router), not the Controller itself

// avoid
function MainController (SomeService) {
 var _this = this;
 // unresolved
 _this.something;
 // resolved asynchronously
 SomeService.doSomething().then(function (response) {
 _this.something = response;
 });
}
angular
 .module('app')
 .controller('MainController', MainController);

// recommended
function config ($routeProvider) {
 $routeProvider
 .when('/', {
 templateUrl: 'views/main.html',
 resolve: {
 // resolve here
 }
 });
}
angular
 .module('app')
 .config(config);

*

Controller.resolve property: Never bind logic to the router itself. Reference a resolve property for each Controller to couple the logic

// avoid
function MainController (SomeService) {
 this.something = SomeService.something;
}

function config ($routeProvider) {
 $routeProvider
 .when('/', {
 templateUrl: 'views/main.html',
 controllerAs: 'vm',
 controller: 'MainController'
 resolve: {
 doSomething: function () {
 return SomeService.doSomething();
 }
 }
 });
}

// recommended
function MainController (SomeService) {
 this.something = SomeService.something;
}

MainController.resolve = {
 doSomething: (SomeService) =&gt; {
 return SomeService.doSomething();
 }
};

function config ($routeProvider) {
 $routeProvider
 .when('/', {
 templateUrl: 'views/main.html',
 controllerAs: 'vm',
 controller: 'MainController'
 resolve: MainController.resolve
 });
}

*

This keeps resolve dependencies inside the same file as the Controller and the router free from logic

Publish and subscribe events

*

$scope: Use the $emit and $broadcast methods to trigger events to direct relationship scopes only

// up the $scope
$scope.$emit('customEvent', data);

// down the $scope
$scope.$broadcast('customEvent', data);

*

$rootScope: Use only $emit as an application-wide event bus and remember to unbind listeners

// all $rootScope.$on listeners
$rootScope.$emit('customEvent', data);

*

Hint: Because the $rootScope is never destroyed, $rootScope.$on listeners aren’t either, unlike $scope.$on listeners and will always persist, so they need destroying when the relevant $scope fires the $destroy event

// call the closure
var unbind = $rootScope.$on('customEvent'[, callback]);
$scope.$on('$destroy', unbind);

*

For multiple $rootScope listeners, use an Object literal and loop each one on the $destroy event to unbind all automatically

var unbind = [
  $rootScope.$on('customEvent1'[, callback]),
  $rootScope.$on('customEvent2'[, callback]),
  $rootScope.$on('customEvent3'[, callback])
];
$scope.$on('$destroy',  () => {
  unbind.forEach(function (fn) {
    fn();
  });
});

Performance

*

One-time binding syntax: In newer versions of Angular (v1.3.0-beta.10+), use the one-time binding syntax {{ ::value }} where it makes sense

// avoid
<h1>{{ vm.title }}</h1>

// recommended
<h1>{{ ::vm.title }}</h1>

Why? : Binding once removes the watcher from the scope's`$$watchers`array after the`undefined` variable becomes resolved, thus improving performance in each dirty-check

*

Consider $scope.$digest: Use $scope.$digest over $scope.$apply where it makes sense. Only child scopes will update

$scope.$digest();

Why? : $scope.$apply will call $rootScope.$digest, which causes the entire application $$watchers to dirty-check again. Using $scope.$digest will dirty check current and child scopes from the initiated $scope

Angular wrapper references

*

$document and $window: Use $document and $window at all times to aid testing and Angular references

// avoid
function dragUpload () {
 return {
 link: function ($scope, $element, $attrs) {
 document.addEventListener('click', function () {

[source]
----
  });
}
----

 };
}

// recommended
function dragUpload () {
 return {
 link: ($scope, $element, $attrs, $document) =>  {
 $document.addEventListener('click', () => {

[source]
----
  });
}
----

 };
}

*

$timeout and $interval: Use $timeout and $interval over their native counterparts to keep Angular’s two-way data binding up to date

// avoid
function dragUpload () {
 return {
 link: function ($scope, $element, $attrs) {
 setTimeout(function () {
 //
 }, 1000);
 }
 };
}

// recommended
function dragUpload ($timeout) {
 return {
 link: ($scope, $element, $attrs) => {
 $timeout(function () {
 //
 }, 1000);
 }
 };
}

Comment standards

*

jsDoc: Use jsDoc syntax to document function names, description, params and returns. INFO: When jsDoc is present, some IDEs like WebStorm will use that documentation to assist in code completion and help.

/**
 * @name SomeService
 * @desc Main application Controller
 */
function SomeService (SomeService) {

/**
 * @name doSomething
 * @desc Does something awesome
 * @param {Number} x - First number to do something with
 * @param {Number} y - Second number to do something with
 * @returns {Number}
 */
 this.doSomething = function (x, y) {
 return x * y;
 };

}
angular
 .module('app')
 .service('SomeService', SomeService);