Franky Chung

Use Yeoman to start a new Angular project with CoffeeScript, Sass, and Jade

Angular is amazing. But what makes it even more fun is the complementary Yeoman tool, mainly the Grunt component that makes developing a joy, such as an awesome build, live reloading, and easy testing.

This article is up to date up to November 17, 2013. Let me know if anything here needs updating and I’ll get on it.

Install Yeoman

npm install -g yo

I also had to install generator-karma for this to work:

npm install -g generator-karma

Install angular generator:

npm install -g generator-angular

Create a folder and cd into it:

yo angular

For the options you can choose here, I don’t use Twitter Bootstrap so I chose no. I also don’t use angular-resource, cookies, or route (use ui-router). However, install ng-route for now, or else the default installation won’t work. I do use sanitize though!

At this point, grunt server should work (By the way, at this time, it’s using Angular v1.2.1)

Compass and Sass

Let’s install Compass. Since we turned off Twitter Bootstrap, we need to do this. If you did enable Twitter Bootstrap, there was an option to use Compass, which is what we want to use too (it’ll compile our SASS for us, and it’s fucking convenient). I am just copying this from the Gruntfile if generated with Bootstrap + Compass, so if you chose that option, you don’t need to do anything for this section.

In Gruntfile, you need to add the following to the watch config object (I placed it under the coffeeTest config):

compass: {
	files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
	tasks: ['compass:server', 'autoprefixer']
},

Then you need to add some config to the compass object:

compass: {
	options: {
		sassDir: '<%= yeoman.app %>/styles',
		cssDir: '.tmp/styles',
		generatedImagesDir: '.tmp/images/generated',
		imagesDir: '<%= yeoman.app %>/images',
		javascriptsDir: '<%= yeoman.app %>/scripts',
		fontsDir: '<%= yeoman.app %>/fonts',
		importPath: '<%= yeoman.app %>/bower_components',
		httpImagesPath: '/images',
		httpGeneratedImagesPath: '/images/generated',
		httpFontsPath: '/fonts',
		relativeAssets: false
	},
	dist: {},
	server: {
		options: {
			debugInfo: true
		}
	}
},

I placed this under the coffee config.

Finally, in the concurrent config, you will need to add compass into the three options. I’ve pasted the whole concurrent config here:

concurrent: {
	server: [
		'coffee:dist',
		'compass:server',
		'copy:styles'
	],
	test: [
		'coffee',
		'compass',
		'copy:styles'
	],
	dist: [
		'coffee',
		'compass:dist',
		'copy:styles',
		'imagemin',
		'svgmin',
		'htmlmin'
	]
},

That should be it for installing Compass! Let’s test it out. In your app/style directory, delete main.css and create main.sass (or main.scss, if you prefer). Put something in there such as:

body
	background: red

Now restart grunt server. Your background should be red. To test that LiveReload works (I love how we did not do anything to make this work), change that red property to green and see if it automatically updates the style without reloading. YES. Now you probably want to get rid of that red background.

Now you can add Sass files with impunity. And use Compass while you’re at it! Fuck yeah. I recommend inuit.css, by the way, but whatever floats your boat.

CoffeeScript

Let’s test CoffeeScript. Change app/scripts/controllers/main.js to app/scripts/controllers/main.coffee and replace the contents of that file with:

'use strict'

angular.module('someApp').controller 'MainCtrl', ($scope) ->
	$scope.awesomeThings = ['HTML5 Boilerplate', 'AngularJS', 'Karma']

In main.html, it seems that it doesn’t use the awesomeThings property anymore, so let’s add it in there just to make sure we have the property on the scope:

<ul>
	<li ng-repeat="thing in awesomeThings">
		{{ thing }}
	</li>
</ul>

There it is! Now add another item in the awesomeThings array, save, and see if LiveReload works — YES. You can change the other generated JavaScript to coffee if you want. (We’ll be changing the test file soon.)

Jade

Let’s add Jade. I love Jade. These instructions came from here.

First let’s add it to our package.json:

npm install grunt-contrib-jade --save-dev

The Gruntfile will automatically load this one. Now we need to modify our Gruntfile a bit. In the watch config, we will add:

jade: {
	files: ['<%= yeoman.app %>/{,*/}*.jade'],
	tasks: ['jade']
},

(I’ve put it under the Compass config.) Also, in the LiveReload config, we need to change the files array slightly:

