[Fuente: https://github.com/inavarroreus/angularjs-styleguide]
Single Responsability
- Cada componente de angular en su fichero
Immediately Invoked Function Expression (IIFE)
- Envolver los componentes de angular con bloques de Javascript
- Esto elimina los efectos de ir dejando muchas declaraciones globales que pueden entrar en conflicto
/** * recommended * * no globals are left behind */ // logger.js (function() { 'use strict'; angular .module('app') .factory('logger', logger); function logger() { } })(); // storage.js (function() { 'use strict'; angular .module('app') .factory('storage', storage); function storage() { } })();
Avoid naming collissions
- Define nombres únicos para los módulos y los submodulos de angular. Con notaciones de puntos
Definitions (aka setters)
- Para la creación de la aplicación angular y sus componentes evitar crear variables , utiliza la sintaxis de getter / setter:
/* avoid */ var app = angular.module('app'); app.controller('SomeController' , SomeController); function SomeController() { }
/* recommended */ angular .module('app') .controller('SomeController' , SomeController); function SomeController() { }
- Use
angular.module('app', []);
to set a module. - Use
angular.module('app');
to get a module.
Named vs Anonymous Functions
/* avoid */ angular .module('app') .controller('Dashboard', function() { }) .factory('logger', function() { });
/* recommended */ // dashboard.js angular .module('app') .controller('Dashboard', Dashboard); function Dashboard() { }
// logger.js angular .module('app') .factory('logger', logger); function logger() { }
controllerAs View Syntax
Es una nueva forma de referenciar el $scope de los controllers. Está pensado sobre para cuando hay varios controllers anidados que no tengas que andar referenciando con $parent
<div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings"> Name: <input type="text" ng-model="settings.name"/> [ <a href="" ng-click="settings.greet()">greet</a> ]<br/> Contact: <ul> <li ng-repeat="contact in settings.contacts"> <select ng-model="contact.type"> <option>phone</option> <option>email</option> </select> <input type="text" ng-model="contact.value"/> [ <a href="" ng-click="settings.clearContact(contact)">clear</a> | <a href="" ng-click="settings.removeContact(contact)">X</a> ] </li> <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> </ul> </div>
angular.module('controllerAsExample', []) .controller('SettingsController1', SettingsController1); function SettingsController1() { this.name = "John Smith"; this.contacts = [ {type: 'phone', value: '408 555 1212'}, {type: 'email', value: 'john.smith@example.org'} ]; } SettingsController1.prototype.greet = function() { alert(this.name); }; SettingsController1.prototype.addContact = function() { this.contacts.push({type: 'email', value: 'yourname@example.org'}); }; SettingsController1.prototype.removeContact = function(contactToRemove) { var index = this.contacts.indexOf(contactToRemove); this.contacts.splice(index, 1); }; SettingsController1.prototype.clearContact = function(contact) { contact.type = 'phone'; contact.value = ''; };
controllerAs with vm
Se trata de que cuando se use este tipo de sintaxis no utilicemos this , sino una variable como por ejemplo “vm” , siglas de ViewModel. Esto es por que la palabra clave “this” es contextual , es decir depende del contexto, si por ejemplo se referencia dentro de una función puede ser diferente a si se referencia en otro sitio.
/* avoid */ function Customer() { this.name = {}; this.sendMessage = function() { }; }
/* recommended */ function Customer() { var vm = this; vm.name = {}; vm.sendMessage = function() { }; }
Se la puede referenciar dentro de los controladores , por ejemplo en watches:
$scope.$watch('vm.title', function(current, original) { $log.info('vm.title was %s', original); $log.info('vm.title is now %s', current); });
Bindable Members Up Top
Poner todos los elementos que sean referenciables al principio del controlador. Que no esten desperdigados por todo el archivo. Y que estén ordenados alfabeticamente.Se gana legibilidad
/* avoid */ function Sessions() { var vm = this; vm.gotoSession = function() { /* ... */ }; vm.refresh = function() { /* ... */ }; vm.search = function() { /* ... */ }; vm.sessions = []; vm.title = 'Sessions';
/* recommended */ function Sessions() { var vm = this; vm.gotoSession = gotoSession; vm.refresh = refresh; vm.search = search; vm.sessions = []; vm.title = 'Sessions'; //////////// function gotoSession() { /* */ } function refresh() { /* */ } function search() { /* */ }
Function Declarations to Hide Implementation Details
Utilizar pequeñas funciones que escondan los detalles de implementación. Y poner estas funciones al final del componente, dejando al principio las referencias.Tiene que ver con el apartado anterior
/** * avoid * Using function expressions. */ function Avengers(dataservice, logger) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; var activate = function() { return getAvengers().then(function() { logger.info('Activated Avengers View'); }); } var getAvengers = function() { return dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); } vm.getAvengers = getAvengers; activate(); } /* * recommend * Using function declarations * and bindable members up top. */ function Avengers(dataservice, logger) { var vm = this; vm.avengers = []; vm.getAvengers = getAvengers; vm.title = 'Avengers'; activate(); function activate() { return getAvengers().then(function() { logger.info('Activated Avengers View'); }); } function getAvengers() { return dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); } }
Defer Controller Logic
Se trata de que todo lo que sea lógica de negocio sea deferido a un servicio en vez de ponerlo dentro del controller. De esta forma se puede reutilizar con otros controllers y se le puede hacer pruebas unitarias más facilmente
/* avoid */ function Order($http, $q) { var vm = this; vm.checkCredit = checkCredit; vm.total = 0; function checkCredit() { var orderTotal = vm.total; return $http.get('api/creditcheck').then(function(data) { var remaining = data.remaining; return $q.when(!!(remaining > orderTotal)); }); }; }
/* recommended */ function Order(creditService) { var vm = this; vm.checkCredit = checkCredit; vm.total = 0; function checkCredit() { return creditService.check(); }; }
Keep Controllers Focused
Definir un controller por vista , no caer en reutilizar el mismo controller para varias vistas. Si hay código común , definirlos dentro de servicios o de factorias. Así es más fácil hacer pruebas e2e
Assigning Controllers
Cuando tenemos que el mapeo de vistas/controllers con rutas puede ser dinámico y hay algunos controllers/views que claramente pueden ser compartidos con distintas rutas , entonces es mejor hacer routing en la configuración de angular. Si ponemos el controller en la vista con ng-controller esa vista siempre estará asociada a ese controller.
/* avoid - when using with a route and dynamic pairing is desired */ // route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html' }); }
<!-- avengers.html --> <div ng-controller="Avengers as vm"> </div>
/* recommended */ // route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm' }); }
<!-- avengers.html --> <div> </div>
Services – Singletons
Tanto los servicios , como los factories son singletons (Singletons – Each component dependent on a service gets a reference to the single instance generated by the service factory.).
Como los services se referencian con new y los factories no , es mejor utilizar factories por consistencia. De hecho en la web de angular ya se refieren a los servicios como factories (https://docs.angularjs.org/guide/services)
Factories – Accessible Members Up Top
Poner los elementos accesibles del servicio al principio de la factory, con los beneficios de legibilidad que ello tiene.Se sigue el patrón http://addyosmani.com/resources/essentialjsdesignpatterns/book/#revealingmodulepatternjavascript
/* avoid */ function dataService() { var someValue = ''; function save() { /* */ }; function validate() { /* */ }; return { save: save, someValue: someValue, validate: validate }; }
/* recommended */ function dataService() { var someValue = ''; var service = { save: save, someValue: someValue, validate: validate }; return service; //////////// function save() { /* */ }; function validate() { /* */ }; }
Data Service : Separate Data Calls
Esconder la lógica de acceso a datos , de llamadas a servicios , etc separadas de la lógica de negocio/presentación de los controllers.
/* recommended */ // dataservice factory angular .module('app.core') .factory('dataservice', dataservice); dataservice.$inject = ['$http', 'logger']; function dataservice($http, logger) { return { getAvengers: getAvengers }; function getAvengers() { return $http.get('/api/maa') .then(getAvengersComplete) .catch(getAvengersFailed); function getAvengersComplete(response) { return response.data.results; } function getAvengersFailed(error) { logger.error('XHR Failed for getAvengers.' + error.data); } } } /* recommended */ // controller calling the dataservice factory angular .module('app.avengers') .controller('Avengers', Avengers); Avengers.$inject = ['dataservice', 'logger']; function Avengers(dataservice, logger) { var vm = this; vm.avengers = []; activate(); function activate() { return getAvengers().then(function() { logger.info('Activated Avengers View'); }); } function getAvengers() { return dataservice.getAvengers() .then(function(data) { vm.avengers = data; return vm.avengers; }); } }
Data Service:Return a Promise from Data Calls
Esto trata de que cuando llamas a un método de un servicio que retorna una promesa, entonces lo suyo es que la función en la que estás retorne también una promesa. De esta forma es más fácil encadenar promesas para realizar acciones a posteriori.
* recommended */ activate(); function activate() { /** * Step 1 * Ask the getAvengers function for the * avenger data and wait for the promise */ return getAvengers().then(function() { /** * Step 4 * Perform an action on resolve of final promise */ logger.info('Activated Avengers View'); }); } function getAvengers() { /** * Step 2 * Ask the data service for the data and wait * for the promise */ return dataservice.getAvengers() .then(function(data) { /** * Step 3 * set the data and resolve the promise */ vm.avengers = data; return vm.avengers; }); }
Directives: Limit 1 Per File
Es más fácil de mantener y de reutilizar
Directives: Limit DOM Manipulation
Para manipular el DOM se utilizan directivas pero hay que limitar estas manipulaciones porque son dificiles de testear y depurar.
A menudo hay alternativas , como el utlizar ngShow / ngHide, angular Templating , utilizar CSS o los animation services
Directives: Provide a Unique Directive Prefix
- Dales a las directivas prefijos únicos y descriptivos. Por ejemplo:
acmeSalesCustomerInfo
which is declared in HTML asacme-sales-customer-info
. - Why?: Con prefijos únicos se identifica el contexto de la directiva tanto como su origen. Por ejemplo el prefijo cc- puede indicar que la directiva es parte de la app CodeCamper mientras que el prefijo acme- puede indicar que la directiva es de la compañia Acme.
- Note: No utilices ng- porque es el prefijo reservado para las directivas de Angular.
Directives: Restrict to Elements and Attributes
- A la hora de crear directivas es mejor crearlas como elementos autocontenidos, es decir poner en el restrict E (custom element) o A (custom attribute).
- Cuando la directiva tiene su propio control es más apropiado definirlo como E.
- Desde Angular 1.3+ el por defecto es EA.
Directives and ControllerAs
- Para controllers de directivas utiliza la sintaxis
controller as
- 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>
angular .module('app') .directive('myExample', myExample); function myExample() { var directive = { restrict: 'EA', templateUrl: 'app/feature/example.directive.html', scope: { max: '=' }, link: linkFunc, controller : ExampleController, controllerAs: 'vm' }; return directive; ExampleController.$inject = ['$scope']; function ExampleController($scope) { // Injecting $scope just for comparison /* jshint validthis:true */ var vm = this; vm.min = 3; vm.max = $scope.max; console.log('CTRL: $scope.max = %i', $scope.max); console.log('CTRL: vm.min = %i', vm.min); console.log('CTRL: vm.max = %i', vm.max); } function linkFunc(scope, el, attr, ctrl) { console.log('LINK: scope.max = %i', scope.max); console.log('LINK: scope.vm.min = %i', scope.vm.min); console.log('LINK: scope.vm.max = %i', scope.vm.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>
Resolving Promises for a Controller
Controller Activation Promises
- Se trata de poner la lógica de arranque del controlador en una función activate
- Why?: Haciendolo de esta forma se hace más fácil de localizar donde empieza a ejecutar el controlador, es más consistente para el testeo, y ayuda para no esparcir la lógica de activación a lo largo del código del controller.
- Note: If you need to conditionally cancel the route before you start use the controller, use a route resolve instead.
-
/* avoid */ function Avengers(dataservice) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); }
/* recommended */ function Avengers(dataservice) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; activate(); //////////// function activate() { return dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); } }
Route Resolve Promises
- Cuando un controller depende de que una promise sea resuelta, es mejor poner esta dependencia utilizando la propiedad resolve del $routeProvider antes de que la lógica del controller se ejecute. Si necesitas condicionalmente cancelar una ruta antes de que el controlador sea activado , utiliza el route resolver.
- 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.
/* avoid */ angular .module('app') .controller('Avengers', Avengers); function Avengers(movieService) { var vm = this; // unresolved vm.movies; // resolved asynchronously movieService.getMovies().then(function(response) { vm.movies = response.movies; }); }
/* better */ // route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm', resolve: { moviesPrepService: function(movieService) { return movieService.getMovies(); } } }); } // avengers.js angular .module('app') .controller('Avengers', Avengers); Avengers.$inject = ['moviesPrepService']; function Avengers(moviesPrepService) { /* jshint validthis:true */ var vm = this; vm.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 and on minification and annotation.
Manual Annotating for Dependency Injection
UnSafe from Minification
- Elimina utilizar abreviaturas a la hora de declarar dependencias sin utilizar alguna forma de minification-safe.
- Why?: The parameters to the component (e.g. controller, factory, etc) will be converted to mangled variables. For example,
common
anddataservice
may becomea
orb
and not be found by AngularJS./* 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) { }
Manually Identify Dependencies
- Utiliza
$inject
para manualmente identificar tus dependencias de los componentes de AngularJS.Why?: This technique mirrors the technique used byng-annotate
, which I recommend for automating the creation of minification safe dependencies. Ifng-annotate
detects injection has already been made, it will not duplicate it.Why?: This safeguards your dependencies from being vulnerable to minification issues when parameters may be mangled. For example,common
anddataservice
may becomea
orb
and not be found by AngularJS.Why?: Avoid creating in-line dependencies as long lists can be difficult to read in the array. Also it can be confusing that the array is a series of strings while the last item is the component’s function./* avoid */ angular .module('app') .controller('Dashboard', ['$location', '$routeParams', 'common', 'dataservice', function Dashboard($location, $routeParams, common, dataservice) {} ]);
/* avoid */ angular .module('app') .controller('Dashboard', ['$location', '$routeParams', 'common', 'dataservice', Dashboard]); function Dashboard($location, $routeParams, common, dataservice) { }
/* recommended */ angular .module('app') .controller('Dashboard', Dashboard); Dashboard.$inject = ['$location', '$routeParams', 'common', 'dataservice']; function Dashboard($location, $routeParams, common, dataservice) { }
Note: When your function is below a return statement the $inject may be unreachable (this may happen in a directive). You can solve this by either moving the $inject above the return statement or by using the alternate array injection syntax.
Note:
ng-annotate 0.10.0
introduced a feature where it moves the$inject
to where it is reachable.// inside a directive definition function outer() { return { controller: DashboardPanel, }; DashboardPanel.$inject = ['logger']; // Unreachable function DashboardPanel(logger) { } }
// inside a directive definition function outer() { DashboardPanel.$inject = ['logger']; // reachable return { controller: DashboardPanel, }; function DashboardPanel(logger) { } }
Manually Identify Route Resolver Dependencies
- Utiliza $inject para manualmente identificar tus dependecias de componentes Angular en el route resolver.Why?: This technique breaks out the anonymous function for the route resolver, making it easier to read.Why?: An
$inject
statement can easily precede the resolver to handle making any dependencies minification safe./* recommended */ function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm', resolve: { moviesPrepService: moviePrepService } }); } moviePrepService.$inject = ['movieService']; function moviePrepService(movieService) { return movieService.getMovies(); }
Minification and Annotation
ng-annotate
- Utilizar la herramienta
ng-annotate como plugin de Gulp o Grunt para comentar funciones que necesiten inyección de dependencias y que las automatizen. Utilizando
/** @ngInject */
- Why?: This safeguards your code from any dependencies that may not be using minification-safe practices.Why?:
ng-min
is deprecated
I prefer Gulp as I feel it is easier to write, to read, and to debug.
The following code is not using minification safe dependencies.
angular .module('app') .controller('Avengers', Avengers); /* @ngInject */ function Avengers(storageService, avengerService) { var vm = this; vm.heroSearch = ''; vm.storeHero = storeHero; function storeHero(){ var hero = avengerService.find(vm.heroSearch); storageService.save(hero.name, hero); } }
When the above code is run through ng-annotate it will produce the following output with the
$inject
annotation and become minification-safe.angular .module('app') .controller('Avengers', Avengers); /* @ngInject */ function Avengers(storageService, avengerService) { var vm = this; vm.heroSearch = ''; vm.storeHero = storeHero; function storeHero(){ var hero = avengerService.find(vm.heroSearch); storageService.save(hero.name, hero); } } Avengers.$inject = ['storageService', 'avengerService'];
Note: If
ng-annotate
detects injection has already been made (e.g.@ngInject
was detected), it will not duplicate the$inject
code.Note: When using a route resolver you can prefix the resolver’s function with
/* @ngInject */
and it will produce properly annotated code, keeping any injected dependencies minification safe.// Using @ngInject annotations function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm', resolve: { /* @ngInject */ moviesPrepService: function(movieService) { return movieService.getMovies(); } } }); }
Note: Starting from AngularJS 1.3 use the
ngApp
directive’sngStrictDi
parameter. When present the injector will be created in “strict-di” mode causing the application to fail to invoke functions which do not use explicit function annotation (these may not be minification safe). Debugging info will be logged to the console to help track down the offending code.<body ng-app="APP" ng-strict-di>
Use Gulp or Grunt for ng-annotate
- Utiliza gulp-ng-annotate o grunt-ng-annotate para meterlo como una tarea automatizada de build. Inject
/* @ngInject */
prior to any function that has dependencies.Why?: ng-annotate will catch most dependencies, but it sometimes requires hints using the/* @ngInject */
syntax.The following code is an example of a gulp task using ngAnnotategulp.task('js', ['jshint'], function() { var source = pkg.paths.js; return gulp.src(source) .pipe(sourcemaps.init()) .pipe(concat('all.min.js', {newLine: ';'})) // Annotate before uglify so the code get's min'd properly. .pipe(ngAnnotate({ // true helps add where @ngInject is not used. It infers. // Doesn't work with resolve, so we must be explicit there add: true })) .pipe(bytediff.start()) .pipe(uglify({mangle: true})) .pipe(bytediff.stop()) .pipe(sourcemaps.write('./')) .pipe(gulp.dest(pkg.paths.dev)); });
Exception Handling
decorators
- Se trata de mejorar el manejo de excepciones sobreescribiendo el servicio que proporciona Angular por defecto. Para ello se utilizan lo que se llaman los decorators –> decorator, Se configuran en el config de la ng-app utilizan el servicio
$provide
sobre el servicio$exceptionHandler
para realizar acciones personalizadas cuando ocurran excepciones.Why?: Provides a consistent way to handle uncaught AngularJS exceptions for development-time or run-time.Note: Another option is to override the service instead of using a decorator. This is a fine option, but if you want to keep the default behavior and extend it a decorator is recommended./* recommended */ angular .module('blocks.exception') .config(exceptionConfig); exceptionConfig.$inject = ['$provide']; function exceptionConfig($provide) { $provide.decorator('$exceptionHandler', extendExceptionHandler); } extendExceptionHandler.$inject = ['$delegate', 'toastr']; function extendExceptionHandler($delegate, toastr) { return function(exception, cause) { $delegate(exception, cause); var errorData = { exception: exception, cause: cause }; /** * Could add the error to a service's collection, * add errors to $rootScope, log errors to remote web server, * or log locally. Or throw hard. It is entirely up to you. * throw exception; */ toastr.error(exception.msg, errorData); }; }
Exception Catchers
- Crearse un factory para exponer un interfaz que recoja y maneje de la manera que tu quieres las excepciones.
- Why?: Provides a consistent way to catch exceptions that may be thrown in your code (e.g. during XHR calls or promise failures).Note: The exception catcher is good for catching and reacting to specific exceptions from calls that you know may throw one. For example, when making an XHR call to retrieve data from a remote web service and you want to catch any exceptions from that service and react uniquely.
/* recommended */ angular .module('blocks.exception') .factory('exception', exception); exception.$inject = ['logger']; function exception(logger) { var service = { catcher: catcher }; return service; function catcher(message) { return function(reason) { logger.error(message, reason); }; } }
Route Errors
- Manejar todos los errores de routing sacando logs utilizando
$routeChangeError
.Why?: Provides a consistent way handle all routing errors.Why?: Potentially provides a better user experience if a routing error occurs and you route them to a friendly screen with more details or recovery options./* recommended */ function handleRoutingErrors() { /** * Route cancellation: * On routing error, go to the dashboard. * Provide an exit clause if it tries to do it twice. */ $rootScope.$on('$routeChangeError', function(event, current, previous, rejection) { var destination = (current && (current.title || current.name || current.loadedTemplateUrl)) || 'unknown target'; var msg = 'Error routing to ' + destination + '. ' + (rejection.msg || ''); /** * Optionally log using a custom service or $log. * (Don't forget to inject custom service) */ logger.warning(msg, [current]); } ); }
Naming
Naming Guidelines
- Utilizar nombres consistenes para todos los componentes siguiendo un patrón que describa la funcionalidad del componente y (opcionalmente) su tipo. Mi patrón recomendado es
feature.type.js
. Hay dos nombres para la mayoría de assets:
- el nombre del fichero (
avengers.controller.js
) - El nombre del componente que se registra en 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.
Feature File Names
- Use consistent names for all components following a pattern that describes the component’s feature then (optionally) its type. My recommended pattern is
feature.type.js
.Why?: Provides a consistent way to quickly identify components.Why?: Provides pattern matching for any automated tasks./** * common options */ // Controllers avengers.js avengers.controller.js avengersController.js // Services/Factories logger.js logger.service.js loggerService.js
/** * recommended */ // controllers avengers.controller.js avengers.controller.spec.js // services/factories logger.service.js logger.service.spec.js // constants constants.js // module definition avengers.module.js // routes avengers.routes.js avengers.routes.spec.js // configuration avengers.config.js // directives avenger-profile.directive.js avenger-profile.directive.spec.js
Note: Another common convention is naming controller files without the word
controller
in the file name such asavengers.js
instead ofavengers.controller.js
. All other conventions still hold using a suffix of the type. Controllers are the most common type of component so this just saves typing and is still easily identifiable. I recommend you choose 1 convention and be consistent for your team./** * recommended */ // Controllers avengers.js avengers.spec.js
Test File Names
- 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.js logger.service.spec.js avengers.routes.spec.js avenger-profile.directive.spec.js
Controller Names
- 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('HeroAvengers', HeroAvengers); function HeroAvengers(){ }
Controller Name Suffix
- Append the controller name with the suffix
Controller
or with no suffix. Choose 1, not both.Why?: TheController
suffix is more commonly used and is more explicitly descriptive.Why?: Omitting the suffix is more succinct and the controller is often easily identifiable even without the suffix./** * recommended: Option 1 */ // avengers.controller.js angular .module .controller('Avengers', Avengers); function Avengers(){ }
/** * recommended: Option 2 */ // avengers.controller.js angular .module .controller('AvengersController', AvengersController); function AvengersController(){ }
Factory Names
- Use consistent names for all factories named after their feature. Use camel-casing for services and factories.Why?: Provides a consistent way to quickly identify and reference factories.
/** * recommended */ // logger.service.js angular .module .factory('logger', logger); function logger(){ }
Directive Component Names
- 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('xxAvengerProfile', xxAvengerProfile); // usage is <xx-avenger-profile> </xx-avenger-profile> function xxAvengerProfile(){ }
Modules
- Cuando tenemos una app con varios módulos , el módulo principal se recomienda llamarlo app.module.js mientras que los demás módulos dependientes de este son nombrados en función de lo que representan. Por ejemplo, un módulo de admin se llamaría admin.module.js. Los módulos registrados correspondientes serían app y admin.
- Sin embargo , cuando tenemos una aplicación con sólo un módulo basta con tener app.js
- Why?: An app with 1 module is named
app.js
. It is the app, so why not be super simple. - Why?: Provides consistency for multiple module apps, and for expanding to large applications.Why?: Provides easy way to use task automation to load all module definitions first, then all other angular files (for bundling).
Configuration
- Separar la configuración de un módulo en un archivo aparte. A configuration file for the main
app
module is namedapp.config.js
(or simplyconfig.js
). A configuration for a module namedadmin.module.js
is namedadmin.config.js
.Why?: Separates configuration from module definition, components, and active code.Why?: Provides a identifiable place to set configuration for a module.
Routes
- Separate route configuration into its own file. Examples might be
app.route.js
for the main module andadmin.route.js
for theadmin
module. Even in smaller apps I prefer this separation from the rest of the configuration. An alternative is a longer name such asadmin.config.route.js
.
Application Structure LIFT Principle
LIFT
- Structure your app such that you can
L
ocate your code quickly,I
dentify the code at a glance, keep theF
lattest structure you can, andT
ry 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 guidelinesL
ocating our code is easyI
dentify code at a glanceF
lat structure as long as we canT
ry to stay DRY (Don’t Repeat Yourself) or T-DRY
Locate
- 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
Identify
- 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.
Flat
- 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.
T-DRY (Try to Stick to DRY)
- 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.
Application Structure
Overall Guidelines
- Have a near term view of implementation and a long term vision. In other words, start small and 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 theapp
folder. 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.
Layout
- Colocar los componentes que definan el layout general de la app en una carpeta llamada layout. These may include a shell view and controller may act as the container for the app, navigation, menus, content areas, and other regions.Why?: Organizes all layout in a single place re-used throughout the application.
Folders-by-Feature Structure
- 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.js app.config.js app.routes.js components/ calendar.directive.js calendar.directive.html user-profile.directive.js user-profile.directive.html layout/ shell.html shell.controller.js topnav.html topnav.controller.js people/ attendees.html attendees.controller.js speakers.html speakers.controller.js speaker-detail.html speaker-detail.controller.js services/ data.service.js localstorage.service.js logger.service.js spinner.service.js sessions/ sessions.html sessions.controller.js session-detail.html session-detail.controller.js
Note: Do not use structuring 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.js app.config.js app.routes.js controllers/ attendees.js session-detail.js sessions.js shell.js speakers.js speaker-detail.js topnav.js directives/ calendar.directive.js calendar.directive.html user-profile.directive.js user-profile.directive.html services/ dataservice.js localstorage.js logger.js spinner.js views/ attendees.html session-detail.html sessions.html shell.html speakers.html speaker-detail.html topnav.html
Modularity
Many Small, Self Contained Modules
- 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.
Create an App Module
- 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?: AngularJS 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.
Keep the App Module Thin
- 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.
Feature Areas are Modules
- 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 will 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.
Reusable Blocks are Modules
- 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.
Module Dependencies
- The application root module depends on the app specific feature modules, the feature modules have no direct dependencies, the cross-application modules depend on all generic modules.Why?: The main app module contains a quickly identifiable manifest of the application’s features.
Why?: Cross application features become easier to share. The features generally all rely on the same cross application modules, which are consolidated in a single module (
app.core
in the image).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 AngularJS’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.
Startup Logic
Configuration
- 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 a less places for configuration.
angular .module('app') .config(configure); configure.$inject = ['routerHelperProvider', 'exceptionHandlerProvider', 'toastr']; function configure (routerHelperProvider, exceptionHandlerProvider, toastr) { exceptionHandlerProvider.configure(config.appErrorPrefix); configureStateHelper(); toastr.options.timeOut = 4000; toastr.options.positionClass = 'toast-bottom-right'; //////////////// function configureStateHelper() { routerHelperProvider.configure({ docTitle: 'NG-Modular: ' }); } }
Run Blocks
- 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); runBlock.$inject = ['authenticator', 'translator']; function runBlock(authenticator, translator) { authenticator.initialize(); translator.initialize(); }
Angular $ Wrapper Services
$document and $window
- Use
$document
and$window
instead ofdocument
andwindow
.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.
$timeout and $interval
- Use
$timeout
and$interval
instead ofsetTimeout
andsetInterval
.Why?: These services are wrapped by Angular and more easily testable and handle AngularJS’s digest cycle thus keeping data binding in sync.
Testing
Unit testing helps maintain clean code, as such I included some of my recommendations for unit testing foundations with links for more information.
Write Tests with Stories
- 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
Testing Library
- Use Jasmine or Mocha for unit testing.Why?: Both Jasmine and Mocha are widely used in the AngularJS community. Both are stable, well maintained, and provide robust testing features.Note: When using Mocha, also consider choosing an assert library such as Chai.
Test Runner
- Use Karma as a test runner.Why?: Karma is easy to configure to run once or automatically when you change your code.Why?: Karma hooks into your Continuous Integration process easily on its own or through Grunt or Gulp.
Why?: Some IDE’s are beginning to integrate with Karma, such as WebStorm and Visual Studio.
Why?: Karma works well with task automation leaders such as Grunt (with grunt-karma) and Gulp(with gulp-karma).
Stubbing and Spying
- Use Sinon for stubbing and spying.Why?: Sinon works well with both Jasmine and Mocha and extends the stubbing and spying features they offer.Why?: Sinon makes it easier to toggle between Jasmine and Mocha, if you want to try both.
Headless Browser
- Use PhantomJS to run your tests on a server.Why?: PhantomJS is a headless browser that helps run your tests without needing a “visual” browser. So you do not have to install Chrome, Safari, IE, or other browsers on your server.Note: You should still test on all browsers in your environment, as appropriate for your target audience.
Code Analysis
- Run JSHint on your tests.Why?: Tests are code. JSHint can help identify code quality issues that may cause the test to work improperly.
Alleviate Globals for JSHint Rules on Tests
- Relax the rules on your test code to allow for common globals such as
describe
andexpect
.Why?: Your tests are code and require the same attention and code quality rules as all of your production code. However, global variables used by the testing framework, for example, can be relaxed by including this in your test specs./* global sinon, describe, it, afterEach, beforeEach, expect, inject */
Animations
Usage
- Use subtle animations with AngularJS 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.
Sub Second
- 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.
animate.css
- Use animate.css for conventional animations.Why?: The animations that animate.css provides are fast, smooth, and easy to add to your application.Why?: Provides consistency in your animations.
Why?: animate.css is widely used and tested.
Note: See this great post by Matias Niemelä on AngularJS animations
Comments
jsDoc
- If planning to produce documentation, use
jsDoc
syntax to document function names, description, params and returns. Use@namespace
and@memberOf
to 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 */ (function() { angular .module('app') .factory('logger', logger); /** * @namespace Logger * @desc Application wide logger * @memberOf Factories */ function logger($log) { var service = { logError: logError }; return service; //////////// /** * @name logError * @desc Logs errors * @param {String} msg Message to log * @returns {String} * @memberOf Factories.Logger */ function logError(msg) { var loggedMsg = 'Error: ' + msg; $log.error(loggedMsg); return loggedMsg; }; } })();
JS Hint
Use an Options File
- Use JS Hint for linting your JavaScript and be sure to customize the JS Hint options file and include in source control. See the JS Hint docs for details on the options.Why?: Provides a first alert prior to committing any code to source control.Why?: Provides consistency across your team.
{ "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "es3": false, "forin": true, "freeze": true, "immed": true, "indent": 4, "latedef": "nofunc", "newcap": true, "noarg": true, "noempty": true, "nonbsp": true, "nonew": true, "plusplus": false, "quotmark": "single", "undef": true, "unused": false, "strict": false, "maxparams": 10, "maxdepth": 5, "maxstatements": 40, "maxcomplexity": 8, "maxlen": 120, "asi": false, "boss": false, "debug": false, "eqnull": true, "esnext": false, "evil": false, "expr": false, "funcscope": false, "globalstrict": false, "iterator": false, "lastsemic": false, "laxbreak": false, "laxcomma": false, "loopfunc": true, "maxerr": false, "moz": false, "multistr": false, "notypeof": false, "proto": false, "scripturl": false, "shadow": false, "sub": true, "supernew": false, "validthis": false, "noyield": false, "browser": true, "node": true, "globals": { "angular": false, "$": false } }
Constants
Vendor Globals
- Create an AngularJS Constant for vendor libraries’ global variables.Why?: Provides a way to inject vendor libraries that otherwise are globals. This improves code testability by allowing you to more easily know what the dependencies of your components are (avoids leaky abstractions). It also allows you to mock these dependencies, where it makes sense.
// constants.js /* global toastr:false, moment:false */ (function() { 'use strict'; angular .module('app.core') .constant('toastr', toastr) .constant('moment', moment); })();
File Templates and Snippets
Use file templates or snippets to help follow consistent styles and patterns. Here are templates and/or snippets for some of the web development editors and IDEs.
Sublime Text
- AngularJS snippets that follow these styles and guidelines.
- Download the Sublime Angular snippets
- Place it in your Packages folder
- Restart Sublime
- In a JavaScript file type these commands followed by a
TAB
ngcontroller // creates an Angular controller ngdirective // creates an Angular directive ngfactory // creates an Angular factory ngmodule // creates an Angular module
Visual Studio
- AngularJS file templates that follow these styles and guidelines can be found at SideWaffle
- Download the SideWaffle Visual Studio extension (vsix file)
- Run the vsix file
- Restart Visual Studio
WebStorm
- AngularJS snippets and file templates that follow these styles and guidelines. You can import them into your WebStorm settings:
- Download the WebStorm AngularJS file templates and snippets
- Open WebStorm and go to the
File
menu - Choose the
Import Settings
menu option - Select the file and click
OK
- In a JavaScript file type these commands followed by a
TAB
:
ng-c // creates an Angular controller ng-f // creates an Angular factory ng-m // creates an Angular module
AngularJS docs
For anything else, API reference, check the Angular documentation.
Contributing
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.
Process
1. Discuss the changes in an Issue.
1. Open a Pull Request, reference the issue, and explain the change and why it adds value.
1. The Pull Request will be evaluated and either merged or declined.
sd
ds
ds
ds
ds
ds
dsds
fd
fdf
d