Skip to content

Instantly share code, notes, and snippets.

@craigspaeth
Created January 25, 2012 21:53
Show Gist options
  • Save craigspaeth/1679031 to your computer and use it in GitHub Desktop.
Save craigspaeth/1679031 to your computer and use it in GitHub Desktop.
Nodes and Backbones and Zombies, oh my!

Javascript

Some examples to be run inside the node console.

Functional, closures, lambdas

// First class players
var fn = function () {
  return 'Yeah ima variable';
}
console.log(fn());
console.log(fn.toString());

// Closures
var closured = function () {
  var foo = 'bar';
  return function () {
    return foo += 'baz';
  }
}
console.log(closured()());
console.log(closured().toString());

// Ruby vs. Javascript

/* Blocks

array = [1,2,3,4]
puts array.map { |n| n * n }

*/

var array = [1,2,3,4];
console.log(array.map(function(num) { return num * num }));

/* Procs & Lambdas

array_1 = [1,2,3,4]
array_2 = [2,3,4,5]
square = lambda { |n| return n * n }
puts array_1.map { |n| square n }
puts array_2.map { |n| square n }

*/

var array_1 = [1,2,3,4];
var array_2 = [2,3,4,5];
var square = function (n) { return n * n };
console.log(array_1.map(square));
console.log(array_2.map(square));

Scope & This

// 
// Var
// 
var imaGlobal = 'Im global!';

(function foo(){
  imaGlobal = "I'm definitley a global!";
})();

(function foo(){
  var imaLocal = "Ima local!";
  imaGlobalToo = "Muhahahaha! Don't forget to var!";
  console.log(imaLocal);
})();

console.log(imaGlobal);
console.log(imaGlobalToo);
console.log(typeof imaLocal);

// 
// This
// 
this.scope = 'global';
this.whatScopeAmIIn = function () {
  console.log("I'm in the " + this.scope + " scope!");
};

function lolThisIsJavaRite() {
  str = "Lemme just pass around 'this'... " + typeof this.scope;
  if (typeof this.scope === 'undefined') str += ", DOH!";
  console.log(str);
}

craig = {
  name: 'Craig',
  favoriteLanguage: 'coffeescript',
  scope: 'craig',
  whatScopeAmIIn: this.whatScopeAmIIn
}

this.whatScopeAmIIn();
lolThisIsJavaRite();
this.whatScopeAmIIn.call(craig);
craig.whatScopeAmIIn();

Everything is an object

var kitten = {
  name: 'Fluffy',
  cute: true,
  purr: function() {
    console.log("Hi! I'm " + this.name  + " purrrr, I'm so full of love!");
  }
};

var thingsCraigLoves = [
  'coffeescript',
  'beer',
  'burritos'
];

console.log(typeof thingsCraigLoves); // Umm wahh?
console.log(typeof kitten);

Object.prototype.doesCraigLoveMe = function() {
  var str = '';
  for (key in this) {
    if (typeof this[key] === 'string') str += this[key];
  };
  if (thingsCraigLoves.indexOf(str) !== -1) return true;
  if (this.cute) return true;
  return false;
};
console.log('burritos'.doesCraigLoveMe());
console.log(kitten.doesCraigLoveMe());
console.log('Capybara'.doesCraigLoveMe());

Prototypical Inheritance

Objects inherit directly from other objects (functions are objects with the "prototype" property)

function Dog(name) {
  this.name = name;
};
Dog.prototype.bark = function() {
  console.log('Ruff, my name is ' + this.name + ". I'm a " + (this.type || 'hopeless mutt'));
};
var sparky = new Dog('Sparky');
sparky.bark();
sparky.type = 'pug';
sparky.bark();

function BassetHound(name) {
  Dog.call(this, name);
  this.type = 'basset hound';
};
BassetHound.prototype.__proto__ = Dog.prototype;
var joe = new BassetHound('Joe');
joe.bark();

Events

var EventEmitter = require('events').EventEmitter;

var chopt = new EventEmitter();
chopt.choppers = [
  {
    name: 'Speedy',
    chopsPerSecond: 1000
  },
  {
    name: 'Sleepy',
    chopSaladIn: 4000
  },
  {
    name: 'Newbie',
    chopSaladIn: 2000
  },
  {
    name: 'Beast Mode',
    chopSaladIn: 500
  },
  {
    name: 'Capybara',
    chopSaladIn: 8000
  }
];

chopt.takeOrder = function(salad) {
  var chopper = this.choppers[Math.floor(Math.random() * this.choppers.length)];
  console.log("You want a " + salad + "? Coming right up, over to " + chopper.name + " please... Next!");
  this.chopSalad(salad, chopper);
};

