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.
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
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>")
})
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"} )
})
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>
JavaScriptMVC's
- 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.
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.
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.
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()
})
})
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" );
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
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.
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:
-
Loads the template a 'mytemplate.ejs'. It might look like:
<h2><%= message %></h2>
-
Renders it with {message: 'hello world'}, resulting in:
<h2>hello world</h2>
-
Inserts the result into the foo element. Foo might look like:
<div id='foo'><h2>hello world</h2></div>
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',{});
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.
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.
JavaScriptMVC is packed with jQuery helpers that make building a jQuery app easier and fast. Here's the some of the most useful plugins:
Rapidly retrieve multiple css styles on a single element:
$('#foo').curStyles('paddingTop',
'paddingBottom',
'marginTop',
'marginBottom');
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')