- Purpose
- Getting started
- Tools
- Foundations of AngularJS
- Debugging
- Build tools and transpilation
- App entry
- Controllers
- Services
- Directives
- Routing
- Events
- Testing
- Cordova integration
- References
This repository provides guidance via explanation and code samples for the foundation of development in Angular 1.x.
It answers many common questions about getting started with AngularJS and future-proofing your app as much as possible.
Where possible, I have provided code examples in ECMAScript 6. If there is no ES6 equivalent available, I've noted this and provided an example in ES5.
First, install Node.js . We won't actually be running node, but we do want its package manager, npm. Note: This is only a build dependency and is not required for production servers.
All commands below are from your terminal or command line
- Clone the project.
git clone https://github.com/SavageBits/angular-styleguide.git
- Install the dependencies.
npm install
- Build and run.
gulp
* This requires Gulp to be installed globally withnpm install gulp -g
or you can run gulp locally withnode .\node_modules\bin\gulp.js
This will build the AngularJS app and open it in your default browser.
-
Editors
- JetBrains WebStorm - Configure WebStorm for ES6. File >> Settings >> Languages & Frameworks >> JavaScript >> JavaScript language version
- Visual Studio Code
-
Command line
- PowerShell or Terminal if on MacOS
I also used Atom extensively but ultimately found it too buggy and RAM-intensive.
//@todo: add whys in the form of avoid vs prefer
Components should have one role.
DOM manipulation should happen only in directives.
Data and logic operations should be done in services.
Avoid using jQuery.
Create one component per file (applies to modules, controllers, services, directives, etc).
File organization is based on the LIFT principle.
- Locate our code easily.
- Identify code at a glance.
- Flat structure kept as long as possible.
- Try to stay DRY
Avoid inconsistency. Prefer organizing projects by feature.
/* by feature by type */
|__ app
|.. |__ accounts
|.. |.. |__ controllers
|.. |.. |.. |-- accounts.js
|.. |.. |__ views
|.. |.. |.. |-- accounts.html
|.. |.. |__ services
|.. |.. |.. |-- accounts.js
|.. |__ payments
|.. |.. |__ controllers
|__ test
|.. |__ accounts
|.. |.. |__ services
|.. |.. |.. |-- accounts.spec.js
|__ [and so on]
/* by feature - preferred */
|__ app
|.. |__ accounts
|.. |.. |-- accountsCtrl.js
|.. |.. |-- accounts.html
|.. |.. |-- accountsSvc.js
|.. |.. |-- accountsSvc.spec.js
|.. |__ payments
|.. |.. |-- paymentsCtrl.js
|.. |.. |-- payments.html
|.. |.. |-- paymentsSvc.js
|.. |.. |-- paymentsSvc.spec.js
|__ [and so on]
As mentioned in Testing, it's common to locate tests in the same directory as the unit-under-test.
Modules that are intended for re-use across multiple projects should be located in the core
directory, which defines its own module in core.module.js
.
The core module will also define any application-specific dependencies such as ngRoute
, ngAnimate
, etc.
The core module is then injected as a dependency into the main app module in app.module.js
.
Application-specific components that do not belong to a single feature should be located in the 'widgets' directory, which defines its own module in widgets.module.js
.
The widgets module is then injected as a dependency into the main app module in app.module.js
.
Use the unminified version of AngularJS for development. This will provide the most meaningful error messages.
Avoid console.log()
. Prefer $log.debug()
. To use $log
, inject it as you would any other dependency.
By using the built-in $logProvider
, you can easily enable logging for your entire application.
/* accountCtrl.js */
function AccountCtrl($rootScope, $log, AccountSvc) {
var vm = this;
$log.debug('Set $logProvider.debugEnabled(false) in app.config.js to turn this message off');
/* ... */
/* app.config.js */
angular.module('app')
.config(function($logProvider) {
$logProvider.debugEnabled(true);
});
The gulpfile.js
defines our build script and our transpilation via Babel. There are other tutorials on these tools, so here's what's noteworthy:
- gulp-ng-annotate - Injection annotations for uglification protection
- gulp-babel - Transpilation from ES5 to ES6
- module-bundle.js - Guarantees that containing modules exist before dependencies that require them. Includes
app.module.js
, all feature modules (*.module.js
), our cross-cutting components (core.module.js
), and our application-specific components (widgets.module.js
) - bundle.js - all other 1st party JavaScript source
The application is auto-bootstrapped using the ngApp attribute in index.html. The ng-strict-di
attribute requires any module dependencies to be minification-safe via dependency injection annotations. These annotations will be handled for you by gulp-ng-annotate.
<!-- index.html -->
<html ng-app='app' ng-strict-di>
<!-- ... -->
/* gulpfile.js */
/* ... */
gulp.task('js', function() {
gulp.src(config.paths.js)
.pipe(babel({
presets: ['es2015']
}))
.pipe(ngAnnotate())
.pipe(concat(config.paths.bundle))
.pipe(gulp.dest('.'))
.pipe(connect.reload());
});
/* ... */
See Cordova integration for mobile development bootstrapping with AngularJS 1.x.
To support future migration to Angular 2, avoid usage of $scope. Reference data on the controller by using "controller as" and "controllerName.propertyName".
/* ES6 accountCtrl.js */
class AccountCtrl {
constructor() {
this.myProperty = 'my property';
}
}
angular
.module('app')
.controller('AccountCtrl', AccountCtrl);
<!-- index.html -->
<body ng-controller='AccountCtrl as accountCtrl'>
My property: {{ accountCtrl.myProperty }}
<account-card></account-card>
</body>
/* accountSvc.js */
class AccountSvc {
constructor($http) {
this.$http = $http;
}
myGetFunction(myApiRoute) {
return this.$http.get(myApiRoute);
}
getAccountById(accountId, successCallback) {
this.$http.get('/assets/data/accounts.json')
.then(function(response) {
for (var i=0; i < response.data.length; i++) {
if (response.data[i].id == accountId) {
successCallback(response.data[i]);
break;
}
}
});
}
}
angular
.module('app.accounts')
.service('accountSvc', AccountSvc);
In Angular 1.x, the .directive()
method expects a factory function with specific properties, so there is no direct path from ES5 to ES6 without some really unsavory code.
That said, here are some good patterns for directives in 1.x.
/* accountCard.js */
function AccountCard() {
return {
templateUrl: 'app/src/widgets/accountCard/account-card.html'
}
}
angular
.module('app.widgets')
.directive('accountCard', AccountCard);
Each feature is responsible for handling routing to its own views which reference that feature's controller(s).
/* accounts.routes.js */
angular
.module('app.accounts')
.config(function($routeProvider,$locationProvider) {
$routeProvider
.when('/', {
templateUrl: '/app/src/accounts/account-list.html',
controller: 'AccountCtrl',
controllerAs: 'vm'
})
.when('/detail/:accountId', {
templateUrl: '/app/src/accounts/account-detail.html',
controller: 'AccountDetailCtrl',
controllerAs: 'vm'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode({
enabled: true,
requireBase: false
})
});
The example injects $rootScope
and uses $on
and $emit
to communicate between controllers using events.
See headerCtrl.js and accountDetailCtrl.js.
We're using karma, mocha, chai, and ngMock for testing.
Run tests. npm test
- This will start karma and run tests in PhantomJS.
A typical pattern when organizing a project by feature is to locate tests with units-under-test. In this example, we've located the test for the account service (accountsSvc.spec.js) in the same directory as the account service itself (accountSvc.js).
See accountSvc.spec.js for an example of mocking out a service along with the AngularJS built-in $http
to test an async call that returns a promise.
John Papa's Angular Style Guide
John Papa's PluralSight course AngularJS Patterns: Clean Code
Todd Motto's Angular styleguide
Cory House's React Slingshot
ECMAScript 6 in WebStorm: Transpiling
Exploring ES6 Classes In AngularJS 1.x