chopt.chopSalad = function(salad, chopper) {
  _self = this;
  setTimeout(function() {
    _self.emit("finishedChoppingSalad", salad, chopper);
  },chopper.chopSaladIn);
};

chopt.on("finishedChoppingSalad", function(salad, chopper) {
  console.log(chopper.name + " finished your: " + salad + ", next in line please!");
});

chopt.takeOrder("Harvest Cobb");
chopt.takeOrder("Thai Cobb");
chopt.takeOrder("Palm Beach Shrimp");
chopt.takeOrder("Mexican Ceaser");
chopt.takeOrder("Grilled Asian");
chopt.takeOrder("Steakhouse");
chopt.takeOrder("Kebab Cobb");
console.log("LET THE CHOPPING BEGIN!\n\n");

jQuery

Simplify the DOM.

$('h1.title').fadeIn(function() {
  $(this).addClass('visible').text('Oh hai!');
});

Underscore

Utility library to make javascript's lack of core functionality not suck so much.

_.map([1, 2, 3], function(num){ return num * 3; });
_.flatten([1, [2], [3, [[[4]]]]]);

Why Coffeescript

JSLint

Code quality tool by Douglas Crockford to make sure you're not making common JS mistakes like forgetting to var. (All of your coffeescript will pass JSLint).

Loops & comprehensions

ECMAScript 5 supports things like Array#map, Array#indexOf, but while browsers catch up you have to shim support for this. Coffeescript comes with it's own syntactical tricks to make this quite nice.

coffeescript

dogs = ['basset hound', 'cute pug', 'ugly poodle', 'cute boxer', 'ugly beagle', 'cute newfounland']

'basset hound' in dogs
# true

("I love #{dog}s" for dog in dogs when dog.indexOf('cute') isnt -1)
# [ 'I love cute pugs', 'I love cute boxers', 'I love cute newfounlands' ]

javascript

var dog, dogs, _i, _len,
  __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };

dogs = ['basset hound', 'cute pug', 'ugly poodle', 'cute boxer', 'ugly beagle', 'cute newfounland'];

__indexOf.call(dogs, 'basset hound') >= 0;

for (_i = 0, _len = dogs.length; _i < _len; _i++) {
  dog = dogs[_i];
  if (dog.indexOf('cute') !== -1) "I love " + dog + "s";
}

Stick to the good parts of JS

Scope

Polluting the global namespace? Nah everything is wrapped in an anonymous function.

coffeescript

foo = 'bar'

javascript

(function() {
  var foo;

  foo = 'bar';

}).call(this);

Where'd my this go!?

coffeescript

foo = =>
  console.log this

javascript

(function() {
  var foo,
    _this = this;

  foo = function() {
    return console.log(_this);
  };

}).call(this);

Truthy & Falsey

"Because the == operator frequently causes undesirable coercion, is intransitive, and has a different meaning than in other languages, CoffeeScript compiles == into ===, and != into !==. In addition, is compiles into ===, and isnt into !==""

Semi-colon insertion

JS compiler will go line by line and insert a semi-colon if you're missing it. This can often cause unwanted behavior if you don't intentionally close your lines with ;. Better yet, without JSLint, things things pass by without warning.

Oh did I mention semi-colons are ruby/python-like optional in coffeescript?

Existential Operator

false, null, undefined, NaN

"It's a little difficult to check for the existence of a variable in JavaScript. if (variable) ... comes close, but fails for zero, the empty string, and false."

coffeescript

foo?

javascript

typeof foo !== "undefined" && foo !== null;

Class Sugar

coffeescript

class Animal
  constructor: (@name) ->

  move: (meters) ->
    alert @name + " moved #{meters}m."

class Snake extends Animal
  move: ->
    alert "Slithering..."
    super 5

class Horse extends Animal
  move: ->
    alert "Galloping..."
    super 45

sam = new Snake "Sammy the Python"
tom = new Horse "Tommy the Palomino"

sam.move()
tom.move()

javascript

var Animal, Horse, Snake, sam, tom,
  __hasProp = Object.prototype.hasOwnProperty,
  __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };

Animal = (function() {

  function Animal(name) {
    this.name = name;
  }

  Animal.prototype.move = function(meters) {
    return alert(this.name + (" moved " + meters + "m."));
  };

  return Animal;

})();

Snake = (function(_super) {

  __extends(Snake, _super);

  function Snake() {
    Snake.__super__.constructor.apply(this, arguments);
  }

  Snake.prototype.move = function() {
    alert("Slithering...");
    return Snake.__super__.move.call(this, 5);
  };

  return Snake;

})(Animal);

