Skip to content

Instantly share code, notes, and snippets.

@moschel
Forked from jupiterjs/JavaScriptMVC.md
Created March 27, 2011 18:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save moschel/889470 to your computer and use it in GitHub Desktop.
Save moschel/889470 to your computer and use it in GitHub Desktop.
JavaScriptMVC Overview

Introduction

JavaScriptMVC is an open-source jQuery-based JavaScript framework. It is nearly a comprehensive (holistic) front-end development framework; packaging utilities for:

  • testing
  • dependency management
  • error reporting
  • package management
  • code cleaning
  • custom events
  • jQuery extensions
  • documentation

The library is broken into 4 mostly independent sub-projects:

essentially everything but UI widgets. However, JMVC's MVC parts are only 7k gzipped.

Everything is a plugin

JavaScriptMVC is broken down into 4 independent sub-projects:

  • StealJS - Dependency Management, Code Generators, Production builds, Code cleaning.
  • FuncUnit - Web testing framework
  • DocumentJS - JS documentation framework
  • jQueryMX - jQuery MVC extentions

In the download, these are arranged into the following folders

funcunit
documentjs
jquery
steal

Within each of these folders, are numerous sub-plugins. For example, JavaScriptMVC's controller is found in jquery/controller/controller.js.

JavaScriptMVC encourages you to use a similar folder structure. We organized the todo app at the end of the application like:

funcunit
documentjs
jquery
steal
todo/
  todo.js
  todo.html

StealJS - Dependency Management

JavaScriptMVC uses StealJS for dependency management and production builds. To use steal, you just have to load the steal script in your page and point it to a script file that loads your other files. For example, putting the following in todo.html loads steal.js and tells it to load todo/todo.js.

<script type='text/javascript' 
        src='../steal/steal.js?todo/todo.js'>
</script>

The todo.js file can then use steal to load any dependencies it needs. JavaScriptMVC comes with the jQuery library in 'jquery/jquery.js'.
The following loads jQuery and uses it to write Todo List:

steal('../jquery/jquery').then(function(){
  $(document.body).append("<h1>Todo List</h1>")
})

Because loading from JavaScriptMVC's root folder is extremely common, steal provides a plugins helper method. We can write the above as:

steal.plugins('jquery').then(function(){
  $(document.body).append("<h1>Todo List</h1>")
})

Loading Non JavaScript resources

Steal can load and build other resource types as well: client side templates, css, LessCSS and CoffeeScript. The following uses an jQuery.tmpl template in todos/views/list.tmpl to write todo list:

steal.plugins('jquery','jquery/view/tmpl')
     .views("list.tmpl")
     .then(function(){

  $(document.body).append("//todo/views/list.tmpl",{message: "Todo List"} )
})

Compression

Having lots of files is very slow for page loading times. StealJS makes building your app into a single JS and CSS file very easy. To generate the files, run:

js steal/buildjs todo/todo.html

For our mini todo app, this produces:

todo/production.js

To use production.js, we just have to load the production version of steal. We do this by changing our steal script tag to:

<script type='text/javascript' 
        src='../steal/steal.js?todo/todo.js'>
</script>

Class

JavaScriptMVC's $.Controller and $.Model are based on it's Class helper - $.Class. $.Class is based on John Resig's simple class. It adds several important features, namely:

  • static methods and properties
  • introspection
  • namespaces
  • callback creation

Creating a $.Class and extending it with a new class is straightforward:

$.Class("Animal",{
  breath : function(){
     console.log('breath'); 
  }
});

Animal("Dog",{
  wag : function(){
    console.log('wag');
  }
})

var dog = new Dog;
dog.wag();
dog.breath();

When a new $.Class is created, it calls the class's init method with the arguments passed to the constructor function:

$.Class('Person',{
  init : function(name, age){
    this.name = name;
    this.age = age;
  },
  speak : function(){
    return "I am "+this.name+".";
  }
});

var justin = new Person("Justin",28);
justin.speak(); //-> 'I am Justin.'

$.Class lets you call base functions with this._super. Lets make a 'classier' person:

Person("ClassyPerson", {
  speak : function(){
    return "Salutations, "+this._super();
  }
});

var fancypants = new ClassyPerson("Mr. Fancy",42);
fancypants.speak(); //-> 'Salutations, I am Mr. Fancy.'

Class provides a callback method that can be used to return a function that has 'this' set appropriately (similar to proxy):

$.Class("Clicky",{
  init : function(){
    this.clickCount = 0;
  },
  wasClicked : function(){
    this.clickCount++;
  },
  addListeners : function(el){
    el.click(this.callback('wasClicked');
  }
})

Callback also lets you curry arguments and chain methods together.

Class lets you define inheritable static properties and methods:

$.Class("Person",{
  findOne : function(id, success){
    $.get('/person/'+id, function(attrs){
      success( new Person( attrs ) );
    },'json')
  }
},{
  init : function(attrs){
    $.extend(this, attrs)
  },
  speak : function(){
    return "I am "+this.name+".";
  }
})

Person.findOne(5, function(person){
  alert( person.speak() );
})

Class also provides namespacing and access to the name of the class and namespace object:

$.Class("Jupiter.Person");

Jupiter.Person.shortName; //-> 'Person'
Jupiter.Person.fullName;  //-> 'Jupiter.Person'
Jupiter.Person.namespace; //-> Jupiter

Putting this all together, we can make a basic ORM-style model layer:

$.Class("ORM",{
  findOne : function(id, success){
    $.get('/'+this.fullName.toLowerCase()+'/'+id, 
      this.callback(function(attrs){
         success( new this( attrs ) );
      })
    },'json')
  }
},{
  init : function(attrs){
    $.extend(this, attrs)
  }
})

ORM("Person",{
  speak : function(){
    return "I am "+this.name+".";
  }
});

Person.findOne(5, function(person){
  alert( person.speak() );
});

ORM("Task")

Task.findOne(7,function(task){
  alert(task.name);
})

This is similar to how JavaScriptMVC's model layer works.

Models

JavaScriptMVC's model and it associated plugins provide lots of tools around organizing model data such as validations, associations, events, lists and more. But the core functionality is centered around service encapsulation and type conversion.

Service Encapsulation

Model makes it crazy easy to connect to JSON REST services and add helper methods to the resulting data. For example, take a todos service that allowed you to create, retrieve, update and delete todos like:

POST /todos name=laundry dueDate=1300001272986 -> {'id': 8}
GET /todos -> [{'id': 5, 'name': "take out trash", 'dueDate' : 1299916158482},
               {'id': 7, 'name': "wash dishes", 'dueDate' : 1299996222986},
               ... ]
GET /todos/5 -> {'id': 5, 'name': "take out trash", 'dueDate' : 1299916158482}
PUT /todos/5 name=take out recycling -> {}
DELETE  /todos/5 -> {}

Making a Model that can connect to these services and add helper functions is shockingly easy:

$.Model("Todo",{
  findAll : "GET /todos",
  findOne : "GET /todos/{id}",
  create  : "POST /todos",
  update  : "PUT  /todos/{id}",
  destroy : "DELETE  /todos/{id}"
},{
  daysRemaining : function(){
    return ( new Date(this.dueDate) - new Date() ) / 86400000
  }
});

This allows you to

// List all todos
Todo.findAll({}, function(todos){
  var html = [];
  for(var i =0; i < todos.length; i++){
    html.push(todos[i].name+" is due in "+
              todos[i].daysRemaining()+
              "days")
  }
  $('#todos').html("<li>"+todos.join("</li><li>")+"</li>")
})

//Create a todo
new Todo({
  name: "vacuum stairs", 
  dueDate: new Date()+86400000
}).save(function(todo){
  alert('you have to '+todo.name+".")
});

//update a todo
todo.update({name: "vacuum all carpets"}, function(todo){
  alert('updated todo to '+todo.name+'.')
});

//destroy a todo
todo.destroy(function(todo){
  alert('you no longer have to '+todo.name+'.')
});

Of course, you can supply your own functions.

Events

Although encapsulating ajax requests in a model is valuable, there's something even more important about models to an MVC architecture - events. $.Model lets you listen model events. You can listen to models being updated, destroyed, or even just having single attributes changed.

$.Model produces two types of events:

  • OpenAjax.hub events
  • jQuery events

Each has advantages and disadvantages for particular situations. For now we'll deal with jQuery events. Lets say we wanted to know when a todo is created and add it to the page. And after it's been added to the page, we'll listen for updates on that todo to make sure we are showing its name correctly. We can do that like:

$(Todo).bind('created', function(todo){
  var el = $('<li>').html(todo.name);
  el.appendTo($('#todos'));
  todo.bind('updated', function(todo){
    el.html(todo.name)
  }).bind('destroyed', function(){
    el.remove()
  })
})

Getter / Setters ?

Model.Lists

Often, in complex JS apps, you're dealing with discrete lists of lots of items. For example, you might have two people's todo lists on the page at once.

Model has the model list plugin to help with this.

$.Model.List("Todo.List")

$.Class('ListWidget',{
  init : function(element, username){
    this.element = element;
    this.username = username;
    this.list = new Todo.List();
    this.list.bind("add", this.callback('addTodos') );
    this.list.findAll({username: username});
    this.element.delegate('.create','submit',this.callback('create'))
  },
  addTodos : function(todos){
    // TODO: gets called with multiple todos
    var el = $('<li>').html(todo.name);
    el.appendTo(this.element.find('ul'));
    todo.bind('updated', function(todo){
      el.html(todo.name)
    })
  },
  create : function(ev){
    var self = this;
    new Todo({name: ev.target.name.value,
              username: this.username}).save(function(todo){
       self.list.push(todo);
    })
  }
});

new ListWidget($("#briansList"), "brian" );
new ListWidget($("#justinsList"), "justin" );

$.Controller - jQuery plugin factory

JavaScriptMVC's controllers are really a jQuery plugin factory. They can be used as a traditional view, for example, making a slider widget, or a traditional controller, creating view-controllers and binding them to models. If anything happens in your application, a controller should respond to it.

Controllers are the most powerful piece of JavaScriptMVC and go the furthest towards helping you develop better JavaScript applications, with features like:

  • jQuery helper
  • auto bind / unbind
  • parameterized actions
  • defaults
  • pub / sub
  • automatic determinism

Overview

Controllers inherit from $.Class. Each Controller is instantiated on an HTML element. Controller methods, or actions, use event delegation for listen to events inside the parent element.

$.Controller('Tabs',{
  click: function() {...},
  '.tab click' : function() {...},
  '.delete click' : function() {...}
})
$('#tabs').tabs();

Creating a controller

When a controller class is created, it automatically creates a jQuery.fn method of a similar name. These methods have three naming rules:

  1. Everything is lowercased

  2. "." is replaced with "_"

  3. "Controllers" is removed from the name

    $.Controller("Foo.Controllers.Bar") // -> .foo_bar()

You create a controller by calling this jQuery.fn method on any jQuery collection. You can pass in options, which are used to set this.options in the controller.

$(".thing").my_widget({message : "Hello"})

Determinism

Controllers provide automatic determinism for your widgets. This means you can look at a controller and know where in the DOM they operate, and vice versa.

First, when a controller is created, it adds its underscored name as a class name on the parent element.

<div id='historytab' class='history_tabs'></div>

You can look through the DOM, see a class name, and go find the corresponding controller.

Second, the controller saves a reference to the parent element in this.element. On the other side, the element saves a reference to the controller instance in jQuery.data.

$("#foo").data('controllers')

A helper method called controller (or controllers) using the jQuery.data reference to quickly look up controller instance on any element.

$("#foo").controller() // returns first controller found
$("#foo").controllers() // returns an array of all controllers on this element

Finally, actions are self labeling, meaning if you look at a method called ".foo click", there is no ambiguity about what is going on in that method.

Responding to Actions

If you name an event with the pattern "selector action", controllers will set these methods up as event handlers with event delegation. Even better, these event handlers will automatically be removed when the controller is destroyed.

".todo mouseover" : function( el, ev ) {}

The el passed as the first argument is the target of the event, and ev is the jQuery event. Each handler is called with "this" set to the controller instance, which you can use to save state.

Removing Controllers

Part of the magic of controllers is their automatic removal and cleanup. Controllers bind to the special destroy event, which is triggered whenever an element is removed via jQuery. So if you remove an element that contains a controller with el.remove() or a similar method, the controller will remove itself also. All events bound in the controller will automatically clean themselves up.

Defaults

Controllers can be given a set of default options. Users creating a controller pass in a set of options, which will overwrite the defaults if provided.

In this example, a default message is provided, but can is overridden in the second example by "hi".

$.Controller("Message", {
  defaults : {
    message : "Hello World"
  }
},{
  init : function(){
    this.element.text(this.options.message);
  }
})

$("#el1").message(); //writes "Hello World"
$("#el12").message({message: "hi"}); //writes "hi"

Parameterized Actions

Controllers provide the ability to set either the selector or action of any event via a customizable option. This makes controllers potentially very flexible. You can create more general purpose event handlers and instantiate them for different situations.

The following listens to li click for the controller on #clickMe, and "div mouseenter" for the controller on #touchMe.

$.Controller("Hello", {
  defaults: {item: “li”, helloEvent: “click”}
}, {
  “{item} {helloEvent}" : function(el, ev){
    alert('hello')�    el // li, div
  }
})

$("#clickMe").hello({item: “li”, helloEvent : "click"});
$("#touchMe").hello({item: “div”, helloEvent : "mouseenter"});

Pub / Sub

JavaScriptMVC applications often use OpenAjax event publish and subscribe as a good way to globally notify other application components of some interesting event. The jquery/controller/subscribe method lets you subscribe to (or publish) OpenAjax.hub messages:

$.Controller("Listener",{
  "something.updated subscribe" : function(called, data){}
})

// called elsewhere
this.publish("some.event", data);

Special Events

Controllers provide support for many types of special events. Any event that is added to jQuery.event.special and supports bubbling can be listened for in the same way as a DOM event like click.

$.Controller("MyHistory",{
  "history.pagename subscribe" : function(called, data){
    //called when hash = #pagename
  }
})

Drag, drop, hover, and history and some of the more widely used controller events. These events will be discussed later.

$.View - Client Side Templates

JavaScriptMVC's views are really just client side templates. jQuery.View is a templating interface that takes care of complexities using templates:

  • Convenient and uniform syntax
  • Template loading from html elements and external files.
  • Synchronous and asynchronous template loading.
  • Template preloading.
  • Caching of processed templates.
  • Bundling of processed templates in production builds.

JavaScriptMVC comes pre-packaged with 4 different templates:

  • EJS
  • JAML
  • Micro
  • Tmpl

And there are 3rd party plugins for Mustache and Dust.

Use

When using views, you almost always want to insert the results of a rendered template into the page. jQuery.View overwrites the jQuery modifiers so using a view is as easy as:

$("#foo").html('mytemplate.ejs',{message: 'hello world'})

This code:

  1. Loads the template a 'mytemplate.ejs'. It might look like:

    <h2><%= message %></h2>
    
  2. Renders it with {message: 'hello world'}, resulting in:

    <h2>hello world</h2>
    
  3. Inserts the result into the foo element. Foo might look like:

    <div id='foo'><h2>hello world</h2></div>
    

jQuery Modifiers

You can use a template with the following jQuery modifier methods:

$('#bar').after('temp.jaml',{});
$('#bar').append('temp.jaml',{});
$('#bar').before('temp.jaml',{});
$('#bar').html('temp.jaml',{});
$('#bar').prepend('temp.jaml',{});
$('#bar').replaceWidth('temp.jaml',{});
$('#bar').text('temp.jaml',{});

Loading from a script tag

View can load from script tags or from files. To load from a script tag, create a script tag with your template and an id like:

<script type='text/ejs' id='recipes'>
<% for(var i=0; i < recipes.length; i++){ %>
  <li><%=recipes[i].name %></li>
<%} %>
</script>

Render with this template like:

$("#foo").html('recipes',recipeData)

Notice we passed the id of the element we want to render.

Asynchronous loading

By default, retrieving requests is done synchronously. This is fine because StealJS packages view templates with your JS download.

However, some people might not be using StealJS or want to delay loading templates until necessary. If you have the need, you can provide a callback paramter like:

$("#foo").html('recipes',recipeData, function(result){
  this.fadeIn()
});

The callback function will be called with the result of the rendered template and 'this' will be set to the original jQuery object.

FuncUnit

Testing is an often overlooked part of front end development. Most functional testing solutions are hard to set up, expensive, use a difficult (non JavaScript) API, are too hard to debug, and are don't accurately simulate events. FuncUnit, JavaScriptMVC's testing solution, is designed to solve all these problems. No setup, firebug debugging, a jQuery-like API, and the most accurate possible event simulation make FuncUnit a comprehensive testing solution.

Overview

FuncUnit is a collection of several components:

  • jQuery - for querying elements and testing their conditions
  • QUnit - jQuery's unit test framework for setting up tests
  • Syn - an event simulation library made for FuncUnit that simulates clicks, types, drags
  • Selenium - used to programatically open and close browsers

FuncUnit tests are written in JavaScript, with an API that looks identical to jQuery. To run them, you open a web page that loads your test. It opens your application in another page, runs your test, and shows the results.

Alternatively, the same test will run from the command line, via Selenium. You run a command (or automate this in your continuous integration system), which launches a browser, runs your tests, and reports results.

Getting Set Up

Getting started with writing a FuncUnit test involves:

  1. Creating a funcunit.html page that loads a test script. The tests run within the QUnit framework, so the page needs some elements required by QUnit.

     <head>
         <link rel="stylesheet" type="text/css" href="funcunit/qunit/qunit.css" />
         <title>FuncUnit Test</title>
     </head>
     <body>
         <h1 id="qunit-header">funcunit Test Suite</h1>
         <h2 id="qunit-banner"></h2>
         <div id="qunit-testrunner-toolbar"></div>
         <h2 id="qunit-userAgent"></h2>
         <ol id="qunit-tests"></ol>
         <script type='text/javascript' src='steal/steal.js?steal[app]=myapp/test/funcunit/mytest.js'></script>
     </body>
    
  2. Create the test script. Steal funcunit, use QUnit's setup method to set your test up, and the test method to write your test.

    steal.plugins('funcunit').then(function(){ module("yourapp test", { setup: function(){ S.open("//path/to/your/page.html"); } }); test("First Test", function(){ ok(true, "test passed"); }); })

You should now have a working basic FuncUnit test. Open the page and see it run.

Writing a Test

