Book Image

Learning JavaScriptMVC

By : Wojciech Bednarski
Book Image

Learning JavaScriptMVC

By: Wojciech Bednarski

Overview of this book

<p>JavaScriptMVC is a client-side, JavaScript framework that builds maintainable, error-free, lightweight applications as quickly as possible. As it does not depend on server components, it can be combined with any web service interface and server-side language.<br /><br />"Learning JavaScriptMVC" will guide you through all the framework aspects and show you how to build small- to mid-size well-structured and documented client-side applications you will love to work on.<br /><br />This book starts from JavaScriptMVC installation and all its components are explained with practical examples. It finishes with an example of building a web application. You will learn what the JavaScriptMVC framework is, how to install it, and how to use it efficiently.<br /><br />This book will guide you on how to build a sample application from scratch, test its codebase using unit testing, as well as test the whole application using functional testing, document it, and deploy the same. After reading Learning JavaScriptMVC you will learn how to install the framework and create a well-structured, documented and maintainable client-side application.</p>
Table of Contents (13 chapters)

Building simple applications


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.

Todo list

We are going to learn JavaScriptMVC on the classic example application – the to-do list.

Note

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

Loader

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>

Note

../steal/steal.js?todo is the equivalent of ../steal/steal.js?todo/todo.js. If file name is not provided StealJS, try to load the JavaScript file with the same name as the given folder.

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.

Model

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}'
            },
            {

            }
        );
    }
);

Note

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.

  1. Open a web browser and type the following line into the JavaScript console:

    var todo = new Todo({name: 'write a book'});

    todo is now an instance of Todo with property name and property value write a book.

  2. Get the property value as follows:

    todo.attr('name');
  3. Set the property value if the property exists, as follows:

    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!'});
  4. Add two new properties:

    todo.attrs({
        person: 'Wojtek',
        dueDate: '1 December 1012'
    });
  5. List all the properties:

    Todo.attrs();

The following screenshot shows the execution of the preceding commands:

Fixtures

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.

Note

Create, Read, Update, Delete (CRUD) are the four basic functions of persistent storage.

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:

View

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)
}

Controller

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.

Routing

Replace the following code:

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

Complete application code

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);
    }
);