Opinionated Angular style guide for Element teams. This styleguide is based on the Angular styleguide created by @john_papa
The purpose of this style guide is to provide guidance on building Element applications by showing the conventions we use and, more importantly, why we chose them.
- Single Responsibility
- Modules
- Controllers
- Services
- Data Services
- Directives
- Resolving Promises for a Controller
- Manual Annotating for Dependency Injection
- Minification and Annotation
- Exception Handling
- Naming
- Application Structure LIFT Principle
- Application Structure
- Modularity
- Startup Logic
- Angular $ Wrapper Services
- Testing
- Animations
- Comments
- Filters
- Angular Docs
- Contributing
- License
[Style Y001]
- Define 1 component per file.
The following example defines the app module and its dependencies, defines a controller, and defines a factory all in the same file.
/* avoid */
angular
.module('app', ['ngRoute'])
.controller('SomeController', SomeController)
.factory('someFactory', someFactory);
class SomeController() { }
class someFactory() { }The same components are now separated into their own files.
/* recommended */
// app.module.js
angular
.module('app', ['ngRoute']);/* recommended */
// someController.js
angular
.module('app')
.controller('SomeController', SomeController);
class SomeController() { }/* recommended */
// someFactory.js
angular
.module('app')
.factory('someFactory', someFactory);
class someFactory() { }[Style Y020]
- Use unique naming conventions with separators for sub-modules.
Why?: Unique names help avoid module name collisions. Separators help define modules and their submodule hierarchy. For example app may be your root module while app.dashboard and app.users may be modules that are used as dependencies of app.
[Style Y021]
- Declare modules without a variable using the setter syntax.
Why?: With 1 component per file, there is rarely a need to introduce a variable for the module.
/* avoid */
var app = angular.module('app', [
'ngAnimate',
'ngRoute',
'app.shared',
'app.dashboard'
]);Instead use the simple setter syntax.
/* recommended */
angular
.module('app', [
'ngAnimate',
'ngRoute',
'app.shared',
'app.dashboard'
]);[Style Y022]
- When using a module, avoid using a variable and instead use chaining with the getter syntax.
Why?: This produces more readable code and avoids variable collisions or leaks.
/* avoid */
var app = angular.module('app');
app.controller('SomeController', SomeController);
class SomeController() { }/* recommended */
angular
.module('app')
.controller('SomeController', SomeController);
class SomeController() { }[Style Y023]
- Only set once and get for all other instances.
Why?: A module should only be created once, then retrieved from that point and after.
/* recommended */
// to set a module
angular.module('app', []);
// to get a module
angular.module('app');[Style Y030]
- Use the
controllerAssyntax over theclassic controller with $scopesyntax.
Why?: Controllers are constructed, "newed" up, and provide a single new instance, and the controllerAs syntax is closer to that of a JavaScript constructor than the classic $scope syntax.
Why?: It promotes the use of binding to a "dotted" object in the View (e.g. customer.name instead of name), which is more contextual, easier to read, and avoids any reference issues that may occur without "dotting".
Why?: Helps avoid using $parent calls in Views with nested controllers.
<!-- avoid -->
<div ng-controller="Customer">
{{ name }}
</div><!-- recommended -->
<div ng-controller="Customer as customer">
{{ customer.name }}
</div>[Style Y031]
-
Use the
controllerAssyntax over theclassic controller with $scopesyntax. -
The
controllerAssyntax usesthisinside controllers which gets bound to$scope
Why?: controllerAs is syntactic sugar over $scope. You can still bind to the View and still access $scope methods.
Why?: Helps avoid the temptation of using $scope methods inside a controller when it may otherwise be better to avoid them or move the method to a factory, and reference them from the controller. Consider using $scope in a controller only when needed. For example when publishing and subscribing events using $emit, $broadcast, or $on consider moving these uses to a factory and invoke from the controller.
/* avoid */
class Customer {
public static $inject = ['$scope'];
constructor($scope){
$scope.name = {};
$scope.sendMessage = this.sendMessage;
}
sendMessage() {}
}/* recommended */
class Customer {
constructor(){
this.name = {};
}
sendMessage() {}
}[Style Y035]
-
Defer logic in a controller by delegating to services and factories.
Why?: Logic may be reused by multiple controllers when placed within a service and exposed via a function.
Why?: Logic in a service can more easily be isolated in a unit test, while the calling logic in the controller can be easily mocked.
Why?: Removes dependencies and hides implementation details from the controller.
Why?: Keeps the controller slim, trim, and focused.
/* avoid */
class Order {
public static $inject = ['$http'];
public isCreditOk: boolean = false;
public total: number = 0;
constructor(private $http) {
}
checkCredit() {
var settings = {};
// Get the credit service base URL from config
// Set credit service required headers
// Prepare URL query string or data object with request data
// Add user-identifying info so service gets the right credit limit for this user.
// Use JSONP for this browser if it doesn't support CORS
return this.$http.get(settings)
.then((data) => {
// Unpack JSON data in the response object
// to find maxRemainingAmount
this.isCreditOk = this.total <= maxRemainingAmount
})
.catch((error) => {
// Interpret error
// Cope w/ timeout? retry? try alternate service?
// Re-reject with appropriate error for a user to see
});
};
}/* recommended */
class Order() {
public static $inject = ['creditService'];
public isCreditOk: boolean = false;
public total: number = 0;
constructor(private creditService) {
}
checkCredit() {
return this.creditService.isOrderTotalOk(this.total)
.then((isOk) => { this.isCreditOk = isOk; })
.catch(showError);
};
}[Style Y037]
-
Define a controller for a view, and try not to reuse the controller for other views. Instead, move reusable logic to factories and keep the controller simple and focused on its view.
Why?: Reusing controllers with several views is brittle and good end-to-end (e2e) test coverage is required to ensure stability across large applications.
[Style Y038]
-
When a controller must be paired with a view and either component may be re-used by other controllers or views, define controllers along with their routes.
Note: If a View is loaded via another means besides a route, then use the
ng-controller="Avengers as vm"syntax.Why?: Pairing the controller in the route allows different routes to invoke different pairs of controllers and views. When controllers are assigned in the view using
ng-controller, that view is always associated with the same controller.
/* avoid - when using with a route and dynamic pairing is desired */
// route-config.ts
angular
.module('app')
.config(config);
function config($stateProvider) {
$stateProvider
.state('/avengers', {
templateUrl: 'avengers.html'
});
}<!-- avengers.html -->
<div ng-controller="Avengers as vm">
</div>/* recommended */
// route-config.ts
angular
.module('app')
.config(config);
function config($stateProvider) {
$stateProvider
.state('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm'
});
}<!-- avengers.html -->
<div>
</div>[Style Y040]
-
Services are instantiated with the
newkeyword, usethisfor public methods and variables. Since services and factories are so similar and services align with Typescript classes, use a service over a factory.Note: All Angular services are singletons. This means that there is only one instance of a given service per injector.
// service
angular
.module('app')
.service('logger', logger);
class logger {
logError(msg) {
/* */
};
}[Style Y050]
- Services should have a single responsibility, that is encapsulated by its context. Once a service begins to exceed that singular purpose, a new service should be created.
[Style Y060]
-
Refactor logic for making data operations and interacting with data to a service. Make data services responsible for XHR calls, local storage, stashing in memory, or any other data operations.
Why?: The controller's responsibility is for the presentation and gathering of information for the view. It should not care how it gets the data, just that it knows who to ask for it. Separating the data services moves the logic on how to get it to the data service, and lets the controller be simpler and more focused on the view.
Why?: This makes it easier to test (mock or real) the data calls when testing a controller that uses a data service.
Why?: Data service implementation may have very specific code to handle the data repository. This may include headers, how to talk to the data, or other services such as
$http. Separating the logic into a data service encapsulates this logic in a single place hiding the implementation from the outside consumers (perhaps a controller), also making it easier to change the implementation.
/* recommended */
// dataservice factory
angular
.module('app.core')
.service('dataservice', dataservice);
class dataService {
public static $inject = ['$http', 'logger'];
constructor dataservice(private $http, private logger) {
}
getAvengers() {
return this.$http.get('/api/maa')
.then(this.getAvengersComplete)
.catch(this.getAvengersFailed);
getAvengersComplete(response) {
return response.data.results;
}
getAvengersFailed(error) {
this.logger.error('XHR Failed for getAvengers.' + error.data);
}
}Note: The data service is called from consumers, such as a controller, hiding the implementation from the consumers, as shown below.
/* recommended */
// controller calling the dataservice factory
angular
.module('app.avengers')
.controller('Avengers', Avengers);
class Avengers {
private avengers : Array<IAvenger>;
Avengers.$inject = ['dataservice', 'logger'];
constructor Avengers(dataservice, logger) {
this.avengers = [];
this.activate();
}
activate() {
return this.getAvengers().then(() => {
this.logger.info('Activated Avengers View');
});
}
getAvengers() {
return this.dataservice.getAvengers()
.then((data) => {
this.avengers = data;
return this.avengers;
});
}
}[Style Y061]
-
When calling a data service that returns a promise such as
$http, return a promise in your calling function too.Why?: You can chain the promises together and take further action after the data call completes and resolves or rejects the promise.
/* recommended */
activate() {
/**
* Step 1
* Ask the getAvengers function for the
* avenger data and wait for the promise
*/
return this.getAvengers().then(() => {
/**
* Step 4
* Perform an action on resolve of final promise
*/
this.logger.info('Activated Avengers View');
});
}
getAvengers() {
/**
* Step 2
* Ask the data service for the data and wait
* for the promise
*/
return this.dataservice.getAvengers()
.then((data) => {
/**
* Step 3
* set the data and resolve the promise
*/
this.avengers = data;
return this.avengers;
});
}[Style Y070]
-
Create one directive per file. Name the file for the directive.
Why?: It is easy to mash all the directives in one file, but difficult to then break those out so some are shared across apps, some across modules, some just for one module.
Why?: One directive per file is easy to maintain.
Note: "Best Practice: Directives should clean up after themselves. You can use
element.on('$destroy', ...)orscope.$on('$destroy', ...)to run a clean-up function when the directive is removed" ... from the Angular documentation.
/* avoid */
/* directives.ts */
angular
.module('app.widgets')
/* order directive that is specific to the order module */
.directive('orderCalendarRange', orderCalendarRange)
/* sales directive that can be used anywhere across the sales app */
.directive('salesCustomerInfo', salesCustomerInfo)
/* spinner directive that can be used anywhere across apps */
.directive('sharedSpinner', sharedSpinner);
function orderCalendarRange() {
/* implementation details */
}
function salesCustomerInfo() {
/* implementation details */
}
function sharedSpinner() {
/* implementation details */
}/* recommended */
/* index.ts */
/**
* @desc order directive that is specific to the order module
* @example <div jhc-order-calendar-range></div>
*/
import orderCalendarRange = require('./order-calendar-range');
angular
.module('sales.order')
.directive('jhcOrderCalendarRange', orderCalendarRange);
.directive('jhcSalesCustomerInfo', salesCustomerInfo);
/* recommended */
/* order-calendar-range-directive.ts */
/**
* @desc order directive that is specific to the order module at a company named Acme
* @example <div acme-order-calendar-range></div>
*/
var directive = [() => {
/* implementation details */
}]
export = directive;/* recommended */
/* customer-info-directive.ts */
/**
* @desc sales directive that can be used anywhere across the app
* @example <div jhc-sales-customer-info></div>
*/
var directive = [() => {
/* implementation details */
}];
export = directive;/* recommended */
/* spinner.directive.ts */
/**
* @desc spinner directive that can be used anywhere across apps
* @example <div jhc-shared-spinner></div>
*/
var directive = [() => {
/* implementation details */
}];
export = directive;Note: There are many naming options for directives, especially since they can be used in narrow or wide scopes. Choose one that makes the directive and its file name distinct and clear. Some examples are below, but see the [Naming](#naming) section for more recommendations.
[Style Y072]
-
When manipulating the DOM directly, use a directive. If alternative ways can be used such as using CSS to set styles or the animation services, Angular templating,
ngShoworngHide, then use those instead. For example, if the directive simply hides and shows, use ngHide/ngShow.Why?: DOM manipulation can be difficult to test, debug, and there are often better ways (e.g. CSS, animations, templates)
[Style Y073]
-
Provide jhc as a short, unique and descriptive directive prefix such as
jhcSalesCustomerInfowhich would be declared in HTML asjhc-sales-customer-info.Why?: The unique short prefix identifies the directive's context and origin.
[Style Y074]
-
When creating a directive that makes sense as a stand-alone element, allow restrict
E(custom element) and optionally restrictA(custom attribute). Generally, if it could be its own control,Eis appropriate. General guideline is allowEAbut lean towards implementing as an element when it's stand-alone and as an attribute when it enhances its existing DOM element.Why?: It makes sense.
Why?: While we can allow the directive to be used as a class, if the directive is truly acting as an element it makes more sense as an element or at least as an attribute.
Note: EA is the default for Angular 1.3 +
<!-- avoid -->
<div class="my-calendar-range"></div>/* avoid */
var directive = [() => {
var directive = {
link: link,
templateUrl: '/template/is/located/here.html',
restrict: 'C'
};
return directive;
function link(scope, element, attrs) {
/* */
}
}];
export = directive;<!-- recommended -->
<my-calendar-range></my-calendar-range>
<div my-calendar-range></div>/* recommended */
var directive = [ () => {
var directive = {
link: link,
templateUrl: '/template/is/located/here.html',
restrict: 'EA'
};
return directive;
function link(scope, element, attrs) {
/* */
}
}];
export = directive;[Style Y075]
-
Use
controller assyntax with a directive to be consistent with usingcontroller aswith view and controller pairings.Why?: It makes sense and it's not difficult.
Note: The directive below demonstrates some of the ways you can use scope inside of link and directive controllers, using controllerAs. I in-lined the template just to keep it all in one place.
Note: Regarding dependency injection, see Manually Identify Dependencies.
<div my-example max="77"></div>/* my-example-directive.ts */
var directive = [() => {
var directive = {
restrict: 'EA',
templateUrl: 'app/feature/example.directive.html',
scope: {
max: '='
},
link: linkFunc,
controller: require('./my-example-controller'),
controllerAs: 'vm',
bindToController: true // because the scope is isolated
};
return directive;
function linkFunc(scope, el, attr, ctrl) {
console.log('LINK: scope.min = %s *** should be undefined', scope.min);
console.log('LINK: scope.max = %s *** should be undefined', scope.max);
console.log('LINK: scope.vm.min = %s', scope.vm.min);
console.log('LINK: scope.vm.max = %s', scope.vm.max);
}
}];
export = directive;/* my-example-controller.ts */
class ExampleController {
public static $inject = ['$scope'];
public min: number;
public max: number;
constructor($scope) {
// Injecting $scope just for comparison
this.min = 3;
console.log('CTRL: $scope.vm.min = %s', $scope.vm.min);
console.log('CTRL: $scope.vm.max = %s', $scope.vm.max);
console.log('CTRL: vm.min = %s', vm.min);
console.log('CTRL: vm.max = %s', vm.max);
}
}
export = ExampleController;<!-- example.directive.html -->
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="vm.max"/></div>
<div>min={{vm.min}}<input ng-model="vm.min"/></div>Note: You can also name the controller when you inject it into the link function and access directive attributes as properties of the controller.
// Alternative to above example
function linkFunc(scope, el, attr, vm) {
console.log('LINK: scope.min = %s *** should be undefined', scope.min);
console.log('LINK: scope.max = %s *** should be undefined', scope.max);
console.log('LINK: vm.min = %s', vm.min);
console.log('LINK: vm.max = %s', vm.max);
}[Style Y076]
-
Use
bindToController = truewhen usingcontroller assyntax with a directive when you want to bind the outer scope to the directive's controller's scope.Why?: It makes it easy to bind outer scope to the directive's controller scope.
Note:
bindToControllerwas introduced in Angular 1.3.0.
<div my-example max="77"></div>var directive = [() => {
var directive = {
restrict: 'EA',
templateUrl: 'app/feature/example.directive.html',
scope: {
max: '='
},
controller: require('./example-controller');
controllerAs: 'vm',
bindToController: true
};
return directive;
}];
export = directive;/* my-example-controller.ts */
class ExampleController {
public min: number;
public max: number;
contructor() {
console.log('CTRL: vm.min = %s', this.min);
console.log('CTRL: vm.max = %s', this.max);
}
}<!-- example.directive.html -->
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="vm.max"/></div>
<div>min={{vm.min}}<input ng-model="vm.min"/></div>[Style Y080]
-
Resolve start-up logic for a controller in an
activatefunction.Why?: Placing start-up logic in a consistent place in the controller makes it easier to locate, more consistent to test, and helps avoid spreading out the activation logic across the controller.
Why?: The controller
activatemakes it convenient to re-use the logic for a refresh for the controller/View, keeps the logic together, gets the user to the View faster, makes animations easy on theng-vieworui-view, and feels snappier to the user.Note: If you need to conditionally cancel the route before you start using the controller, use a route resolve instead.
/* avoid */
class Avengers {
public avengers : Array<IAvenger>;
public title: string;
constructor(dataservice) {
this.avengers = [];
this.title = 'Avengers';
dataservice.getAvengers().then((data) => {
this.avengers = data;
return data;
});
}
}/* recommended */
class Avengers {
public avengers : Array<IAvenger>;
public title: string;
constructor Avengers(dataservice) {
this.avengers = [];
this.title = 'Avengers';
this.activate();
}
////////////
activate() {
return dataservice.getAvengers().then((data) => {
this.avengers = data;
return data;
});
}
}[Style Y081]
-
When a controller depends on a promise to be resolved before the controller is activated, resolve those dependencies in the
$stateProviderbefore the controller logic is executed. If you need to conditionally cancel a route before the controller is activated, use a route resolver. -
Use a route resolve when you want to decide to cancel the route before ever transitioning to the View.
Why?: A controller may require data before it loads. That data may come from a promise via a custom factory or $http. Using a route resolve allows the promise to resolve before the controller logic executes, so it might take action based on that data from the promise.
Why?: The code executes after the route and in the controller’s activate function. The View starts to load right away. Data binding kicks in when the activate promise resolves. A “busy” animation can be shown during the view transition (via
ng-vieworui-view)Note: The code executes before the route via a promise. Rejecting the promise cancels the route. Resolve makes the new view wait for the route to resolve. A “busy” animation can be shown before the resolve and through the view transition. If you want to get to the View faster and do not require a checkpoint to decide if you can get to the View, consider the controller
activatetechnique instead.
/* avoid */
angular
.module('app')
.controller('Avengers', Avengers);
class Avengers {
public movies: Array<IMovie>;
public static $inject = ['movieService'];
constructor(movieService) {
// resolved asynchronously
movieService.getMovies().then((response) => {
this.movies = response.movies;
});
}
}/* better */
// route-config.js
angular
.module('app')
.config(config);
function config($stateProvider) {
$stateProvider
.state('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm',
resolve: {
moviesPrepService: (movieService) => {
return movieService.getMovies();
}
}
});
}
// avengers.js
angular
.module('app')
.controller('Avengers', Avengers);
class Avengers {
public movies: Array<IMovie>;
public static $inject = ['moviesPrepService'];
constructor(moviesPrepService) {
this.movies = moviesPrepService.movies;
}
}Note: The example below shows the state resolve points to a named function, which is easier to debug and easier to handle dependency injection.
/* even better */
// route-config.js
angular
.module('app')
.config(config);
var config = ($stateProvider) => {
$stateProvider
.state('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm',
resolve: {
moviesPrepService: moviesPrepService
}
});
};
var moviesPrepService = (movieService) => {
return movieService.getMovies();
};
// avengers.js
angular
.module('app')
.controller('Avengers', Avengers);
class Avengers {
public movies: Array<IMovie>;
public static $inject = ['moviesPrepService'];
constructor(moviesPrepService) {
this.movies = moviesPrepService.movies;
}
}Note: The code example's dependency on `movieService` is not minification safe on its own. For details on how to make this code minification safe, see the sections on [dependency injection](#manual-annotating-for-dependency-injection) and on [minification and annotation](#minification-and-annotation).
[Style Y090]
-
Avoid using the shortcut syntax of declaring dependencies without using a minification-safe approach.
Why?: The parameters to the component (e.g. controller, factory, etc) will be converted to mangled variables. For example,
commonanddataservicemay becomeaorband not be found by Angular./* avoid - not minification-safe*/ angular .module('app') .controller('Dashboard', Dashboard); function Dashboard(common, dataservice) { }
This code may produce mangled variables when minified and thus cause runtime errors.
/* avoid - not minification-safe*/ angular.module('app').controller('Dashboard', d);function d(a, b) { }
[Style Y120]
-
Use consistent names for all components following a pattern that describes the component's feature then (optionally) its type. The recommended pattern is
feature-type.ts. There are 2 names for most assets:- the file name (
avengers-controller.js) - the registered component name with Angular (
AvengersController)
Why?: Naming conventions help provide a consistent way to find content at a glance. Consistency within the project is vital. Consistency with a team is important. Consistency across a company provides tremendous efficiency.
Why?: The naming conventions should simply help you find your code faster and make it easier to understand.
- the file name (
[Style Y121]
-
Use consistent names for all components following a pattern that describes the component's feature then (optionally) its type. The recommended pattern is
feature-type.ts.Why?: Provides a consistent way to quickly identify components.
Why?: Provides pattern matching for any automated tasks.
/** * recommended */ // controllers avengers-controller.ts __tests__/avengers-controller-spec.ts // services/factories logger-service.ts __tests__/logger-service-spec.ts // constants constants.ts // module definition module-name/index.ts // configuration avengers-config.ts // directives avenger-profile-directive.ts __tests__/avenger-profile-directive-spec.ts
[Style Y122]
-
Name test specifications similar to the component they test with a suffix of
spec.Why?: Provides a consistent way to quickly identify components.
Why?: Provides pattern matching for karma or other test runners.
/** * recommended */ avengers-controller-spec.ts logger-service-spec.ts avenger-profile-directive-spec.ts
[Style Y123]
-
Use consistent names for all controllers named after their feature. Use UpperCamelCase for controllers, as they are constructors.
Why?: Provides a consistent way to quickly identify and reference controllers.
Why?: UpperCamelCase is conventional for identifying object that can be instantiated using a constructor.
/** * recommended */ // avengers.controller.js angular .module .controller('HeroAvengersController', HeroAvengersController); class HeroAvengersController { }
[Style Y124]
-
Append the controller name with the suffix
Controller.Why?: The
Controllersuffix is more commonly used and is more explicitly descriptive./** * recommended */ // avengers-controller.ts angular .module .controller('AvengersController', AvengersController); class AvengersController { }
[Style Y125]
-
Use consistent names for all factories and services named after their feature. Use camel-casing for services and factories. Avoid prefixing factories and services with
$. Only suffix service and factories withServicewhen it is not clear what they are (i.e. when they are nouns).Why?: Provides a consistent way to quickly identify and reference factories.
Why?: Avoids name collisions with built-in factories and services that use the
$prefix.Why?: Clear service names such as
loggerdo not require a suffix.Why?: Service names such as
avengersare nouns and require a suffix and should be namedavengersService./** * recommended */ // logger-service.ts angular .module .service('logger', logger); class logger { }
/** * recommended */ // credit-service.ts angular .module .service('creditService', creditService); class creditService { } // customer.service.ts angular .module .service('customersService', customersService); class customersService { }
[Style Y126]
-
Use consistent names for all directives using camel-case. Use a short prefix to describe the area that the directives belong (some example are company prefix or project prefix).
Why?: Provides a consistent way to quickly identify and reference components.
/** * recommended */ // avenger-profile.directive.js angular .module .directive('jhcAvengerProfile', avengerProfile); // usage is <jhc-avenger-profile> </jhc-avenger-profile> function avengerProfile() { }
[Style Y140]
-
Structure your app such that you can
Locate your code quickly,Identify the code at a glance, keep theFlattest structure you can, andTry to stay DRY. The structure should follow these 4 basic guidelines.Why LIFT?: Provides a consistent structure that scales well, is modular, and makes it easier to increase developer efficiency by finding code quickly. Another way to check your app structure is to ask yourself: How quickly can you open and work in all of the related files for a feature?
When I find my structure is not feeling comfortable, I go back and revisit these LIFT guidelines
Locating our code is easyIdentify code at a glanceFlat structure as long as we canTry to stay DRY (Don’t Repeat Yourself) or T-DRY
[Style Y141]
-
Make locating your code intuitive, simple and fast.
Why?: I find this to be super important for a project. If the team cannot find the files they need to work on quickly, they will not be able to work as efficiently as possible, and the structure needs to change. You may not know the file name or where its related files are, so putting them in the most intuitive locations and near each other saves a ton of time. A descriptive folder structure can help with this.
/bower_components /client /app /avengers /blocks /exception /logger /core /dashboard /data /layout /widgets /content index.html .bower.json
[Style Y142]
-
When you look at a file you should instantly know what it contains and represents.
Why?: You spend less time hunting and pecking for code, and become more efficient. If this means you want longer file names, then so be it. Be descriptive with file names and keeping the contents of the file to exactly 1 component. Avoid files with multiple controllers, multiple services, or a mixture. There are deviations of the 1 per file rule when I have a set of very small features that are all related to each other, they are still easily identifiable.
[Style Y143]
-
Keep a flat folder structure as long as possible. When you get to 7+ files, begin considering separation.
Why?: Nobody wants to search 7 levels of folders to find a file. Think about menus on web sites … anything deeper than 2 should take serious consideration. In a folder structure there is no hard and fast number rule, but when a folder has 7-10 files, that may be time to create subfolders. Base it on your comfort level. Use a flatter structure until there is an obvious value (to help the rest of LIFT) in creating a new folder.
[Style Y144]
-
Be DRY, but don't go nuts and sacrifice readability.
Why?: Being DRY is important, but not crucial if it sacrifices the others in LIFT, which is why I call it T-DRY. I don’t want to type session-view.html for a view because, well, it’s obviously a view. If it is not obvious or by convention, then I name it.
[Style Y150]
-
Have a near term view of implementation and a long term vision. In other words, start small but keep in mind on where the app is heading down the road. All of the app's code goes in a root folder named
app. All content is 1 feature per file. Each controller, service, module, view is in its own file. All 3rd party vendor scripts are stored in another root folder and not in theappfolder. I didn't write them and I don't want them cluttering my app (bower_components,scripts,lib).Note: Find more details and reasoning behind the structure at this original post on application structure.
[Style Y152]
-
Create folders named for the feature they represent. When a folder grows to contain more than 7 files, start to consider creating a folder for them. Your threshold may be different, so adjust as needed.
Why?: A developer can locate the code, identify what each file represents at a glance, the structure is flat as can be, and there is no repetitive nor redundant names.
Why?: The LIFT guidelines are all covered.
Why?: Helps reduce the app from becoming cluttered through organizing the content and keeping them aligned with the LIFT guidelines.
Why?: When there are a lot of files (10+) locating them is easier with a consistent folder structures and more difficult in flat structures.
/** * recommended */ app/ app.module.ts app.config.ts components/ calendar-directive.ts calendar-directive.html user-profile-directive.ts user-profile-directive.html layout/ shell.html shell-controller.ts topnav.html topnav-controller.ts people/ attendees.html attendees-controller.ts people-routes.ts speakers.html speakers-controller.ts speaker-detail.html speaker-detail-controller.ts services/ data-service.ts localstorage-service.ts logger-service.ts spinner-service.ts sessions/ sessions.html sessions-controller.ts sessions-routes.ts session-detail.html session-detail-controller.ts
Note: Do not structure your app using folders-by-type. This requires moving to multiple folders when working on a feature and gets unwieldy quickly as the app grows to 5, 10 or 25+ views and controllers (and other features), which makes it more difficult than folder-by-feature to locate files.
/* * avoid * Alternative folders-by-type. * I recommend "folders-by-feature", instead. */ app/ app-module.ts app-config.ts app-routes.ts directives.ts controllers/ attendees.ts session-detail.ts sessions.ts shell.ts speakers.ts speaker-detail.ts topnav.ts directives/ calendar-directive.ts calendar-directive.html user-profile-directive.ts user-profile-directive.html services/ dataservice.ts localstorage.ts logger.ts spinner.ts views/ attendees.html session-detail.html sessions.html shell.html speakers.html speaker-detail.html topnav.html
[Style Y160]
-
Create small modules that encapsulate one responsibility.
Why?: Modular applications make it easy to plug and go as they allow the development teams to build vertical slices of the applications and roll out incrementally. This means we can plug in new features as we develop them.
[Style Y161]
-
Create an application root module whose role is pull together all of the modules and features of your application. Name this for your application.
Why?: Angular encourages modularity and separation patterns. Creating an application root module whose role is to tie your other modules together provides a very straightforward way to add or remove modules from your application.
[Style Y162]
-
Only put logic for pulling together the app in the application module. Leave features in their own modules.
Why?: Adding additional roles to the application root to get remote data, display views, or other logic not related to pulling the app together muddies the app module and make both sets of features harder to reuse or turn off.
Why?: The app module becomes a manifest that describes which modules help define the application.
[Style Y163]
-
Create modules that represent feature areas, such as layout, reusable and shared services, dashboards, and app specific features (e.g. customers, admin, sales).
Why?: Self contained modules can be added to the application with little or no friction.
Why?: Sprints or iterations can focus on feature areas and turn them on at the end of the sprint or iteration.
Why?: Separating feature areas into modules makes it easier to test the modules in isolation and reuse code.
[Style Y164]
-
Create modules that represent reusable application blocks for common services such as exception handling, logging, diagnostics, security, and local data stashing.
Why?: These types of features are needed in many applications, so by keeping them separated in their own modules they can be application generic and be reused across applications.
[Style Y165]
-
The application root module depends on the app specific feature modules and any shared or reusable modules.
Why?: The main app module contains a quickly identifiable manifest of the application's features.
Why?: Each feature area contains a manifest of what it depends on, so it can be pulled in as a dependency in other applications and still work.
Why?: Intra-App features such as shared data services become easy to locate and share from within
app.core(choose your favorite name for this module).Note: This is a strategy for consistency. There are many good options here. Choose one that is consistent, follows Angular's dependency rules, and is easy to maintain and scale.
My structures vary slightly between projects but they all follow these guidelines for structure and modularity. The implementation may vary depending on the features and the team. In other words, don't get hung up on an exact like-for-like structure but do justify your structure using consistency, maintainability, and efficiency in mind.
In a small app, you can also consider putting all the shared dependencies in the app module where the feature modules have no direct dependencies. This makes it easier to maintain the smaller application, but makes it harder to reuse modules outside of this application.
[Style Y170]
-
Inject code into module configuration that must be configured before running the angular app. Ideal candidates include providers and constants.
Why?: This makes it easier to have less places for configuration.
angular
.module('app')
.config(configure);
var configure = ['routerHelperProvider', 'exceptionHandlerProvider', 'toastr', (routerHelperProvider, exceptionHandlerProvider, toastr) {
exceptionHandlerProvider.configure(config.appErrorPrefix);
configureStateHelper();
toastr.options.timeOut = 4000;
toastr.options.positionClass = 'toast-bottom-right';
////////////////
var configureStateHelper = [() => {
routerHelperProvider.configure({
docTitle: 'NG-Modular: '
});
}];
}];[Style Y171]
-
Any code that needs to run when an application starts should be declared in a factory, exposed via a function, and injected into the run block.
Why?: Code directly in a run block can be difficult to test. Placing in a factory makes it easier to abstract and mock.
angular
.module('app')
.run(runBlock);
var runBlock = ['authenticator', 'translator', (authenticator, translator) => {
authenticator.initialize();
translator.initialize();
}];[Style Y180]
-
Use
$documentand$windowinstead ofdocumentandwindow.Why?: These services are wrapped by Angular and more easily testable than using document and window in tests. This helps you avoid having to mock document and window yourself.
[Style Y181]
-
Use
$timeoutand$intervalinstead ofsetTimeoutandsetInterval.Why?: These services are wrapped by Angular and more easily testable and handle Angular's digest cycle thus keeping data binding in sync.
Best practice guides for unit and e2e testing in Element can be found here Unit testing and here Protractor testing
[Style Y190]
-
Write a set of tests for every story. Start with an empty test and fill them in as you write the code for the story.
Why?: Writing the test descriptions helps clearly define what your story will do, will not do, and how you can measure success.
it('should have Avengers controller', function() { // TODO }); it('should find 1 Avenger when filtered by name', function() { // TODO }); it('should have 10 Avengers', function() { // TODO (mock data?) }); it('should return Avengers via XHR', function() { // TODO ($httpBackend?) }); // and so on
[Style Y210]
-
Use subtle animations with Angular to transition between states for views and primary visual elements. Include the ngAnimate module. The 3 keys are subtle, smooth, seamless.
Why?: Subtle animations can improve User Experience when used appropriately.
Why?: Subtle animations can improve perceived performance as views transition.
[Style Y211]
-
Use short durations for animations. I generally start with 300ms and adjust until appropriate.
Why?: Long animations can have the reverse affect on User Experience and perceived performance by giving the appearance of a slow application.
[Style Y220]
-
If planning to produce documentation, use
jsDocsyntax to document function names, description, params and returns. Use@namespaceand@memberOfto match your app structure.Why?: You can generate (and regenerate) documentation from your code, instead of writing it from scratch.
Why?: Provides consistency using a common industry tool.
/** * Logger Factory * @namespace Factories */ angular .module('app') .service('logger', logger); /** * @namespace Logger * @desc Application wide logger * @memberOf Factories */ class Logger { public static $inject = ['$log']; constructor(private $log: ng.ILogService) { } /** * @name logError * @desc Logs errors * @param {String} msg Message to log * @returns {String} * @memberOf Factories.Logger */ LogError(msg: string): string { var loggedMsg = 'Error: ' + msg; this.$log.error(loggedMsg); return loggedMsg; } } export = Logger;
[Style Y420]
-
Avoid using filters for scanning all properties of a complex object graph. Use filters for select properties.
Why?: Filters can easily be abused and negatively affect performance if not used wisely, for example when a filter hits a large and deep object graph.
For anything else, API reference, check the Angular documentation.
Open an issue first to discuss potential changes/additions. If you have questions with the guide, feel free to leave them as issues in the repository. If you find a typo, create a pull request. The idea is to keep the content up to date and use github’s native feature to help tell the story with issues and PR’s, which are all searchable via google. Why? Because odds are if you have a question, someone else does too! You can learn more here at about how to contribute.
By contributing to this repository you are agreeing to make your content available subject to the license of this repository.
1. Discuss the changes in a GitHub issue.
2. Open a Pull Request, reference the issue, and explain the change and why it adds value.
3. The Pull Request will be evaluated and either merged or declined.
tldr; Use this guide. Attributions are appreciated.
Copyright (c) 2014-2015 John Papa
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