Horse = (function(_super) {

  __extends(Horse, _super);

  function Horse() {
    Horse.__super__.constructor.apply(this, arguments);
  }

  Horse.prototype.move = function() {
    alert("Galloping...");
    return Horse.__super__.move.call(this, 45);
  };

  return Horse;

})(Animal);

sam = new Snake("Sammy the Python");

tom = new Horse("Tommy the Palomino");

sam.move();

tom.move();

Backbone.js

MVC for the client side. Not your typical idea of MVC though.

"When working on a web application that involves a lot of JavaScript, one of the first things you learn is to stop tying your data to the DOM. It's all too easy to create JavaScript applications that end up as tangled piles of jQuery selectors and callbacks, all trying frantically to keep data in sync between the HTML UI, your JavaScript logic, and the database on your server. For rich client-side applications, a more structured approach is often helpful."

Models

class App.Models.Artwork extends Vertebrae.Model
  
  Backbone.Undoable @
  
  relations:
    'artist': -> App.Models.Artist
    'partner': -> App.Models.Partner
    'edition_sets': -> App.Collections.EditionSets
    'images': -> App.Collections.ArtworkImages
    'flags': -> App.Collections.Flags
  
  initialize: ->
    _.defer @setRelated
    @bind 'change', => _.defer @setRelated
  # ...

Just like models in Rails, Models in Backbone is where all of your business logic and server interaction should be. However, unlike Rails, models will be fairly skinny compared to views, because a lot of this logic is already on the server side in the Rails models and often a model only needs to grab JSON from the server to populate it's properties.

Collections

class Artworks extends Backbone.Collection
  model: Artwork

Collections are simply a collection of models with some extra functionality on top. This will generally be the thinnest part of a backbone application. Often needing no more than...

Views

class App.Views.ArtworksIndex extends Backbone.View
  
  el: '#artworks'
  
  initialize: ->
    @collection = new App.Collections.Artworks()

Backbone Views are very much un-like traditional Views in frameworks like Ruby on Rails. According to the Backbone documentation: Views are a "logical, re-usable piece of UI". Simply put, Views act as controllers (storing event handlers like 'click .save': saveArtwork) and a chunk of UI logic for one 'section' of your UI (e.g. Header, SearchResultRow).

It's best practice to have a View be associated with one Model or one Collection and one DOM element (including it's children of course). If it encompasses more than this, it probably needs to be refactored in to multiple views.

It is also a good practice to bind events on models or collections to callbacks in a view. For instance, clicking "Save to my collection" in one view (The artwork detail page) may want to update another view (The user collection tray). To simply and cleanly accomplish this you can bind a callback in the "collection tray" view to the current user's collection "add" event.

# Clicking the save artwork button in the artwork detail view adds the model to the user collection
$('.save_artwork').click -> App.currentUser.get('collections').first().get('artworks').add @model

# The user collection tray updates whenever a model is added to the collection
App.currentUser.get('collections').first().get('artworks').bind 'add', (model) ->
  $('.collection_tray').append(JST['artworks/collection_list_item']{ artwork: model }).slideUp()

Unlike Views in server-side frameworks like ROR, Views should not have any more than trivial HTML in them. Instead you should use...

Client-side templates

We use Haml.js for our templating language, and we use Jammit to automagically package any .jst.haml templates in app/views to be stored in a global JST object that is a hash of '/path': function pairs. The function can be passed a hash of data to be embedded in the template.

JST['artworks/show'] { title: 'Water Lillies' }

Calling JST['/path'] will use Haml.js to parse the associated template and return a string of HTML, which you can then use to inject in the DOM.

$('body').prepend(JST['artworks/show'] { title: 'Water Lillies' } )

Routers

class App.Routers.Index extends Vertebrae.Router
  
  routes:
    '': 'index'
    'artworks': 'artworks'
    'artworks/partner/:partner': 'artworksPartner'
    'artworks/artist/:artist': 'artworksArtist'
    'artworks/artwork/:artwork': 'artworksArtwork'
    'users': 'users'

Backbone Routers are very similar to Ruby on Rails controllers. The Router's separation of concern is simply to act as a controller for hash route changes. There should be as little as possible UI logic or Business logic in Routers. Generally a Router will only get the necessary Models and Views needed to set up that route's state.

Generally there is no need for more than one Router per Backbone App.

