We installed JavaScriptMVC and went briefly through its components. Now, we are ready to build our first JavaScriptMVC application.
Excited? Let's do the magic.
We are going to learn JavaScriptMVC on the classic example application – the to-do list.
If you are curious and want to compare different JavaScript frameworks based on the todos
application examples, then the GitHub project is absolutely fantastic. You can find it at https://github.com/tastejs/todomvc/tree/gh-pages/architecture-examples. The project home page is at http://todomvc.com/.
In the Todo
folder that we created during installing JavaScriptMVC, create a folder named todo
. Create files named todo.html
and todo.js
inside todo
.
The project directory should have following structure:
Todo/ .git .gitmodules todo/ todo.html todo.js documentjs funcunit jquery js js.bat steal
Copy and paste the following code into todo.html
to load the StealJS
and todo.js
files:
<!doctype html> <html> <head> <title>Todo List</title> <meta charset="UTF-8" /> </head> <body> <ul id="todos"> <li>all done!</li> </ul> <script src="../steal/steal.js?todo"></script> </body> </html>
In todo.js
, add the following code to load the
jQueryMX
plugins. They are necessary to implement this application:
steal( 'jquery/class', 'jquery/model', 'jquery/dom/fixture', 'jquery/view/ejs', 'jquery/controller', 'jquery/controller/route', function ($) { } );
Open the page in a web browser by typing http://YOUR_LOCAL_WEB_SERVER/Todo/todo.html
, and use a web development tool, such as Google Chrome Inspector, to check if StealJS
and all the listed plugins are loaded properly.
The next step is to add a model to our application by extending $.Model
from the jQueryMX
project.
The first parameter is the model name (string), the second parameter is the object with the class properties and methods. The last parameter is the prototype instance property, which we leave as an empty object for this example:
steal( 'jquery/class', 'jquery/model', 'jquery/dom/fixture', 'jquery/view/ejs', 'jquery/controller', 'jquery/controller/route', function ($) { $.Model('Todo', { findAll: 'GET /todos', findOne: 'GET /todos/{id}', create: 'POST /todos', update: 'PUT /todos/{id}', destroy: 'DELETE /todos/{id}' }, { } ); } );
Class properties are not random; they are described in the model API. http://javascriptmvc.com/docs.html#!jquerymx.
We've created the Todo
model for our todo
list application. Now, it's time to play around with it.
var todo = new Todo({name: 'write a book'});
todo
is now an instance of Todo
with property name and property value write a book
.
todo.attr('name'),
todo.attr('name', 'write JavaScript book'),
Or by attrs
, where we can set more then one property at the time as well as add a new property:
todo.attrs({name: 'write JavaScriptMVC book!'});
todo.attrs({ person: 'Wojtek', dueDate: '1 December 1012' });
Todo.attrs();
The following screenshot shows the execution of the preceding commands:
Since we have no backend service to handle /todo API calls in our frontend application, any attempt to invoke one of the model's CRUD methods on the Todo
model will cause a network error.
At this point, $
.fixture
comes to the rescue. With this feature, we can work on a project even when backend code is not ready yet.
Create fixtures for the Todo
model:
steal( 'jquery/class', 'jquery/model', 'jquery/util/fixture', 'jquery/view/ejs', 'jquery/controller', 'jquery/controller/route', function ($) { $.Model('Todo', { findAll: 'GET /todos', findOne: 'GET /todos/{id}', create: 'POST /todos', update: 'PUT /todos/{id}', destroy: 'DELETE /todos/{id}' }, { } ); // Fixtures (function () { var TODOS = [ // list of todos { id: 1, name: 'read The Good Parts' }, { id: 2, name: 'read Pro Git' }, { id: 3, name: 'read Programming Ruby' } ]; // findAll $.fixture('GET /todos', function () { return [TODOS]; }); // findOne $.fixture('GET /todos/{id}', function (orig) { return TODOS[(+orig.data.id) - 1]; }); // create var id = 4; $.fixture('POST /todos', function () { return { id: (id++) }; }); // update $.fixture('PUT /todos/{id}', function () { return {}; }); // destroy $.fixture('DELETE /todos/{id}', function () { return {}; }); }()); } );
Now, we can use our Todo
model
methods as if backend services were here.
For instance, we can list all todos
:
Todo.findAll({}, function(todos) { console.log('todos: ', todos); });
The following screenshot shows the output of the console.log('todos: ', todos);
command:
Now, it is a good time to add some HTML code to actually see something beyond the browser console. To do this, use the open source client-side template system Embedded JavaScript (EJS).
Create a new file todos.ejs
in the todo
directory (the same folder where todo.js
is located), and add the following code to it:
<% $.each(this, function(i, todo) { %> <li <%= ($el) -> $el.model(todo) %>> <strong><%= todo.name %></strong> <em class="destroy">delete</em> </li> <% }) %>
Then, type the following in the console:
$('#todos').html('todos.ejs', Todo.findAll());
Now, we can see all todos
printed:
Basically, the EJS template is an HTML file with injected JavaScript code between <%
and %>
or <%=
and %>
(and a few other ways).
The difference is that in the second case, all the values returned by the JavaScript code are escaped and printed out. In the first one, they are only evaluated.
The first line is a jQuery each
loop— no magic here. However, the next line could be a new thing for many readers. It is ECMAScript Harmony-like, arrow style syntax for functions used by the EJS parser that doesn't
darken the whole picture by its simplicity.
The following syntax:
($el) -> $el.model(todo)
Can be explained as follows:
function ($el) { return $el.model(todo) }
Let's add some action to our user interface.
Add the following code to the todo.js
file, and refresh the application in a browser:
$.Controller('Todos', { // init method is called when new instance is created 'init': function (element, options) { this.element.html('todos.ejs', Todo.findAll()); }, // add event listener to strong element on click 'li strong click': function (el, e) { // trigger custom event el.trigger('selected', el.closest('li').model()); // log current model to the console console.log('li strong click', el.closest('.todo').model()); }, // add event listener to em element on click 'li .destroy click': function (el, e) { // call destroy on the model to prevent memory leaking el.closest('.todo').model().destroy(); }, // add event listener to Todo model on destroyed '{Todo} destroyed': function (Todo, e, destroyedTodo) { // remove element from the DOM tree destroyedTodo.elements(this.element).remove(); console.log('destroyed: ', destroyedTodo); } }); // create new controller instance new Todos('#todos'),
Now, you can click on the todo
name to see the console log or delete it.
The init
method is called when a new controller is instantiated.
When the controller
element is removed from the DOM tree (in our case, #todos
), the destroy
method is called automatically, unbinding all controller
event handlers and releasing its element to prevent memory leakage.
// create new Todo controller instance new Todos('#todos'),
With:
// routing $.Controller('Routing', { init: function () { new Todos('#todos'), }, // the index page 'route': function () { console.log('default route'), }, // handle URL witch hash ':id route': function (data) { Todo.findOne(data, $.proxy(function (todo) { // increase font size for current todo item todo.elements(this.element).animate({fontSize: '125%'}, 750); }, this)); }, // add event listener on selected '.todo selected': function (el, e, todo) { // pass todo id as a parameter to the router $.route.attr('id', todo.id); } }); // create new Routing controller instance new Routing(document.body);
Refresh the application and try to click on the todo
list elements. You will see that the URL updates after clicking on the todo
item with its corresponding ID.
Here is the complete
code for the Todo
application:
steal( 'jquery/class', 'jquery/model', 'jquery/util/fixture', 'jquery/view/ejs', 'jquery/controller', 'jquery/controller/route', function ($) { $.Model('Todo', { findAll: 'GET /todos', findOne: 'GET /todos/{id}', create: 'POST /todos', update: 'PUT /todos/{id}', destroy: 'DELETE /todos/{id}' }, { } ); // Fixtures (function () { var TODOS = [ // list of todos { id: 1, name: 'read The Good Parts' }, { id: 2, name: 'read Pro Git' }, { id: 3, name: 'read Programming Ruby' } ]; // findAll $.fixture('GET /todos', function () { return [TODOS]; }); // findOne $.fixture('GET /todos/{id}', function (orig) { return TODOS[(+orig.data.id) - 1]; }); // create var id = 4; $.fixture('POST /todos', function () { return { id: (id++) }; }); // update $.fixture('PUT /todos/{id}', function () { return {}; }); // destroy $.fixture('DELETE /todos/{id}', function () { return {}; }); }()); $.Controller('Todos', { // init method is called when new instance is created 'init': function (element, options) { this.element.html('todos.ejs', Todo.findAll()); }, // add event listener to strong element on click 'li strong click': function (el, e) { // trigger custom event el.trigger('selected', el.closest('li').model()); // log current model to the console console.log('li strong click', el.closest('.todo').model()); }, // add event listener to em element on click 'li .destroy click': function (el, e) { // call destroy on the model to prevent memory leaking el.closest('.todo').model().destroy(); }, // add event listener to Todo model on destroyed '{Todo} destroyed': function (Todo, e, destroyedTodo) { // remove element from the DOM tree destroyedTodo.elements(this.element).remove(); console.log('destroyed: ', destroyedTodo); } }); // routing $.Controller('Routing', { init: function () { new Todos('#todos'), }, // the index page 'route': function () { console.log('default route'), }, // handle URL witch hash ':id route': function (data) { Todo.findOne(data, $.proxy(function (todo) { // increase font size for current todo item todo.elements(this.element).animate({fontSize: '125%'}, 750); }, this)); }, // add event listener on selected '.todo selected': function (el, e, todo) { // pass todo id as a parameter to the router $.route.attr('id', todo.id); } }); // create new Routing controller instance new Routing(document.body); } );
18.188.218.226