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.jsscripts.jsmain.cssindex.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 https://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