files: [
	'{.tmp,<%= yeoman.app %>}/{,*/}*.html',
	'.tmp/styles/{,*/}*.css',
	'{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js',
	'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]

The change is in the second line there, because now we need LiveReload to look at the .tmp folder for html files that Jade will generate.

Next in our concurrent config, we will add Jade. Here is the full concurrent config:

concurrent: {
	server: [
		'coffee:dist',
		'compass:server',
		'jade',
		'copy:styles'
	],
	test: [
		'coffee',
		'compass',
		'copy:styles'
	],
	dist: [
		'coffee',
		'compass:dist',
		'jade',
		'copy:styles',
		'imagemin',
		'svgmin',
		'htmlmin'
	]
},

This should be enough to make it work. Let’s test it out! Lets turn main.html into main.jade:

.header
	ul.nav.nav-pills.pull-right
		li.active
			a(href='#') Home
		li
			a(href='#') About
		li
			a(href='#') Contact
	h3.text-muted someApp

.jumbotron
	h1 'Allo, 'Allo!
	p.lead Always a pleasure scaffolding your apps.
	p
		a.btn.btn-lg.btn-success(href='#') Splendid!
	ul
		li(ng-repeat='thing in awesomeThings')
			| 

.row.marketing
	h4 HTML5 Boilerplate
	p
		| HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.
	h4 Angular
	p
		| AngularJS is a toolset for building the framework most suited to your application development.
	h4 Karma
	p Spectacular Test Runner for JavaScript.

.footer
	p ♥ from the Yeoman team

Restart grunt server and see if it loads. YES! Now change the Jade file to see if LiveReload works. YES!

We’re almost there!

Karma

Let’s set up karma:watch. I want Karma to run all my unit tests every time I save any CoffeeScript files. I found this here.

Add this bit above your initConfig:

var karmaConfig = function(configFile, customOptions) {
	var options = { configFile: configFile, keepalive: true };
	var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' };
	return grunt.util._.extend(options, customOptions, travisOptions);
};

Then in your karma config, we need to add a watch task. Here is my whole karma config:

karma: {
	unit: {
		configFile: 'karma.conf.js',
		singleRun: true
	},
	watch: { options: karmaConfig('karma.conf.js', { singleRun:false, autoWatch: true}) }
},

Since we’ll be generating our test files at the same time as we have our development server running, we need to edit our concurrent config server array. Change coffee:dist to coffee.

Our updated concurrent config:

concurrent: {
	server: [
		'coffee',
		'compass:server',
		'jade',
		'copy:styles'
	],
	test: [
		'coffee',
		'compass',
		'copy:styles'
	],
	dist: [
		'coffee',
		'compass:dist',
		'jade',
		'copy:styles',
		'imagemin',
		'svgmin',
		'htmlmin'
	]
},

Sweet! Let’s change our generated test to CoffeeScript. Change test/spec/controllers/main.js to test/spec/controllers/main.coffee and replace the contents with:

'use strict'

describe 'Controller: MainCtrl', ->

	# load the controller's module
	beforeEach module 'someApp'

	MainCtrl = {}
	scope = {}

	# Initialize the controller and a mock scope
	beforeEach inject ($controller, $rootScope) ->
		scope = $rootScope.$new()
		MainCtrl = $controller 'MainCtrl'
			$scope: scope

	it 'should attach a list of awesomeThings to the scope', ->
		expect(scope.awesomeThings.length).toBe 3

Next, we’ll update our app/scripts/app.js to app/scripts/app.coffee so Karma can bootstrap the application:

'use strict'

angular.module('someApp', [
	'ngSanitize', 
	'ngRoute'
])

.config ($routeProvider) ->

	$routeProvider.when('/',
		templateUrl: 'views/main.html'
		controller: 'MainCtrl'
	).otherwise redirectTo: '/'

Next, we need to edit our karma.conf.js. In the config, in the files array, change it to:

files: [
	'app/bower_components/angular/angular.js',
	'app/bower_components/angular-mocks/angular-mocks.js',
	'app/bower_components/angular-sanitize/angular-sanitize.js',
	'app/bower_components/angular-route/angular-route.js',
	// 'app/scripts/*.js',
	// 'app/scripts/**/*.js',
	'.tmp/scripts/*.js',
	'.tmp/scripts/*/**.js',
	'test/mock/**/*.js',
	// 'test/spec/**/*.js'
	'.tmp/spec/**/*.js'
]

The commented out lines are what used to be there. This is assuming all your files are in CoffeeScript and are being generated into the .tmp folder.

Run grunt karma:watch somewhere — I prefer another pane in tmux but it could be as simple as opening another tab in terminal. If you edited the awesomeThings array earlier, it should fail. Edit the test so it passes, save, and watch it pass! Now developing any app is going to be so much fun.

ui-router

I haven’t even bothered to learn ng-route because ui-router is amazing. Let’s use it:

bower install --save angular-ui-router

Now let’s change index.html to index.jade and replace angular-route.js in the modules.js build block with angular-ui-router.js:

!!! 5
html.no-js
	head
		meta(charset='utf-8')
		meta(http-equiv='X-UA-Compatible', content='IE=edge')
		title Some app
		meta(name='description', content='')
		meta(name='viewport', content='width=device-width')
		// Place favicon.ico and apple-touch-icon.png in the root directory 

		// build:css(.tmp) styles/main.css
		link(rel='stylesheet', href='styles/main.css')
		// endbuild

	body(ng-app='someApp')
		.container(ui-view)

		script(src='bower_components/angular/angular.js')

		// build:js scripts/modules.js
		script(src='bower_components/angular-sanitize/angular-sanitize.js')
		script(src='bower_components/angular-ui-router/release/angular-ui-router.js')
		// endbuild

		// build:js({.tmp,app}) scripts/scripts.js
		script(src='scripts/app.js')
		script(src='scripts/controllers/main.js')
		// endbuild

Note I’ve also changed ng-view to ui-view in the container div.

Now to go your app.coffee and replace ngRoute with ui.router. Then update the route definition using ui-router’s syntax.

'use strict'

angular.module('someApp', [
	'ngSanitize',
	'ui.router'
])
	
.config ($stateProvider, $urlRouterProvider) ->

	$urlRouterProvider.otherwise '/'

	$stateProvider.state 'main'
		url: '/'
		templateUrl: 'views/main.html'
		controller: 'MainCtrl'

Finally, edit your karma.conf.js to use ui-router so your tests pass!

files: [
	'app/bower_components/angular/angular.js',
	'app/bower_components/angular-mocks/angular-mocks.js',
	'app/bower_components/angular-sanitize/angular-sanitize.js',
	'app/bower_components/angular-ui-router/release/angular-ui-router.js',
	'.tmp/scripts/*.js',
	'.tmp/scripts/*/**.js',
	'.tmp/spec/**/*.js'
],

Restart your grunt server and grunt karma:watch and you’re golden!

The build

We want to make sure our build will minifies and concatenates all our assets according to our build blocks.

Right off the bat, your main.css, modules.js, and scripts.js should work. Now we’re just missing our html files since we use Jade.

This is because htmlmin needs to run after Jade in our concurrent:dist task. We’ll make a dist2 task and move htmlmin into there so it always happens after:

concurrent: {
	server: [
		'coffee',
		'compass:server',
		'jade',
		'copy:styles'
	],
	test: [
		'coffee',
		'compass',
		'copy:styles'
	],
	dist: [
		'coffee',
		'compass:dist',
		'jade',
		'copy:styles',
		'imagemin',
		'svgmin',
	],
	dist2: [
		'htmlmin'
	]
},

Then we’ll add it to our build task. (We also have to move useminPrepare after the Jade files are converted to html, so we will move it after the concurrent tasks):

grunt.registerTask('build', [
	'clean:dist',
	'concurrent:dist',
	'concurrent:dist2',
	'useminPrepare',
	'autoprefixer',
	'concat',
	'ngmin',
	'copy:dist',
	'cdnify',
	'cssmin',
	'uglify',
	'rev',
	'usemin'
]);

We also have to edit useminPrepare to find the right html file:

useminPrepare: {
	html: '.tmp/index.html',
	options: {
		dest: '<%= yeoman.dist %>'
	}
},

And since we’re reading our html file from .tmp, we need to change our modules.js’s build block to know to still read from app directory:

// build:js(app) scripts/modules.js
script(src='bower_components/angular-sanitize/angular-sanitize.js')
script(src='bower_components/angular-ui-router/release/angular-ui-router.js')
// endbuild

Finally we’ll have to edit our htmlmin config to look in the .tmp directory instead of the app directory for our html files.

Change your htmlmin config:

files: [{
	expand: true,
	cwd: '.tmp',
	src: ['*.html', 'views/*.html'],
	dest: '<%= yeoman.dist %>'
}]

We’re finally done! Run grunt build, then close grunt server and run grunt server:dist to make sure everything worked!

Shared November 17, 2013