Writing FuncUnit tests involves a similar repeated pattern:

  1. Opening a page (only do this once in the setup method).
  2. Perform some action (click a link, type into an input, drag an element)
  3. Wait for some condition to be true (an element becomes visible, an element's height reaches a certain value) 3*. Check conditions of your page (does the offset of a menu element equal some expected value).
  • This can be done implicitly in step 2. If the conditions of your wait don't become true, the test will never complete and will fail.

This pattern breaks down into the three types of methods in the FuncUnit API: actions, waits, and getters.

Most commands (except open) follow the pattern of being called on a FuncUnit object, similar to a jQuery.fn method. The S method is similar to $, except it doesn't return a jQuery object. It accepts any valid jQuery selector and the results are chainable:

S("a.myel").click().type("one").visible();

QUnit Methods

If you're familiar with QUnit, this section will be review. FuncUnit adds its own API on top of the QUnit framework.

Module is the method used to define a collection of tests. It accepts a module name, and an object containing a setup method (called before each test), and a teardown method (called after each test).

module("module name", { 
    setup: function(){}
    teardown: function(){}	
});

Test is the method used to define each test. Ok and equals are the two most common assertions. If their conditions are true, the assertion will pass. If any assertion fails, the test fails.

Action Commands

Actions include open, click, dblclick, rightclick, type, move, drag, and scroll.

S("#foo").click();
S("#foo").type("one");
S("#foo").drag("#bar");

They use the syn library to accurately simulate the event exactly as the browser would process it from a real user. Click causes the correct sequence of browser events in each browser (mousedown, click, mouseup, focus if its a form element).

Note: Action commands don't actually get processed synchronously. This means you can't set a breakpoint on an action and step through the statements one by one. Each action command is asynchronous, so in order to prevent crazy nested callbacks everywhere, the FuncUnit API will add action commands to a queue, calling each after the previously queued item has completed.

Wait Commands

Waits are used to make your tests wait for some condition in the page to be true before proceeding. Waits are needed after almost every action to prevent your tests from being brittle. If you don't use a wait, you're assuming the action's results are instantaneous, which often, they aren't.

Waits correspond to jQuery methods of the same name.

Dimensions - width, height Attributes - attr, hasClass, val, text, html Position - position, offset, scrollLeft, scrollTop Selector - size, exists, missing Style - css, visible, invisible

Waits, like actions, are asynchronous commands, and add themselves to the FuncUnit command queue, so they can't be inspected with breakpoints.

Each wait accepts the attribute you'd expect it to to wait for its condition to be true. For example, width accepts a single number (the width to wait for). HasClass accepts a class string. Css accepts a property and its value.

You can also pass a function instead of a wait value. This function will be called over and over until it is true, and then the wait is complete.

S(".foo").width(10);       // wait for foo to be 10px
S(".foo").hasClass("bar")  // wait for foo to have class "bar"
S(".foo").visible()        // wait for foo to be visible

Getters

Getters are used to test conditions of the page, usually within an assertion. They are actually the exact same method names as waits, listed above, but called without a wait condition.

var width = S(".foo").width(); var text = S(".foo").text();

Every action and wait command accepts an optional callback function, which is called after the method completes. In these callbacks, you'd test page conditions with getters and do assertions.

S(".foo").visible(function(){
    ok(S(".bar").size(), 5, "there are 5 bars");
});

You can set breakpoints inside these callbacks, test page conditions, and debug assertions that are failing.

A Real Test

Here's a test for an autocomplete widget.

module("autosuggest",{
  setup: function() {
    S.open('autosuggest.html')
  }
});

test("JavaScript results",function(){
  S('input').click().type("JavaScript")

  // wait until we have some results
  S('.autocomplete_item').visible(function(){
    equal( S('.autocomplete_item').size(), 5, "there are 5 results")
  })
});

Running Tests

There are two ways to run tests: browser and command line. To run from browser, just open your test page in the browser. Devs can use this while developing and debugging.

To run from command line, open your test with funcunit/envjs.

funcunit/envjs myapp/funcunit.html

You can configure Selenium options (like browsers) in myapp/settings.js.

FuncUnit = { browsers: ["*firefox", "*iexplore", "*safari", "*googlechrome"] };

Special Events and Dom Extensions

JavaScriptMVC is packed with jQuery helpers that make building a jQuery app easier and fast. Here's the some of the most useful plugins:

CurStyles

Rapidly retrieve multiple css styles on a single element:

$('#foo').curStyles('paddingTop',
                    'paddingBottom',
                    'marginTop',
                    'marginBottom');

Fixtures

Often you need to start building JS functionality before the server code is ready. Fixtures simulate Ajax responses. They let you make Ajax requests and get data back. Use them by mapping request from one url to another url:

$.fixture("/todos.json","/fixtures/todos.json")

And then make a request like normal:

$.get("/todos.json",{}, function(){},'json')

Drag Drop Events

Hover

Default

Destroyed

Hashchange

Building a todo list

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment