app/
controllers/
index.js
views/
config/
node_modules/
package.json
public/
css/
js/
test/
npm init
npm init
`
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.
See npm help json
for definitive documentation on these fields
and exactly what they do.
Use npm install <pkg> --save
afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (bad-grizzly)
`
npm init
name: (bad-grizzly)
version: (0.0.0)
description:
entry point: (index.js)
entry point: (index.js) app/index.js
(Just keep hitting `Enter`. :-)
package.json
$ cat package.json
{
"name": "bad-grizzly",
"version": "0.0.0",
"description": "bad-grizzly ===========",
"main": "app/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/jedfoster/bad-grizzly.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/jedfoster/bad-grizzly/issues"
},
"homepage": "https://github.com/jedfoster/bad-grizzly"
}
json
"scripts": {
"start": "node app/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
app/
app/
$ mkdir app
$ touch app/index.js
app/
Hello World example from nodejs.org:
`javascript
var http = require('http');
http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
`
$ npm install --save express
Updates our package.json
$ cat package.json
{
"name": "bad-grizzly",
"version": "0.0.0",
...,
"dependencies": {
"express": "^4.8.7"
}
}
A simple Express app
`javascript
var express = require('express');
var app = express();
app.get('/', function(req, res) { res.send('Greetings, earthlings!'); });
app.listen('3000');
console.log("The server is now listening on port 3000");
`
$ npm start
<control>-C
in your terminal.
app/
`javascript
var express = require('express');
var app = express();
app.get('/', function(req, res) { res.send('Greetings, earthlings!'); });
app.listen('3000');
console.log("The server is now listening on port 3000");
`
Express provides routing methods that mirror HTTP verbs, here we used the GET
verb.
javascript
app.get('/', function(req, res) {
res.send('Greetings, earthlings!');
});
Other verbs, like POST
, map to similar methods:
javascript
app.post('/form', function(req, res) { ... });
We've hardcoded the port number in two places:
javascript
app.listen('3000');
console.log("The server is now listening on port 3000");
Use Node's global process
object to access the environment
`javascript
app.port = process.env.PORT || 3000;
app.listen(app.port);
console.log("The server is now listening on port %s", app.port);
`
This won't get us very far.
javascript
app.get('/', function(req, res) {
res.send('Greetings, earthlings!');
});
We want to write HTML, we just don't want to write HTML
$ npm install --save jade
Jade is a "terse and simple templating language" for Node.
We can write this:
jade
doctype html
html(lang="en")
head
title= pageTitle
body
h1 Jade template engine
#container.col
if youAreUsingJade
p You are amazing
else
p Get on it!
p.
Jade is a terse and simple templating language...
And it will become:
html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Jade</title>
</head>
<body>
<h1>Jade template engine</h1>
<div id="container" class="col">
<p>You are amazing</p>
<p>
Jade is a terse and simple templating language...
`javascript
var express = require('express');
var app = express();
app.set('view engine', 'jade');
app.set('views', __dirname + '/views');
`
javascript
$ mkdir app/views
$ touch app/views/index.jade
Open index.jade
and add this code:
jade
doctype html
html
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible' content='IE=edge')
title= pageTitle
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='stylesheet' href='/css/app.css')
body
h1= 'Greetings, ' + user
javascript
app.get('/', function(req, res) {
res.render('index', {
pageTitle: 'Hello, ' + process.env.USER,
user: process.env.USER
});
});
Install packages from:
Install CSS, JavaScript, Sass, images, you name it. If it goes on the front-end you can install it with Bower.
Install Bower:
$ npm install -g bower
Install a Bower package from the registry...
$ bower install <package>
...From a GitHub repo
$ bower install <github-user-name>/<repo-name>
...From a private git server
$ bower install https://git.drft.io/jedfoster/shoestring.git
Customize the install location with a <code>.bowerrc</code> file.
$ touch .bowerrc
json
{
"directory": "./public/vendor"
}
bower.json
$ touch bower.json
json
{
"name": "app-name",
"version": "1.0.0",
"dependencies": {
"shoestring-css": "latest"
}
}
$ bower install
It's a good idea to add node_modules
and bower_components
(or equivalent) to your .gitignore
`
node_modules
public/vendor
`
jade
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible' content='IE=edge')
title= pageTitle
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='stylesheet' href='/vendor/shoestring-css/shoestring.css')
`javascript
app.set('view engine', 'jade');
app.set('views', __dirname + '/views');
app.use(express.static(__dirname + '/../public'));
`
Restart the app and refresh.
$ npm install --save-dev mocha
$ npm install --save-dev supertest
javascript
"dependencies": {
"express": "^4.8.7",
"jade": "^1.6.0"
},
"devDependencies": {
"mocha": "^1.21.4",
"supertest": "^0.13.0"
}
npm test
Edit package.json
json
"test": "./node_modules/mocha/bin/mocha"
$ npm test
Mocha will execute all scripts in test/
.
test/
$ mkdir test
$ touch test/test.js
`javascript
var assert = require("assert");
describe('When adding 1 and 1', function() {
it('should return 2', function() {
assert.equal((1 + 1), 2);
});
});
`
$ npm test
`
$ npm test
bad-grizzly@0.0.0 test /Code/bad-grizzly ./node_modules/mocha/bin/mocha
When adding 1 and 1 ✓ should return 2
1 passing (5ms)
`
Be suspicious if your test passes on the first try.
Change line 3:
javascript
assert.equal((1 + 1), 3);
$ npm test
`
When adding 1 and 1
1) should return 2
0 passing (6ms) 1 failing
1) When adding 1 and 1 should return 2: AssertionError: 2 == 3 at Context.<anonymous> (/Code/bad-grizzly/test/test.js:5:12) at callFn (/Code/bad-grizzly/nodemodules/mocha/lib/runnable.js:249:21) at Test.Runnable.run (/Code/bad-grizzly/nodemodules/mocha/lib/runnable.js:242:7) at Runner.runTest (/Code/bad-grizzly/nodemodules/mocha/lib/runner.js:373:10) at /Code/bad-grizzly/nodemodules/mocha/lib/runner.js:451:12 at next (/Code/bad-grizzly/nodemodules/mocha/lib/runner.js:298:14) at /Code/bad-grizzly/nodemodules/mocha/lib/runner.js:308:7 at next (/Code/bad-grizzly/nodemodules/mocha/lib/runner.js:246:23) at Object.onImmediate (/Code/bad-grizzly/node_modules/mocha/lib/runner.js:275:5) at processImmediate as _immediateCallback
npm ERR! Test failed. See above for more details.
npm ERR! not ok code 0
`
A real test.
`javascript
var request = require('supertest');
var app = require('../app/index.js');
describe("POST /compile", function() {
it("responds successfully", function(done) {
request(app)
.post('/compile')
.expect(200, done);
});
});
`
SuperTest runs the app and simulates HTTP requests, just like we would make from the browser.
$ npm test
`
The server is now listening on port 3000
POST /compile 1) responds successfully
0 passing (4ms) 1 failing
1) POST /compile responds successfully: TypeError: Object #<Object> has no method 'address' at Test.serverAddress (/Code/bad-grizzly/node_modules/supertest/lib/test.js:57:18)
...
npm ERR! Test failed. See above for more details.
npm ERR! not ok code 0
`
We need to export
our app in app/index.js
.
Edit line 2:
javascript
var app = module.exports = express();
Now the app is a proper module and can be included into other scripts, like our test.
$ npm test
`
POST /compile
1) responds successfully
0 passing (18ms) 1 failing
1) POST /compile responds successfully:
Error: expected 200 "OK", got 404 "Not Found"
`
When practicing TDD, your tests should fail the first time, so you know that you have actually fixed the feature. This is the "red" in "red, green, refactor".
The test fails because we don't have a /compile
route. Add one:
javascript
app.post('/compile', function(req, res) {
res.status(200).end();
});
$ npm test
On its own, Express can't parse the body of a request.
$ npm install --save body-parser
$ npm install --save node-sass
We'll also need node-sass
.
Add this, starting on line 3.
`javascript
var nodeSass = require('node-sass');
var bodyParser = require('body-parser');
app.use(bodyParser.json());
`
Replace app.post('/compile')
with this:
`javascript
app.post('/compile', function(req, res) {
var stats = {};
var css = nodeSass.render({ data: req.body.compiler.sass + ' ', outputStyle: req.body.compiler.outputStyle, stats: stats,
success: function(css) {
res.json({
compiler: {
css: css,
stats: stats
}
});
},
error: function(error) {
res.status(500).send(error);
}
});
});
`
We need to submit some data with our test.
javascript
request(app)
.post('/compile')
.send({
compiler: {
sass: '$red: #f00;\n.test {\n color: $red;\n}',
outputStyle: 'compressed'
}
})
.set('Content-Type', 'application/json')
.expect(function(res) {
if(res.body.compiler.css != '.test{color:#f00;}') throw new Error('expected ".test{color:#f00;}", got "' + res.body.compiler.css + '"');
})
.expect('Content-Type', /json/)
.expect(200, done);
$ npm test
`
POST /compile
✓ responds successfully (40ms)
1 passing (43ms)
`
Here's a closer look at the JSON the server is returning:
json
{
"compiler": {
"css": ".test{color:#f00;}",
"stats": {
"entry": "data",
"start": 1410121632247,
"includedFiles": [],
"end": 1410121632248,
"duration": 1
}
}
}
When it creates a record, the Ember Data uses POST
, but subsequent updates use PUT
, which we have not accounted for.
We need to perform the same action with two different verbs.
`javascript
var compile = function(req, res) {
var stats = {};
var css = nodeSass.render({ data: req.body.compiler.sass, outputStyle: req.body.compiler.outputStyle, stats: stats,
success: function(css) {
res.json({
compiler: {
css: css,
stats: stats
}
});
},
error: function(error) {
res.status(500).send(error);
}
}); };
app.post('/compile', compile);
app.put('/compile', compile);
`
Install Ember and Ember Data with Bower:
$ bower install ember#~1.7.0 --save
$ bower install ember-data#~1.0.0-beta.9 --save
NOTE: The #~1.0.0-beta.9
is very important. Bower lets you specify a version for a package by adding a #
followed by the desired version.
$ bower install <package>#<version>
I spent a week troubleshooting issues with an outdated version of Ember Data I received when I ran just bower install ember-data
. I want to save you that grief.
app.js
Need application-specific JavaScript.
$ mkdir public/js
$ touch public/js/app.js
index.jade
`jade
doctype html
html
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible' content='IE=edge')
title= pageTitle
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='stylesheet' href='/vendor/shoestring-css/shoestring.css')
body
script(src='/vendor/jquery/dist/jquery.min.js')
script(src='/vendor/handlebars/handlebars.min.js')
script(src='/vendor/ember/ember.js')
script(src='/js/app.js')
`
Ember.Create()
Open js/app.js
and add this:
javascript
var App = Ember.Application.create();
You now have an Ember application.
index.jade
Let's mock up our app.
`jade
.left
h3 Input
textarea(name='sass')
select(name='outputStyle') option(value='nested') Nested option(value='compressed') Compressed
.right h3 Output
pre code
p
`
`jade
script(type='text/x-handlebars')
.left
h3 Input
{{textarea value=sass}}
select(name='outputStyle')
option(value='nested') Nested
option(value='compressed') Compressed
.right h3 Output
pre
code
{{sass}}
p
`
Ember will render the contents of this script tag to the page when it loads.
Try typing something into our input field.
With that simple markup we bound our input to another element on the page.
Congratulations, you just wrote your first Ember template.
Ember follows the Model-View-Controller pattern.
Some core concepts:
Routes are the URLs for the various states of the objects in your app.
Ember places significant importance on the URL.
A route links a model with its template.
The router is a manifest of the routes in your app. It also keeps the URL in the browser in sync with your app.
Technically, a route is optional, as we've already proven. However, in practice, you'll probably want to define a route explicitly.
Templates are the V in Ember's MVC. A template describes the UI for a model, and is written in Handlebars.
Templates update themselves as their model's data changes.
Models store the persistent state of objects in our app. Commonly, a model's data is retrieved from a database through a REST API.
A controller acts an intermediary between the model and template.
Allows you to manipulate data either before it is displayed or before it is stored.
Controllers are optional; if you don't create one, Ember will auto-generate one for you in memory.
Views are primarily used for complex, custom event handling and creating reusable components.
Ember includes a number of views for HTML elements such as form inputs and links.
A component is a standalone view. It enables you to build reusable elements and can help simplify your templates.
Handlebars allows you to "register" helper functions that modify data before it's displayed. Useful for date formatting for example.
Ember favors convention over configuration. Ember's naming conventions glue your app together without unnecessary boilerplate.
For a URL /users
, Ember will look for these objects:
App.UsersRoute
App.UsersController
users
If you go against Ember's naming conventions, you're going to have a bad time. Just let it happen.
Conventions enable magic! We've already seen some of that magic happen with our simple template.
Ember gives you some things for free:
App.ApplicationRoute
App.ApplicationController
application
templateOur app is leveraging these conventions:
jade
script(type='text/x-handlebars')
.left
h3 Input
Our template doesn't have a name, so Ember uses it as the application
template. Ember also created a route and controller for us.
Let's use the index
template in index.jade
:
jade
script(type='text/x-handlebars' id='index')
.left
While we're at it, let's add an App.IndexRoute
with a model to app.js
:
`javascript
var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function () {
return { };
}
});
`
`jade
script(type='text/x-handlebars' id='index')
.left
h3 Input
{{textarea value=sass}}
{{view Ember.Select content=outputStyles value=selectedOutputStyle}}
`
We'll need to create a controller to set up the values.
`javascript
App.IndexController = Ember.ObjectController.extend({
outputStyles: ['nested', 'compressed'],
selectedOutputStyle: 'nested'
});
`
Add it to index.jade
.
jade
script(src='/vendor/jquery/dist/jquery.min.js')
script(src='/vendor/handlebars/handlebars.min.js')
script(src='/vendor/ember/ember.js')
script(src='/vendor/ember-data/ember-data.js')
script(src='/js/app.js')
`javascript
var App = Ember.Application.create();
App.Compiler = DS.Model.extend({
sass: DS.attr('string'),
outputStyle: DS.attr('string'),
css: DS.attr('string'),
stats: DS.attr()
});
`
Note, this contains attributes for both the request and response.
The objects that our API is expecting:
`json
// The request
{
"compiler": {
"sass": "$red: #f00;\n.test {\n color: $red;\n}",
"outputStyle": "compressed"
}
}
// The response
{
"compiler": {
"css": ".test{color:#f00;}",
"stats": {
...
}
}
}
`
The object Ember will use:
`javascript
var App = Ember.Application.create();
App.Compiler = DS.Model.extend({
sass: DS.attr('string'),
outputStyle: DS.attr('string'),
css: DS.attr('string'),
stats: DS.attr()
});
`
Add this model to the route:
javascript
App.IndexRoute = Ember.Route.extend({
model: function () {
return this.store.createRecord('compiler');
}
});
This creates an empty instance of our model.
RESTAdapter
Ember Data's RESTAdapter
tries to infer URLs from model names.
The default path is the plural form of the model name.
Given a model, Post
, RESTAdapter
would make requests to /posts
Given our Compiler
model, it would try to use /compilers
. But that won't work...
Our model isn't conventional, so we need to set the URL explicitly.
`javascript
var App = Ember.Application.create();
App.ApplicationAdapter = DS.RESTAdapter.extend({
buildURL: function(type, id, record) {
return '/compile'
}
});
`
By defining our own buildURL
method we can force it to use the the path we've already built.
Let's add a button to our form so that we can manually invoke the API call for debugging.
`jade
{{view Ember.Select content=outputStyles value=selectedOutputStyle}}
<button {{action 'compile'}}> Go</button>
.right
`
We use the built-in helper action
which fires an event, compile
, on click.
Listen for the compile
event in our controller:
`javascript
App.IndexController = Ember.ObjectController.extend({
outputStyles: ['nested', 'compressed'],
selectedOutputStyle: 'nested',
actions: {
compile: function() {
alert("Button clicked");
}
}
});
`
Refresh, paste .test { color: red; }
into the into the input, and click "Go". Yay!
Now, let's actually POST
input to the server:
javascript
actions: {
compile: function() {
this.get('model').save()
.then(function(response) {
console.log('Success: ', response);
});
}
}
Refresh. Paste .test { color: red; }
. Click.
Now in the console, we should see something like:
Let's show the CSS returned by the API:
jade
pre
code
{{css}}
Our output is updated whenever we change the value of the input.
Because the API doesn't return a value for sass
, the model updates itself with its blank default value.
When we click "Go", our input is cleared. We need our input to not be so tightly linked to the model.
Let's decouple our input template from the model.
handlebars
{{textarea value=sassInput}}
Now, set the model attribute to the value in the template.
`javascript
compile: function() {
this.set('sass', this.get('sassInput'));
this.get('model').save()
.then(function(response) {
console.log('Success: ', response);
});
}
`
Now, as we click "Go" our input remains.
In the template, we access our model's properties directly by name {{sassInput}}
, {{sass}}
.
But in our controller we access those properties with this.get('sassInput')
or this.set('sass')
. Why?
Data binding, one of Ember's most powerful features.
Earlier, binding allowed us—with three lines of code!—to mirror our textarea.
get()
and set()
notifies observing objects when a property changes.
Which leads us to...
Our input should send itself whenever it is changed.
Wrap our compile
action in an observer:
`javascript
selectedOutputStyle: 'nested',
compileOnEntry: function() {
this.send('compile');
}.observes('sassInput'),
`
Refresh. Paste $red: blue; .test { color: $red; }
.
Automatic updating FTW!
outputStyle
We aren't sending a value for outputStyle
.
Easy fix:
javascript
this.set('sass', this.get('sassInput'));
this.set('outputStyle', this.selectedOutputStyle);
We should observe selectedOutputStyle
and recompile when that changes:
javascript
compileOnEntry: function() {
this.send('compile');
}.observes('sassInput', 'selectedOutputStyle'),
Ember is making a request to /compile
immediately after the page loads.
Wrap save()
in a conditional that checks if sassInput
is blank:
javascript
if(! Ember.isBlank(this.get('sassInput'))) {
this.get('model').save()
.then(function(response) {
console.log('Success: ', response);
});
}
We should do something with unsuccessful responses.
save()
returns a promise. We already have the "success" callback argument to then()
, so all we have to do is add a "fail" callback:
`javascript
var self = this;
...
if(! Ember.isBlank(this.get('sassInput'))) {
this.get('model').save()
.then(function(response) {
console.log('Success: ', response);
},
function(err) {
console.log('Error: ', err);
self.set('css', err.responseText);
});
}
`
Let's show a previously saved snippet of Sass, just like on SassMeister.com.
Add a route for /gist
`javascript
var App = Ember.Application.create();
App.Router.map(function() {
this.resource('gist', { path: '/gist/:gist_id' });
});
`
Ember's router uses URL fragments (hashes) to identify URLs.
Our app will now respond to a URL like /#/gist/1
.
Hashes are ugly.
Ember makes it easy to change.
javascript
App.Router.reopen({
location: 'history'
});
If we visit /gist/1
right now, our server will return 404.
Stop the server and modify the index route to this:
`javascript
var index = function(req, res) {
res.render('index', {
pageTitle: 'Hello, ' + process.env.USER,
user: process.env.USER
});
};
app.get('/', index);
app.get('/gist/*', index);
`
Just as we refactored the /compile
route to respond to both POST
and PUT
, we refactor /
so we can reuse the functionality for /gist/1
.
We don't have a template for this route, so we won't seeing anything if go to /gist/1
.
`jade
script(type='text/x-handlebars' id='gist')
.left
h3 Sass
pre code {{sass}}
.right h3 CSS
pre
code
{{css}}
script(src='/vendor/jquery/dist/jquery.min.js')
`
We also don't have a model associated with this route.
Refactor App.Compiler
, replacing it with this:
`javascript
var schema = {
sass: DS.attr('string'),
css: DS.attr('string'),
outputStyle: DS.attr('string'),
stats: DS.attr()
};
App.Compiler = DS.Model.extend(schema);
App.Gist = DS.Model.extend(schema);
`
Refactoring code is a crucial practice. Take every opportunity to break your code into reusable chunks. It keeps your codebase clean and maintainable.
Still not seeing anything at /gist/1
.
Let's have a look at the console.
Ember is trying to load the model with GET /compile
, but that route only responds to POST
and PUT
.
Our route is /gist
; so Ember passes gist
as the type
argument to buildURL
. We can conditionally return a different URL for different types of models.
`javascript
App.ApplicationAdapter = DS.RESTAdapter.extend({
buildURL: function(type, id, record) {
if(type == 'gist') return 'http://gist.drft.io/gists/' + id + '.json';
return '/compile';
}
});
`
I created an Amazon S3 bucket that has a few JSON files that match the models we defined in our Ember app.
Refresh, and we now see our template populated with JSON loaded from a remote server. Whee!
Remember how Ember gives us an `application` template for free? Let's look a gift horse in the mouth and add our own `application` template.
`jade
body
script(type='text/x-handlebars') nav {{#link-to 'index'}}Home{{/link-to}} {{#link-to 'gist' 1}}Gist 1{{/link-to}} {{#link-to 'gist' 2}}Gist 2{{/link-to}} {{#link-to 'gist' 3}}Gist 3{{/link-to}}
{{outlet}}
`
{{outlet}}
is a placeholder for other templates.
{{#link-to}}
is a built-in Ember helper that builds links... to stuff.
You've just built a Node.js server with a REST API and an Ember front end. Along the way you used: the command line, npm, Node, bower, Express, LibSass, SCSS, HTTP verbs, ports, TDD, mocha, supertest, JSON, MVC, Jade, HTML, CSS, Ember, Ember Data, Handlebars, AJAX, URLs, and the history API.
And all in under 200 lines of code!