angular1 guides

Angular 1 Guidelines

Table of Contents

Introduction

Angular *1.** ecosystem is huge and it does not provides any structure about the names or the project source tree structure. It provides many functionalities like services, directives, configs, without any structure guide. If someone start developing an Angular project without any structure, soon, he will stuck on the different file and the inclusion ordering mess.

Our proposal follows the following file structure.

app
├── sections
│   ├── leaf-api
│   │   ├── leaf-api.ts
│   │   ├── leaf-api-controller.js
│   │   ├── leaf-api.html
│   │   ├── leaf-api.less
│   │   ├── leaf-api_test.js
├── components
│   ├── tree-object
│   │   ├── tree-object.js
│   │   ├── tree-object-directive.js
│   │   ├── tree-object.html
│   │   ├── tree-object.less
│   │   ├── tree-object_test.js
│   ├──  services
│   │   ├── api-service.js
│   │   ├── services.js
├── less
│   ├── fonts.less
│   ├── scopes.less
│   ├── app.less
├── assets
├── app.js
├── index.mustache

Components

A component could contain:
* Directive
* Service
* Filter

The file that contains a component should prostfixed by the component type.

tree-object-directive.js

Each component should contain its own module definition file.

tree-object.js

Each component included in one folder, each folder should only located on components folder.

tree-object
├── ...

Each component should contain the unit test file postfixed by _test.js

tree-object_test.js

Directives

Directives should also contain the html, tree-object-directive.html and the less file that contains the stylesheet only for the specific component, tree-object-directive.less.

The less file inside a component will be namespaced be the directive name. Hence, the less code inside that file will apply only in the child of the specific directive.

// tree-object-directive.less
pa-tree-object {
  // css rules
  div {
    // css rules
  }
  ...
}

Sections

Sections can be defined as entire segments of the application. Conceptually, it could be an entire page. The section follows the same file structure with components. The difference is that it contains only -controller.js files.

Naming

  • Folders and file names should be kebab case. app/components/tree-object/tree-object.js
  • Angular module names should follow the file structure on camelCase, dot separated and prefix by the project acronym. pa.app.components.treeObject

Module registration

Directive

What we always do on a directive:
* Use bindToController: true in order to directly attach the function’s variables on the view without the usage of $scope.
* Use controllerAs: controllerAsName. The controller of the directive should be on camel case, postfixed by Ctrl
* Always create an isolated scope, scope: {}
* Try to always restrict: 'E'

Considering the above, we expect that the template of a directive file will be the following:

  angular.module('pa.app.components.treeObject')
  .directive('paTreeObject', function() {
    return {
      templateUrl: 'components/tree-object/tree-object.html'
      controller: TreeObject,
      restrict: 'E',
      scope: {},
      bindToController: true,
      controllerAs: 'treeObjectCtrl'
    };
  });
  /*@ngInject*/
  function TreeObject() {
    // controller code
  }

We assume that this boilerplate code will be repeated on every directive registration. To avoid this, we created a file in the utilities of the project that handles this boilerplate code and makes the directive registration more friendly. The idea is to use only the necessary fields:

pa.directive('pa.app.components.treeObject', 'treeObject', {
     templateUrl: 'components/tree-object/tree-object.html'
  },/*@ngInject*/
  function() {
    // controller code
  }
);

A straight forward implementation for the directive registration could be following:

function createDirective(moduleName, directiveName, options, controller) {
  var ctrl = controller || options;
  var opts = controller ? options : {};

  var prefixedDirectiveName = 'pa' + _.upperFirst(directiveName);
  var controllerAsName = directiveName + 'Ctrl';
  var defaultDirectiveParams = {
    controller: ctrl,
    restrict: 'E',
    scope: {},
    bindToController: true,
    controllerAs: controllerAsName
  };

  _.assignIn(defaultDirectiveParams, opts);

  angular.module(moduleName)
    .directive(prefixedDirectiveName, function() { 
      return defaultDirectiveParams; 
    });
}

Controller

The controller follows the same registration pattern with the directive. The implementation of the registration function depicted on #Angular-registration functions.

pa.controller('pa.app.components.treeFamily', 'treeFamily', /*@ngInject*/ function() {
  // controller code
});

and the html root element for the specific controller should contain the ng-controller attribute.

<div class="page-tree-family" ng-controller="treeFamilyController as treeFamilyCtrl">
</div>

Controller Function

The controller incarnates the logic of the directive/controller. When we use the controller with the bindToController: true option, the this keyword of the function contain all the variables that binds on the view. The requirement is a straightforward structure that will easily indicates the variable and the methods that are exported on the view (attached on function’s this). Ideally, we would like to see the exported functionality on the beginning of the file, and scroll down only to see the implementation. The proposal is the following:

/*@ngInject*/
function($service1, $service2) {
  var ctrl = this;
  ctrl.var1 = false;
  ctrl.var2 = 1;
  ctrl.method1 = method1;
  ctrl.method2 = method2;

  function method1() {
    ++ctrl.var2;
  }

  function method2() {
    ctrl.var1 = true;
  }
}

Why we use this structure?

We always bind the this variable on the ctrl variable because in most of the cases we want to apply our functionality on the exported variable. We use ctrl as conversion because essentially the function is the controller function.

We assign firstly the variables and secondly the methods in the beginning of the controller. Thus, it is very easy to recognize the functionality that is exported on the view. If the user needs more details about the implementation of the exported functions, he can scroll on the method implementation.

Service

wg.service('wg.app.components.services', 'servicesApi', /*@ngInject*/ function() {
  // service code
});

Service function

The service follows the same pattern with the controllers/directives function. We return the exported values on the beginning of the function and we implement the methods below.

/*@ngInject*/
function(service1, service2) {
  var service = {
    do1: do1,
    do2: do2
  };

  return service;

  function do1() {

  }

  function do2() {

  }
}

Ng Inject

Normally, in order to avoid the common minification problem on Angular, Angular proposes the syntax:


['service1', 'service2', function(service1, service2) { }]

This syntax guaranties that the arguments of the array will not minified on the distribution build. Our problem is that we hate the boilerplate code, and this the previous syntax will add a lot of code following the same pattern on each function that works with dependency injection. We take advantage of the gulp ng annotate. Hence, we just add the /*@ngInject*/ comment before each function. During the gulp build process, the ng-annotate will replace all the /*@ngInject*/ definitions with the array syntax.

/*@ngInject*/
function(service1, service2) {

}

Angular registration functions

Directive is on #Directive section

function createController(moduleName, controllerName, controller) {
  angular.module(moduleName)
    .controller(`${controllerName}Controller`, controller);
}

function createService(moduleName, serviceName, controller) {
  const prefixedServiceName = 'wg' + _.upperFirst(serviceName);

  angular.module(moduleName)
    .factory(prefixedServiceName, controller);
}

function createFilter(moduleName, filterName, controller) {
  const prefixedFilterName = 'wg' + _.upperFirst(filterName);

  angular.module(moduleName)
    .filter(prefixedFilterName, controller);
}

About the author: John Apostolidis

Frontend developer

Leave a Reply

Your email address will not be published.