From 0 to View in Backbone

  1. Setup a global namespace
  • Namespace backbone components
  • A namespace for any global helper functions
  • jQuery's global namespace is $ and jQuery
  • Can be 'window' if you're daring enough
  1. Create your Backbone components
  • Declare your backbone classes after your namespace but before your app is initialized.
  1. Run 'init app' logic
  • Load up any initial models & views
  • Start a router
  • This will be large or small depending on how much of the initial state you bootstrap from the server. (e.g. a single page app might provide nothing but bootstrapped User JSON and a big empty body tag)
# Namespace our app and Backbone components
window.App =
  Models: {}
  Collections: {}
  Views: {}
  Routers: {}

# Create our backbone components
class App.Models.Artwork extends Backbone.Model
  urlRoot: '/api/v1/artwork/'
  
class App.Views.ArtworksShow extends Backbone.View
  el: '#show_artwork'
  
  initialize: ->
    @model.bind 'change', @render
  
  render: =>
    $(@el).html JST['artworks/index'] model: @model

class App.Routers.Index extends Backbone.Router
  routes:
    '/artwork/:id': 'show'
    
  show: (id) ->
    new App.Models.Artwork(id: id).fetch
      success: (artwork) ->
        new App.Views.ArtworksShow model: artwork

# Initialize our app
$ ->
  App.router = new App.Routers.Index()
  Backbone.history.start()

Takeaway notes

  • State should be kept track of in the "Model" layer.
  • It's not Rails! There's more of a one to one relation with models & views. Views can change models, a view listens to models for changes and updates itself, models should not directly change views.
  • It's easy to use views as a placeholder to store a bunch of jQuery in the initialize function. But to keep things clean it's best to break things up into methods on a view that are bound to events on the model/collection & DOM.
  • Utilize backbone as much as you can! You should try to wrap data into models as much as possible to benefit from the events and methods Backbone provides otherwise things can get messy. Backbone wraps a RESTful API really well, so you should often use a model to persist things instead of $.ajax.
  • Relations
  • Convenience static methods
  • Router before & after callbacks and events
  • Find methods
    • Help pass around the same client side models so that it's easy to "find" the same model if multiple views are supposed to bind to their events.

What's Inertia, Torque, and Impulse

Thick client applications that sit on top of a thin node server. The node server authenticates with Gravity and proxies requests from our API.

  • Inertia - CMS application for galleries
  • Torque - Admin panel 2.0
  • Impulse - Boilerplate for setting up projects like this

Why

  • Javascript on the server and client means that you can test your client side Backbone Models & Collections by stubbing client-side things like window and $. This is nice because it ensures modularity and runs these tests in milli-seconds.
  • Headless testing via Zombie.js (more later)
  • Ensuring a clear separation of concerns. Our thick client applications consume our API meaning we are forced to eat our own dog food. Changing something in our Admin application won't affect our Partner CMS or main client facing application.
  • Developer productivity++ Smaller project means smaller test suite, lightweight framework means less boot time, switch programming language context less (always coffeescript/js)

Testing & Zombie.js

Insanely fast, headless full-stack testing using Node.js.

var Browser = require("zombie");
var assert = require("assert");

// Load the page from localhost
browser = new Browser()
browser.visit("http://localhost:3000/", function () {

  // Fill email, password and submit form
  browser.
    fill("email", "zombie@underworld.dead").
    fill("password", "eat-the-living").
    pressButton("Sign Me Up!", function() {

      // Form submitted, new page loaded.
      assert.ok(browser.success);
      assert.equal(browser.text("title"), "Welcome To Brains Depot");

    })

});

Sample test from Torque:

require '../helpers/spec_helper.coffee'

describe "index", ->

  it 'renders the home page when logged in', (done) ->
    browser.visit 'login', ->
      browser.wait ->
        $('header.main').length.should.be.ok
        done()
  
  it 'sets the current user', (done) ->
    browser.visit '', ->
      browser.wait (-> browser.window.App.currentUser?), ->
        browser.window.App.currentUser.get('id')?.should.be.ok
        done()
  • The browser is evented and async, so our tests should also be.
  • browser is exposed globally in the test suite so you can bind to browser events, or access the browser js environment directly with browser.window
  • Capybara like API? Who cares, if you know jQuery you can use that aliased globally.

Much faster than Capybara!

  • Stub API responses means no waiting for a test database and full trip.
  • It's not firefox! Means no waiting for a full browser and selenium driver.
  • It's all V8 javascript!

Potentially more stable!

  • Less tests in one place means less chance of one test failing a whole hour long suite :-P
  • Less moving parts: Zombie is running inside node, the browser.window is inside node, the testing framework is inside node, and the API is being stubbed by code being run inside node. (Some acceptance tests can be written synchronously because it's all runing inside one environment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment