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 havedeps
,include
orstyles
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 tosrc/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.
|
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.
|
Module sources structure
|
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
|
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
andexport
features.censhare modules - refer to the configuration contained in
cs.pkg.json
files, that is used to bind a singleangular 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:
- Global module imports
- Empty line
- Dependency module imports
- 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.
|
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)
- Example:
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.
/* ----- 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.
/* ----- 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 |
---|---|
bindings | Yes, use '@' , '<' , '&' only |
controller | Yes |
controllerAs | Yes, default is $ctrl |
require | Yes (new Object syntax) |
template | Yes |
templateUrl | Yes |
transclude | Yes |
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
- Note:
- Use
require
alongside$onInit
to reference any inherited logic - Do not override the default
$ctrl
alias for thecontrollerAs
syntax, therefore do not usecontrollerAs
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 belowthis.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
/* ----- 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 };
/* ----- 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):
/* ----- 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 |
---|---|---|
bindToController | No | Use bindings in components |
compile | Yes | For pre-compile DOM manipulation/events |
controller | No | Use a component |
controllerAs | No | Use a component |
link functions | Yes | For pre/post DOM manipulation/events |
multiElement | Yes | See docs |
priority | Yes | See docs |
require | No | Use a component |
restrict | Yes | Defines directive usage, always use 'A' |
scope | No | Use a component |
template | No | Use a component |
templateNamespace | Yes (if you must) | See docs |
templateUrl | No | Use a component |
transclude | No | Use 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
.
/* ----- 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;
/* ----- 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
/* ----- 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
- Understanding the .component() method
- Using “require” with $onInit
- Understanding all the lifecycle hooks, $onInit, $onChange, $postLink, $onDestroy
For anything else, including API reference, check the Angular documentation.