These notes were prepared to help colleagues get set up for Angular development. They may be helpful for others. Subjects covered include:
Node, NPM, Yeoman, generator-angular, Grunt, grunt-connect-proxy, Usemin, Bower, Compass, Bootstrap, Sass / SCSS, Karma, Jasmine, karma-ng-html2js-preprocessor, jasmine-given, jasmine-stealth, CoffeeScript, JSHint, Editorconfig, Chrome dev tools & Sublime Text 3 plugins.
gem update --system
gem install compass
npm install -g yo
this will install Grunt for builds and Bower for client side dependencies.generator-angular (Git Bash):
npm install -g generator-angular
Karma
npm install --save-dev karma-jasmine karma-chrome-launcher
npm install -g karma-cli
NOTE: There are some videos included in this tutorial. The steps followed in the videos may differ from those in the text. So follow the steps outline in the text and use the videos for reference if you get stuck. You will need to view most of the videos full screen.
Generate the project:
mkdir myapp
cd myapp
# Answer yes to all questions Yeoman asks
yo angular myapp
What just happened:
package.json
file was created for npm and the npm install
command was invokedbower.json
file created and the bower install
command was invokedTo start a test server:
grunt serve
You should see this:
What did Yeoman install?
Node.js runs JavaScript on the command line rather than in the browser. Among other things, it can be used to:
Npm is node's package manager. It allows one package to depend on other packages declared in a package.json
file. Consider this command:
npm install
The command above installs all the dependencies of a package (and all the dependencies of each dependency). This is a local install - local in this sense means local to the project. Running npm install
results in a node_modules
folder being created. Each folder in node_modules
contains a package with may itself contain a node_modules
folder.
Some node packages are intended to be used on the command line (e.g. Yeoman) so they need to be installed globally. We do this with the -g
flag:
`npm install -g yo`
Sometimes we want the convenience of installing a command globally, but the flexibility to have a different version of the package for each project. We use CLI wrapper packages to accomplish this.
Let's consider the example of the Karma test runner. A normal (global) install would look like this:
npm install -g karma
Instead, we install karma-cli
globally:
npm install -g karma-cli
karma-cli
is just a wrapper command that looks for a locally installed version of Karma in the node_modules
folder of our project. Running karma start
outside our project or before doing an npm install
will result in the following warning:
Cannot find local Karma!
Please install Karma by `npm install karma --save-dev`.
If you wanna use a global instance, please set NODE_PATH env variable.
Note: when we install Yeoman, it installs grunt in this way using the grunt-cli
wrapper.
Because yeoman normally only uses node for build steps, all our dependencies will be dev dependencies. If we want to install a new local dev dependency (e.g. karma-jasmine) and save it to our package.json
:
npm install --save-dev karma-jasmine
Note: If you are cloning a project created with Yeoman, you will need to manually install the dependencies:
npm install
You may need to run this occasionally after running git pull
if members of your team add new dependencies.
Also see: egghead.io NodeJS videos
Dependencies related to the build - in other words, related to grunt, are found in package.json
:
{
"name": "moo2",
"version": "0.0.0",
"dependencies": {},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-autoprefixer": "~0.4.0",
"grunt-bower-install": "~1.0.0",
"grunt-concurrent": "~0.5.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-compass": "~0.7.2",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-cssmin": "~0.7.0",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-imagemin": "~0.3.0",
"grunt-contrib-jshint": "~0.7.1",
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-watch": "~0.5.2",
"grunt-google-cdn": "~0.2.0",
"grunt-newer": "~0.6.1",
"grunt-ngmin": "~0.0.2",
"grunt-rev": "~0.1.0",
"grunt-svgmin": "~0.2.0",
"grunt-usemin": "~2.0.0",
"jshint-stylish": "~0.1.3",
"load-grunt-tasks": "~0.4.0",
"time-grunt": "~0.2.1",
"karma-ng-scenario": "^0.1.0",
"grunt-karma": "^0.8.3",
"karma": "^0.12.14",
"karma-ng-html2js-preprocessor": "^0.1.0",
"karma-chrome-launcher": "^0.1.3",
"grunt-jasmine": "^0.1.0",
"karma-jasmine": "^0.1.5",
"grunt-contrib-jade": "^0.11.0"
},
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"test": "grunt test"
}
}
If we were to look inside the Gruntfile.js
, we would find a correlation between the tasks configured there and the packages listed in package.json
.
At the bottom of Gruntfile.js
, we see some tasks registered:
Let's try test
:
grunt test
# Executed 1 of 1 SUCCESS
This runs the unit tests. If you get an error, check that you have already run this command:
npm install --save-dev karma-jasmine karma-chrome-launcher
Now, let's try build
:
grunt build
Now have a look in your dist
folder.
What happened?
vendor.js
scripts.js
main.css
index.html
file was minifiedNow let's test that in the browser:
grunt serve:dist
This time, the server serves our app from the dist
directory rather than app
and .tmp
(.tmp
is used for output of preprocessed scripts and css).
To remove the dist
directory, run this command:
grunt clean
Also see: egghead.io Grunt videos
If our RESTful API and web app will reside on the same domain, this simplifies our app, because we do not need to make cross origin requests. However, in development we may want to run our API server on a different host or port. If we use a proxy we can make it appear to the browser that the REST end point and app are served from the same domain.
Grunt connect proxy is very handy for accessing REST APIs during development without having to worry about CORS.
Let's install it:
npm install grunt-connect-proxy --save-dev
Look at package.json
and a property has been added:
"grunt-connect-proxy": "^0.1.10"
Edit the connect:
property in Gruntfile.js
adding proxies:
in place of livereload:
:
proxies: [
{
context: '/people',
host: 'ncredinburgh.com',
port: 80,
https: false,
changeOrigin: false
}
],
livereload: {
options: {
open: true,
base: [
'.tmp',
'<%= yeoman.app %>'
],
middleware:function (connect, options) {
if (!Array.isArray(options.base)) {
options.base = [options.base];
}
// Setup the proxy
var middlewares = [require('grunt-connect-proxy/lib/utils').proxyRequest];
// Serve static files.
options.base.forEach(function(base) {
middlewares.push(connect.static(base));
});
// Make directory browse-able.
var directory = options.directory || options.base[options.base.length - 1];
middlewares.push(connect.directory(directory));
return middlewares;
}
}
},
Edit the serve
task by adding configureProxies
:
grunt.task.run([
'clean:server',
'bowerInstall',
'concurrent:server',
'configureProxies', //add here
'autoprefixer',
'connect:livereload',
'watch'
]);
Then start up a server:
grunt serve
If you visit http://localhost:9000/people you should see the contents of http://ncredinburgh.com/people. Note that CSS and other styling will not load, so it will look a little ugly.
Tip: Change the connect.options.hostname:
setting from localhost
to 0.0.0.0
this will enable you to view your application using your computer's IP on any other computer on the network (very handy for testing on mobile).
These are browser dependencies (JQuery, Angular etc).
They are specified by the bower.json
file:
{
"name": "moo2",
"version": "0.0.0",
"dependencies": {
"angular": "1.2.15",
"json3": "~3.2.6",
"es5-shim": "~2.1.0",
"jquery": "~1.11.0",
"bootstrap-sass-official": "~3.1.0",
"angular-resource": "1.2.15",
"angular-cookies": "1.2.15",
"angular-sanitize": "1.2.15",
"angular-route": "1.2.15"
},
"devDependencies": {
"angular-mocks": "1.2.15",
"angular-scenario": "1.2.15"
}
}
To add a new dependency (e.g moment.js):
bower install momentjs --save
Then add script tag into into index.html
:
<script src="bower_components/momentjs/moment.js"></script>
Note: If your app has JS errors after running git pull
to retrieve others' changes, it is possible that another member of your team has added a bower dependency. To fix this:
bower install
Note: generator-angular
's grunt tasks will attempt to keep the script tags in index.html
up to date with the bower dependencies.
Also see: egghead.io Bower video
What are Sass, SCSS, Compass and Bootstrap? Click the links above to find out.
Sublime Text 3 is an extensible text editor that's ideal for Angular development. We recommend installing the Package Control plugin to make installing third-party extensions easier.
Note that Package Control is invoked with the Crtl+' not Ctrl+` as stated_
Use Crtl+Shift+P and type install
. Hit enter when you see: Package Control Install. Use Package Control to install the following:
Editor config is a module available for most text editors and IDEs it allows you to set basic formatting rules (tabs, line endings etc) in an editor agnostic way. The remove trailing white space is very handy if JSHint is set to be white space sensitive.
The settings are controlled by the .editorconfig
file.
JSHint will assist with code quality alerting you to common errors in the editor margin.
Its settings are controlled by the .jshintrc
file. They can be overridden with comments in JavaScript.
To run jshint on the command line:
grunt jshint
Try each of the following commands and answer the following questions:
test/spec
?app.js
and views/
?index.html
changed?yo angular:controller 'poddington-pea-ctrl'
yo angular:route clanger
yo angular:factory flump
yo angular:directive 'flower-pot-man'
The egghead.io site is a great place to learn more about Angular controllers, directives, factories and more.
We suggest that you start with the beinding tutorial, then keep clicking the next button to get to subsequent lessons.
Worth reading:
Karma is a test runner and Jasmine is a BDD test framework.
In karma.config.js
set:
autoWatch: true, //change to true
singleRun: false
Because karma.config
has singleRun
set to false
, Karma will not exit but rather continue to watch all the files listed in the files
property and run all the tests when any of them are modified. To run tests, run the following command:
karma start
# this is short hand for:
# karma start karma.config.js
If you get an error saying command karma not found
, run this command to resolve the issue:
npm install -g karma-cli
Node CLI packages are wrappers that look for a local version of a command in a projects node_modules
folder. This allows you to use a different version of the command in each project but still access it from the command line.
You may be wondering what the Debug button does. If you press it, you will be taken to a blank page (not very helpful). However, if you open the web inspector (Ctrl + Shift + i) while viewing the blank page, you will see the console.log output.
In a test you can add a value, object or function to window
and inspect it after the tests have run. You can also add the command debugger
in a test. This will allow you to step through code in the Chrome debugger (you will need to refresh the debugger window with the inspector open).
Unit tests do not allow HTTP requests. Often, we want to use a template URL in directives to avoid writing HTML as concatenated strings. If we do this, we need to pre-cache the templates in the template cache.
karma.conf
:
preprocessors: {
'app/views/**/*.html': ['ng-html2js'],
'test/views/**/*.html': ['ng-html2js'],
},
ngHtml2JsPreprocessor: {
prependPrefix: 'served/',
cacheIdFromPath: function(filepath) {
return filepath.replace(/app\/|.tmp\//,'');
},
// or define a custom transform function
moduleName: 'templates'
},
Install karma-ng-html2js-preprocessor
:
npm install karma-ng-html2js-preprocessor --save-dev
Prime the cache at by adding templates module to each spec file:
beforeEach(module('otherModules','templates'));
Jasmine is great, but it can be a bit verbose (you can learn Jasmine syntax here). Test Double, the authors of jasmine-given, recommend writing tests in CoffeeScript. To do this, edit the karma.conf
file:
preprocessors: {
'/**/*.coffee': ['coffee'], //add this line
//...
}
files: [
// ...
'test/spec/**/*.js',
'test/spec/**/*.coffee', //add this line
// ...
]
Then run the following command:
npm install -save-dev karma-coffee-preprocessor
jasmine-given allows us to write tests in the GIVEN
, WHEN
, THEN
format. It does not require CoffeeScript, but CoffeeScript makes it even more readable. jasmine-stealth adds a few helper functions which will help keep our test readable. To install we will use bower:
bower install jasmine-given jasmine-stealth --save-dev
Edit the karma.conf
file:
files: [
'app/bower_components/jasmine-given/dist/jasmine-given.js', //add
'app/bower_components/jasmine-stealth/dist/jasmine-stealth.js', //add
'app/bower_components/angular/angular.js',
//...,
],
}
Now, if we create the following spec it should run:
# test/spec/foo.coffee
describe "assigning stuff to this", ->
Given -> @number = 24
Given -> @number++
When -> @number *= 2
Then -> @number == 50
# or
Then -> expect(@number).toBe(50)
Below is an example of a directive unit test using jasmine-given:
To see this example as JavaScript copy and paste into Try CoffeeScript (very helpful for learning CoffeeScript).
describe "Directive: twinDollarInput", ->
Given -> module('emcdApp', 'templates')
Given -> @markup = """
<twin-dollar-input
ng-model="pence"
maxlength="{{max}}"
decimal-label="Amount in cents"
symbol="£"
>
</twin-dollar-input>
"""
Given ->
inject ($rootScope, $compile) =>
@scope = $rootScope.$new()
@compile = $compile
describe 'input should display pence', ->
Given -> @scope.pence = '100'
Given -> @scope.max = '11'
Given -> @subject = angular.element(@markup)
Given -> @compile(@subject)(@scope)
Given -> @scope.$digest()
Given -> @input = @subject.find('input')
Given -> @formatAmountEl = @subject.find('.simple-pattern--format-amount')
describe 'input should display cents', ->
Then -> @input.val() == '100'
describe 'left should show amount in dollars', ->
Then -> @formatAmountEl.text() == '$1.00'
describe 'updating cents input should update dollars', ->
When -> @input.val(400)
When -> @input.triggerHandler('change')
Then -> @formatAmountEl.text() == '$4.00'
describe 'dollar amount shoud split thousands by commas', ->
When -> @input.val(500000000)
When -> @input.triggerHandler('change')
Then -> @formatAmountEl.text() == '$5,000,000.00'
describe 'should trunate input to value to maxlength', ->
When -> @input.val(100000000000000000000)
When -> @input.triggerHandler('change')
Then -> @formatAmountEl.text() == '$100,000,000.00'
And -> @input.val().length == 11
describe 'change maxlength should trunate value to maxlength', ->
When -> @input.val(100000000000000000000)
When -> @input.triggerHandler('change')
When -> @scope.max = 3
When -> @scope.$digest()
Then -> @formatAmountEl.text() == '$1.00'
And -> @input.val() == '100'
describe 'change model should update view', ->
When -> @scope.pence = '10000'
When -> @scope.$digest()
Then -> @formatAmountEl.text() == '$100.00'
And -> @input.val() == '10000'
Currently used | Worth investigating |
---|---|
yeoman | lineman |
generator-angular | lineman-angular (gives: testem, jasmine-given, jasmine-stealth) |
jasmine | jasmine && jasmine-given && jasmine-stealth mocha |
grunt | broccoli || gulp || make |
karma | testem |
Sublime Text 3 | WebStorm || Atom |
Bower | browserify |
Compass | libsass |
sass | stylus || less be aware of BEM |
Bootstrap | Foundation 5 |
Phil Holden
Tags: angular
July 2nd 2014