This article is based on Todd Motto’s AngularJS styleguide. The original article can be checked on https://github.com/toddmotto/angularjs-styleguide.

Essential rules

This is a quick overview of all essential rules for frontend development:

  • All frontend sources should be contained in the censhare-Client5/web/src directory.
  • All frontend sources should be TypeScript. .js files are not allowed in the source directory.
  • There should be no .js or .js.map files in the sources directory and .ts files should not be compiled inside the sources directory. In case that .ts files are compiled in sources for a special reason, please make sure to not commit any of the compiled results.
  • cs.pkg.json files should NOT have deps, include or styles fields (Read more about cs.pkg.json here)
  • New features which are added to the project should respect the ES6 module format.
  • Components, constants, etc. should be all contained in separate files: desired-name.component.ts, desired-name.module.ts
  • A module definition should be contained in a separate file: desired-name.module.ts
  • The dependencies section of a module definition should consist only of imported module names. Inline-written strings are not allowed.
  • Files should not be named with camelCase. (Read more about file naming conventions here)
  • Any committed changes should be valid in terms of TypeScript compilation and TSLint validity.
  • Enabling TSLint and stylelint is needed to ensure the correctness of your TypeScript and Sass style.
  • Git hooks will prevent committing anything invalid with respect to the TSLint rules. Also live tests will treat incorrect styles as error.
  • Styles should be imported statically by adding less imports to src/ng1/styles.less.
  • Relative import paths are preferred to ones using @cs/... shortcuts.

Note about code-quality tools

Both TypeScript, TSLint and stylelint are developer dependencies, defined in the censhare-Client5/web/package.json file. Dependencies are automatically installed in node-modules, a gitignored folder on server build. Dependencies can also be manually installed by executing the yarn install command in the censhare-Client5/web directory.

Frontend developers should configure their developer environments/IDEs to use those installations of TypeScript, TSLint, and stylelint and make sure that we are not using the ones bundled with the IDE (@WebStorm) or installed globally in the system. A good practice is to avoid the global installation of those tools.

File structure overview

General structure

Files related to functionality of the censhare Web client are located in censhare-Client5 directory.

censhare-Client5/
├─ .idea/          │
├─ .settings/      ├─ Shared code style and environment settings for IDEs
├─ .vscode/        │
├─ bin/           - WTF, do we even need it / does anybody use it? //TODO @kko: replace this message ? :D
├─ src/           - Meant for future separation of web and java sources - java sources will go here
├─ tools/         - Tools, scripts and resources used in development, but not bundled to product
│  ├── ide-settings         - Recommended configuration exports for IDEs
│  ├── scripts              - Helper scripts to automate some maintenance and development tasks
│  └── ui-resources         - Resources needed to modify custom icon fonts
├─ web/                     - Root for frontend sources
│  ├── 3rd-party            - Deprecated libraries injected out of the packet manager's scope. Soon to be gone
│  ├── img                  - Image resources
│  ├── sass                 - Global stylesheet resources
│  ├── src                  - Application sources
│  ├── styleguide           - Styleguide sources
│  ├── styleguide-old       - Old styleguide sources. Deprecated
│  ├── styles               - Font resources
│  ├── index.html           - Entry HTML document loaded by the browser
│  ├── cs.pkg.json          - Definition of base censhare module
│  ├── package.json         - Definition of the application’s npm package
│  ├── system.config.json   - SystemJS configuration
│  ├── tsconfig.json        - TypeScript configuration for the application
│  ├── tsconfig-doc.json    - Typescript configuration for the compodoc documantation
│  ├── tslint.json          - TSLint configuration
│  ├── .gitignore           - Git ignore configuration for frontend sources
│  ├── .stylelintrc         - stylelint configuration
│  └── yarn.lock            - Ensures strict version compatibility of npm packages

Note: After switching branches, please check for any “ghost leftovers” in the directory structure, for example in censhare-Client5/web/cs which was being used in the old structure, but should not exist anymore.

Frontend sources structure

Modules of the censhare Web are grouped according to the product functionality.

web /src
├─ ng1 - Code compliant with AngularJS 1.x goes here
│  ├── Client               - Code providing product functionalities
│  │   ├── ADMIN            - Administrative and diagnostic functionalities
│  │   ├── base             - Reusable UI components
│  │   ├── collaboration    - Communication and interaction between users
│  │   ├── DAM              - Digital asset management
│  │   ├── Demo             - Examples and guides
│  │   ├── flatplan         - Flatplan - issue planning editor
│  │   ├── frames           - Main view modes of the web client
│  │   ├── inquiries        - Reports and statistics
│  │   ├── MRM              - Resource Management, Project Planning
│  │   ├── multieditor      - Multieditor functionality
│  │   ├── OC               - Online Channel
│  │   ├── pim              - Product Information Management
│  │   ├── social           - Social media integration
│  │   └── W2P              - Web to print - document, layout and media editors
│  └──Framework             - censhare framework code, backend-of-the-frontend


Module sources structure

moduleDirectory
├─ translations
│  ├── de.utf8
│  ├── en.utf8
│  ├── fr.utf8
│  ├── it.utf8
│  └── ja.utf8
├─ stuff-viewer-widget.module.ts
├─ stuff-viewer-widget.component.ts
├─ stuff-viewer-widget-headless.constant.ts
├─ stuff-viewer-widget.component.html
├─ stuff-viewer-widget.scss
├─ cs.pkg.json


File naming conventions

File naming conventions should be kept simple and lowercased. For components, use the component’s name with name of the type of file in the middle.

  • Example: stuff-viewer.*.ts*, stuff-viewer-grid.*.ts

Module definition files should contain module as type file name. This convention allows to import the module by directory name:

  • Example: stuff-viewer.module.ts

stuff-viewer.module.ts
stuff-viewer.component.ts
stuff-viewer.service.ts
stuff-viewer.directive.ts
stuff-viewer.filter.ts
stuff-viewer.spec.ts
stuff-viewer.component.html
stuff-viewer.scss


Module system

Terms

Often it is confusing to verify what is, and what is not called a “module”. In censhare we have three different types of modules and the following terms can be used to create a distinction between module types:

  • Angular modules - are logical forms of code organization defined by calling the angular.module function in the code.

  • ES6 modules - is a commonly used name referring to ES2015+ import and export features.

  • censhare modules - refer to the configuration contained in cs.pkg.json files, that is used to bind a single angular module with a certain backend functionality.

Simple module

There are three types of things we can import and we should import them in the following order:

  1. Global module imports
  2. Empty line
  3. Dependency module imports
  4. Sub-modular imports of providers (components, constants, etc.)

In the example below, we call angular.module and pass the module name and the array of dependency modules as arguments. Then we use .component() on the module object to add the angular component to the module. Then we get the .name property of the created module and assign it to csStuffViewerRowModule which we export.

Why? Now we can import this module as dependency to other modules by using this exported variable containing a string name, instead of using the string straightforward. This means all the module dependencies are strictly checked by the TypeScript compiler and we don’t need to worry about any typos and renaming problems.

/* ----- csStuffViewer/csStuffViewerRow/stuff-viewer-row.module.ts ----- */
 
import * as angular from 'angular' ; // global import
 
import { csDependencyForStuffViewerRowModule } from `../../location-of-the/dependency-module-file.module` // dependency import
import { stuffViewerRowComponent } from './stuff-viewer-row.component' ; // sub-modular import
 
export const csStuffViewerRowModule = angular
   .module( 'csStuffViewerRow' , [
     csDependencyForStuffViewerModule
   ])
   .component(stuffViewerRowComponent)
   .name;


Exports and naming conventions

  • Exported module name constants should be prefixed with ‘cs’ and suffixed by ‘Module’:

    export const csStuffViewerModule: string = angular.module...`

  • Exported angular providers within the module (components, constants, etc.) should NOT be prefixed with ‘cs’, but can be suffixed by the sub-part category if their names are probable to conflict with others. Their name should contain information about the parent module as well.

    • Example: .component('csStuffViewerRow', stuffViewerRowComponent)

Why? It allows us to quickly distinguish a module export from an ex. component export, and saves us a lot of pain when using automatic imports, intelligence and other IDE tools.

Example - Widget module
/* ----- csStuffViewer/stuff-viewer.module.ts ----- */ 
 
import * as angular from 'angular' ;     // global import first 
 
import { csDependencyForStuffViewerModule } from `../../location-of-the/dependency-module-file.module`;     // dependency imports follow 
import { csStuffViewerRowModule } from `./csStuffViewerRow/stuff-viewer-row.module.ts` // dependency import
import { stuffViewerComponent } from './stuff-viewer.component' ; // sub-modular import 
import { stuffViewerHeadComponent } from './stuff-viewer-head.component' ; // sub-modular import 
import { StuffViewerHeadless } from './stuff-viewer-headless.constant' ; // sub-modular import 
 
export const csStuffViewerModule: string = angular       // exporting module name 
     .module( 'csDependencyForStuffViewerModule' , [       // calling angular.module with name and dependency array argumants 
         csDependencyForStuffViewerModule 
         csStuffViewerRowModule 
     ]) 
     .component( 'stuffViewerComponent' , assetAnnotationsOverviewWidgetRowComponent)       // adding components and constants to module 
     .component( 'stuffViewerComponent' , assetAnnotationsOverviewWidgetComponent) 
     .constant(StuffViewerHeadless) 
     .name;      // getting the module name to export


cs.pkg.json

A cs.pkg.json file contains information needed to bind some frontend functionality with the backend. Such files include mainly censhare Framework implementations, like widgets, dialogs, etc.

A cs.pkg.json file should not contain deps, includes or styles fields. These configurations were used in previous versions of censhare, but now are handled purely by frontend package management.

Example
/* ----- csStuffViewer/cs.pkg.json ----- */ 
 
{ 
   "name" : "csStuffViewer" , 
   "since" : "2018.1" , 
   "version" : "1.0.0" , 
   "filename" : "stuff-viewer.module" , 
   "implementations" : [ 
     { 
       "type" : "csWidget" , 
       "name" : "csLayoutElementsWidget" , 
       "properties" : { 
         "developmentOnly" : true , 
         "contentComponentName" : "csLayoutElementsWidgetMainComponent" , 
         "configComponentName" : "csLayoutElementsWidgetConfig" , 
         "headComponentName" : "csLayoutElementsWidgetHeadComponent" , 
         "headlessDataManager" : "csLayoutElementsWidgetHeadless" , 
         "widgetInfo" : { 
           "icon" : "cs-icon-edit" , 
           "title" : "csLayoutElementsWidget.title" , 
           "description" : "csLayoutElementsWidget.description" 
         }, 
         "requiredApplications" : [] 
       } 
     }, 
     { 
       "type" : "csHeadlessWidgetDataManager" , 
       "name" : "csLayoutElementsWidgetHeadless" , 
       "constructor" : "csLayoutElementsWidgetHeadless" 
     } 
   ], 
   "translations" : [ 
     "translation" 
   ] 
}


Translations

Translations are provided by .utf8 files contained within a path defined in the cs.pkg.json file of the module. When developing a feature, you must provide only the English and German translations. For other languages, do not modify/create files at all.

Styles

Styles are contained in .scss files. They need to be manually imported by adding Sass imports to the src/ng1/_styles.scss file. If you have multiple stylesheets in a module, you can also combine them by importing them into a single file and then importing that file to src/ng1/_styles.scss.

AngularJS providers

This section describes the recommended TypeScript syntax to use with AngularJS providers, like:

The use of factory, value, and provider recipes is not recommended.

Components

Component theory

Components are essentially templates with a controller. They are not directives, nor should directives be replaced with components, unless “template directives” are updated with controllers, which are best suited as a component. Additionally, components contain bindings that define inputs and outputs for data and events, lifecycle hooks and the ability to use one-way data flow and event Objects to get data back up to a parent component. These are the new defacto standard in Angular 1.5 and above. Everything template and controller driven that we create will likely be a component, which may be a stateful, stateless or routed component. You can think of a “component” as a complete piece of code, not just the .component() definition Object. Let’s explore some best practices and advisories for components, then dive into how you should be structuring them via stateful, stateless and routed component concepts.

Supported properties

These are the supported properties for .component() that you can/should use:

Property

Support

bindingsYes, use '@', '<', '&' only
controllerYes
controllerAsYes, default is $ctrl
requireYes (new Object syntax)
templateYes
templateUrlYes
transcludeYes


Controllers

Controllers should only be used alongside components, never anywhere else. If you feel you need a controller, what you really need is likely a stateless component to manage that particular piece of behaviour.

Here are some advisories for using Class for controllers:

  • If you need to access the lexical scope, use arrow functions
  • Bind all public functions directly to the Class
  • Make use of the appropriate lifecycle hooks, $onInit, $onChanges, $postLink and $onDestroy
    • Note: $onChanges is called before $onInit, see resources section for articles detailing this in more depth
  • Use require alongside $onInit to reference any inherited logic
  • Do not override the default $ctrl alias for the controllerAs syntax, therefore do not use controllerAs anywhere.

One-way dataflow and Events

One-way dataflow was introduced in Angular 1.5, and redefines component communication.

Here are some advisories for using one-way dataflow:

  • In components that receive data, always use one-way databinding syntax '<'
  • Do not use '=' two-way databinding syntax anymore, anywhere
  • Components that have bindings should use $onChanges to clone the one-way binding data to break Objects passing by reference and updating the parent data
  • Use $event as a function argument in the parent method (see stateful example below $ctrl.addTodo($event))
  • Pass an $event: {} Object back up from a stateless component (see stateless example below this.onAddTodo).
  • Why? This mirrors Angular 2 and keeps consistency inside every component. It also makes state predictable.

Stateful components

Let’s define what we’d call a “stateful component”.

  • Fetches state, essentially communicating to a backend API through a service
  • Does not directly mutate state
  • Renders child components that mutate state
  • Also referred to as smart/container components
Example of a stateful component
/* ----- todo/todo.component.ts ----- */ 
import { TodoController } from './todo.controller' ; 
import { TodoService } from './todo.service' ; 
import { TodoItem } from '../common/model/todo' ; 
import template from './todo.component.html!text' ; 
 
export const todoComponent: angular.IComponentOptions  = { 
   controller: TodoController, 
   template 
};
Example HTML
/* ----- todo/todo.component.html ----- */ 
< div class = "todo" > 
   < todo-form 
     todo = "$ctrl.newTodo" 
     on-add-todo = "$ctrl.addTodo($event);" > 
   </ todo-form > 
   < todo-list 
     todos = "$ctrl.todos" > 
   </ todo-list > 
</ div >

The following example shows a stateful component, that fetches state inside the controller, through a service, and then passes it down into stateless child components. Notice how there are no Directives being used such as ng-repeat and friends inside the template. Instead, data and functions are delegated into <todo-form> and <todo-list> stateless components.

/* ----- todo/todo.controller.ts ----- */ 
export class TodoController { 
   static $inject: string[] = [ 'TodoService' ]; 
   todos: TodoItem[]; 
 
   constructor( private todoService: TodoService) { } 
 
   $onInit() { 
     this .newTodo = new TodoItem( '' , false ); 
     this .todos = []; 
     this .todoService.getTodos().then(response => this .todos = response); 
   } 
   addTodo({ todo }) { 
     if (!todo) return ; 
     this .todos.unshift(todo); 
     this .newTodo = new TodoItem( '' , false ); 
   } 
} 
 
/* ----- todo/todo.module.ts ----- */ 
import angular from 'angular' ; 
import { todoComponent } from './todo.component' ; 
 
 
export const TodoModule = angular 
   .module( 'todo' , []) 
   .component( 'todo' , todoComponent) 
   .name; 
 
/* ----- todo/todo.service.ts ----- */ 
export class TodoService { 
   static $inject: string[] = [ '$http' ]; 
 
   constructor( private $http: angular.IHttpService) { } 
 
   getTodos() { 
     return this .$http.get( '/api/todos' ).then(response => response.data); 
   } 
} 
 
/* ----- common/model/todo.ts ----- */ 
export class TodoItem { 
     constructor( 
         public title: string, 
         public completed: boolean ) { } 
}


Stateless components

Let’s define what we’d call a “stateless component”.

  • Has defined inputs and outputs using bindings: {}
  • Data enters the component through attribute bindings (inputs)
  • Data leaves the component through events (outputs)
  • Mutates state, passes data back up on-demand (such as a click or submit event)
  • Doesn’t care where data comes from, it’s stateless
  • Are highly reusable components
  • Also referred to as dumb/presentational components

An example of a stateless component (let’s use <todo-form> as an example), complete with it’s low-level module definition (this is only for demonstration, so some code has been omitted for brevity):

Example - stateless component
/* ----- todo/todo-form/todo-form.component.html ----- */ 
< form name = "todoForm" ng-submit = "$ctrl.onSubmit();" > 
   < input type = "text" ng-model = "$ctrl.todo.title" > 
   < button type = "submit" >Submit</ button > 
</ form >

Note how the <todo-form> component fetches no state, it simply receives it, mutates an Object via the controller logic associated with it, and passes it back to the parent component through the property bindings. In this example, the $onChanges lifecycle hook makes a clone of the initial this.todo binding Object and reassigns it, which means the parent data is not affected until we submit the form, alongside one-way data flow new binding syntax '<'.

/* ----- todo/todo-form/todo-form.component.ts ----- */ 
import { TodoFormController } from './todo-form.controller' ; 
import template from './todo-form.component.html!text' ; 
 
export const todoFormComponent: angular.IComponentOptions = { 
   bindings: { 
     todo: '<' , 
     onAddTodo: '&' 
   }, 
   controller: TodoFormController, 
   template 
}; 
 
/* ----- todo/todo-form/todo-form.controller.ts ----- */ 
import { Event } from '../common/event' ; 
 
export class TodoFormController { 
 
   constructor() {} 
   $onChanges(changes) { 
     if (changes.todo) { 
       this .todo = Object.assign({}, this .todo); 
     } 
   } 
   onSubmit() { 
     if (! this .todo.title) return ; 
     this .onAddTodo( new Event({ 
         todo: this .todo 
       })); 
   } 
} 
 
/* ----- todo/todo-form/index.ts ----- */ 
import angular from 'angular' ; 
import { todoFormComponent } from './todo-form.component' ; 
 
export const TodoFormModule = angular 
   .module( 'todo.form' , []) 
   .component( 'todoForm' , todoFormComponent) 
   .name;


Importing templates

To avoid writing inline templates we can use import statement on static HTML file - we just need to always tag the import with !text suffix to maintain compatibility with our build tools.

import template from './todo-form.component.html!text' ; 
 
(...) 
 
export const todoFormComponent: angular.IComponentOptions = { 
   bindings: { 
     todo: '<' , 
     onAddTodo: '&' 
   }, 
   controller: TodoFormController, 
   template 
};


groupedInjections helper decorator

@groupedInjections decorator applied to controller class allows us to easily separate injected providers from other properties used in the controller. To use it, we need to decorate a class and declare getDependencies function returning injected providers mapped as an object.

Now we can call getDependencies function from any method, getting only those injections that we need in certain scopes. Using it with object destructuring syntax increases code readability even more.

@groupedInjections 
export class AssetAnnotationsOverviewWidgetController { 
     public static $inject: string[] = [ 
         '$scope' , 
         '$document' , 
         'pageInstance' 
     ]; 
 
     private getDependencies: () => { 
         $scope: ng.IScope; 
         $document: ng.IDocumentService; 
         pageInstance: IPageInstance; 
     }; 
 
     (...) 
 
     public functionThatUsesInjectedFeatures = () => { 
         const {pageInstance} = this .getDependencies(); 
         const pageStatus = pageInstance.getStatus(); 
     }


Directives

Directive theory

Directives gives us template, scope bindings, bindToController, link and many other things. The usage of these should be carefully considered now .component() exists. Directives should not declare templates and controllers anymore, or receive data through bindings. Directives should be used solely for decorating the DOM. By this, it means extending existing HTML - created with .component(). In a simple sense, if you need custom DOM events/APIs and logic, use a Directive and bind it to a template inside a component. If you need a sensible amount of DOM manipulation, there is also the $postLink lifecycle hook to consider, however this is not a place to migrate all your DOM manipulation to, use a Directive if you can for non-Angular things.

Here are some advisories for using Directives:

  • Never use templates, scope, bindToController or controllers
  • Always restrict: 'A' with Directives
  • Use compile and link where necessary
  • Remember to destroy and unbind event handlers inside $scope.$on('$destroy', fn);

Recommended properties

Due to the fact directives support most of what .component() does (template directives were the original component), I’m recommending limiting your directive Object definitions to only these properties, to avoid using directives incorrectly:

Property

Use it?

Why

bindToControllerNoUse bindings in components
compileYesFor pre-compile DOM manipulation/events
controllerNoUse a component
controllerAsNoUse a component
link functionsYesFor pre/post DOM manipulation/events
multiElementYesSee docs
priorityYesSee docs
requireNoUse a component
restrictYesDefines directive usage, always use 'A'
scopeNoUse a component
templateNoUse a component
templateNamespaceYes (if you must)See docs
templateUrlNoUse a component
transcludeNoUse a component


Constants or Classes

There are a few ways to approach using TypeScript and directives, either with an arrow function and easier assignment, or using an TypeScript Class. Choose what’s best for you or your team, keep in mind Angular 2 uses Class.

Example using a constant with an Arrow function an expression wrapper returning an Object literall
/* ----- todo/todo-autofocus.directive.ts ----- */
import angular from 'angular';
 
export const todoAutoFocus = ($timeout: angular.ITimeoutService) => (<angular.IDirective> {
  restrict: 'A',
  link($scope, $element, $attrs) {
    $scope.$watch($attrs.todoAutoFocus, (newValue, oldValue) => {
      if (!newValue) {
        return;
      }
      $timeout(() => $element[0].focus());
    });
  }
});
 
todoAutoFocus.$inject = ['$timeout'];
 
/* ----- todo/index.ts ----- */
import angular from 'angular';
import { todoComponent } from './todo.component';
import { todoAutoFocus } from './todo-autofocus.directive';
 
export const TodoModule = angular
  .module('todo', [])
  .component('todo', todoComponent)
  .directive('todoAutoFocus', todoAutoFocus)
  .name;
using TypeScript Class (note manually calling new TodoAutoFocus when registering the directive) to create the Object
/* ----- todo/todo-autofocus.directive.ts ----- */
import angular from 'angular';
 
export class TodoAutoFocus implements angular.IDirective {
  static $inject: string[] = ['$timeout'];
  restrict: string;
 
  constructor(private $timeout: angular.ITimeoutService) {
    this.restrict = 'A';
  }
 
  link($scope, $element: HTMLElement, $attrs) {
    $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => {
      if (!newValue) {
        return;
      }
 
      $timeout(() => $element[0].focus());
    });
  }
}
 
/* ----- todo/index.ts ----- */
import angular from 'angular';
import { TodoComponent } from './todo.component';
import { TodoAutofocus } from './todo-autofocus.directive';
 
export const TodoModule = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .directive('todoAutofocus', ($timeout: angular.ITimeoutService) => new TodoAutoFocus($timeout))
  .name;

Services

Service theory

Services are essentially containers for business logic that our components shouldn’t request directly. Services contain other built-in or external services such as $http, that we can then inject into component controllers elsewhere in our app. We have two ways of doing services, using .service() or .factory(). With TypeScript Class, we should only use .service(), complete with dependency injection annotation using $inject.

Classes for Service

Example
/* ----- todo/todo.service.ts ----- */
export class TodoService {
  static $inject: string[] = ['$http'];
 
  constructor(private $http: angular.IHttpService) { }
  getTodos() {
    return this.$http.get('/api/todos').then(response => response.data);
  }
}
 
/* ----- todo/index.ts ----- */
import angular from 'angular';
import { TodoComponent } from './todo.component';
import { TodoService } from './todo.service';
 
export const todo = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .service('TodoService', TodoService)
  .name;


Resources

For anything else, including API reference, check the Angular documentation.