Skip to content

Instantly share code, notes, and snippets.

@jupiterjs
Created March 12, 2011 05:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jupiterjs/867069 to your computer and use it in GitHub Desktop.
Save jupiterjs/867069 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.

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

$.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.

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