Monday, September 17, 2012

MVC in javascript

See code, running application and qunit test here: http://www.danbunea.ro/blogspot/mvcjs/

Problem

Today, in modern web applications, more and more code is moved from being generated server side (in java, c# with asp.net , ruby on ror, php) to be executed on the client browser, in javascript. This created a massive problem for all those used to having code well organized in modern web frameworks using the very powerfull Model View Controller pattern, since most of the code is written now in javascript.

Could MVC in javascript be a solution?

Well, there are more and more frameworks trying to adress the problem of having tons of javascript, whioch becomes a nightmare as well as replace the dynamic generation on the server , thinks such backbone.js, but I find it not very MVC. So why wouldn't I actually write my own MVC framework.

The first thing we want to do it actually list the users on the page, so that means the controller is invoked, it takes the decistion to load the users from the server into the model, and after that send the model to the view to render the users as html. So we have an html file index.html

...
<body>

<div id="list">
 <a href="http://www.blogger.com/blogger.g?blogID=9648422#" id="add" onclick="controller.add();">Add</a>
 <div id="usersList">
 </div>
</div>
<div id="form">
</div>
<div id="tests">
 <h1 id="qunit-header">
mvc.js</h1>
</div>
</body>  
    <h2 id="qunit-userAgent">
</h2>
<ol id="qunit-tests">  
    </ol>
</pre>
<script>


 


jQuery(window).load(function () {

    controller = new UsersController();
    controller.index();
});

</script>


</body>

</html>
 

now the controller class:

UsersController.prototype = new Object;
//constructor definition
UsersController.prototype.constructor = UsersController;


function UsersController() {
 this.model = new UsersModel();
 this.view=new UsersView();
};


UsersController.prototype.index=function(){
 this.model.loadUsers();
 this.view.renderUsersList(this.model);
};

and the model:

UsersModel.prototype = new Object;
//constructor definition
UsersModel.prototype.constructor = UsersModel;

function UsersModel()
{
 this.users=[];
}

UsersModel.prototype.loadUsers=function()
{
 this.users = eval(new HttpRequest().get("http://localhost/users/list.aspx"));
};
 
now let's see the view that renders the list. For it we actually use jQuery templates:

UsersView.prototype = new Object;
//constructor definition
UsersView.prototype.constructor = UsersView;

function UsersView()
{
    //templates
    this.userLine = "<div if='user${user.id}' class='user' >"+
 " <div id='fullName${user.id}' class='cell'>${user.fullName}</div>"+
 " <div id='username${user.id}' class='cell'>${user.username}</div>"+
 " <div id='email${user.id}' class='cell'>${user.email}</div>"+
 " <div class='cell'><a id='edit${user.id}' href='#' onClick='controller.edit(${user.id})'>Edit</a></div>"+
 " <div class='cell'><a id='delete${user.id}' href='#' onClick='controller.delete(${user.id})'>Delete</a></div>"+
 "</div>";
 jQuery.template("userLine", this.userLine);

}

UsersView.prototype.renderUsersList=function(model)
{
 jQuery("#usersList").html("");
 jQuery("#list").show();
 jQuery("#form").hide();

 for(var i=0;i<model.users.length;i++)
 {
  jQuery.tmpl("userLine", { user: model.users[i] }).appendTo("#usersList");
 }
};

Hmmm, very simple. Now let's implement the add:

UsersController.prototype.add=function(){
 this.view.renderAdd();
};

UsersController.prototype.save=function(id,fullName,username,password,email){
  id=this.model.users.length+1;
  this.model.users.push({id:id,fullName:fullName,username:username,password:password, email:email});

};
 
and in the view:

UsersView.prototype = new Object;
//constructor definition
UsersView.prototype.constructor = UsersView;

function UsersView()
{
    //templates
    this.userLine = "<div if='user${user.id}' class='user' >"+
 " <div id='fullName${user.id}' class='cell'>${user.fullName}</div>"+
 " <div id='username${user.id}' class='cell'>${user.username}</div>"+
 " <div id='email${user.id}' class='cell'>${user.email}</div>"+
 " <div class='cell'><a id='edit${user.id}' href='#' onClick='controller.edit(${user.id})'>Edit</a></div>"+
 " <div class='cell'><a id='delete${user.id}' href='#' onClick='controller.delete(${user.id})'>Delete</a></div>"+
 "</div>";
 jQuery.template("userLine", this.userLine);

    this.userForm = "<div class='fields'  style='display:table-row'>"+
 " <div id='fullName_${user.id}' style='display:table-row'>Full Name:<input id='fullName' type='text' value='${user.fullName}'/></div>"+
 " <div id='username_${user.id}' style='display:table-row'>Username:<input id='username' type='text' value='${user.username}'/></div>"+
 " <div id='password_${user.id}' style='display:table-row'>Password:<input id='password' type='password' value='${user.password}'/></div>"+
 " <div id='email_${user.id}' style='display:table-row'>Email:<input id='email' type='text' value='${user.email}'/></div>"+
 " <a id='save' href='#' onClick='controller.save(\"${user.id}\",jQuery(\"#fullName\").val(),jQuery(\"#username\").val(),jQuery(\"#password\").val(),jQuery(\"#email\").val());'>Save</a>"+
 " <a id='cancel' href='#' onClick='controller.list();'>Cancel</a>"+
 "</div>";
 jQuery.template("userForm", this.userForm);
}


...



UsersView.prototype.renderAdd=function()

{

    var user={fullName:"",username:"",password:"",email:"" };



    jQuery("#form").html("").show();

    jQuery("#list").hide();



    jQuery.tmpl("userForm", { user: user}).appendTo("#form");

};

Now what about edit?

UsersController.prototype.edit=function(id){
 var user = this.model.findUserById(id);
 this.view.renderEdit(user);
};

UsersController.prototype.save=function(id,fullName,username,password,email){
 if(id=="") 
 {
  id=this.model.users.length+1;
  this.model.users.push({id:id,fullName:fullName,username:username,password:password, email:email});
 }
 else
 {
  var user = this.model.findUserById(id);
  user.fullName=fullName;
  user.username=username;
  user.password=password;
  user.email=email;
 }
 this.view.renderUsersList(this.model);
};

and in the model:

UsersModel.prototype.findUserById=function(id)
{
 for(var i=0;i<this.users.length;i++)
 {
  if(this.users[i].id==id)
  return this.users[i];
 }
 return null;
};

and the view is refactored to:

UsersView.prototype = new Object;
//constructor definition
UsersView.prototype.constructor = UsersView;

function UsersView()
{
    //templates
    this.userLine = "<div if='user${user.id}' class='user' >"+
 " <div id='fullName${user.id}' class='cell'>${user.fullName}</div>"+
 " <div id='username${user.id}' class='cell'>${user.username}</div>"+
 " <div id='email${user.id}' class='cell'>${user.email}</div>"+
 " <div class='cell'><a id='edit${user.id}' href='#' onClick='controller.edit(${user.id})'>Edit</a></div>"+
 " <div class='cell'><a id='delete${user.id}' href='#' onClick='controller.delete(${user.id})'>Delete</a></div>"+
 "</div>";
 jQuery.template("userLine", this.userLine);

    this.userForm = "<div class='fields'  style='display:table-row'>"+
 " <div id='fullName_${user.id}' style='display:table-row'>Full Name:<input id='fullName' type='text' value='${user.fullName}'/></div>"+
 " <div id='username_${user.id}' style='display:table-row'>Username:<input id='username' type='text' value='${user.username}'/></div>"+
 " <div id='password_${user.id}' style='display:table-row'>Password:<input id='password' type='password' value='${user.password}'/></div>"+
 " <div id='email_${user.id}' style='display:table-row'>Email:<input id='email' type='text' value='${user.email}'/></div>"+
 " <a id='save' href='#' onClick='controller.save(\"${user.id}\",jQuery(\"#fullName\").val(),jQuery(\"#username\").val(),jQuery(\"#password\").val(),jQuery(\"#email\").val());'>Save</a>"+
 " <a id='cancel' href='#' onClick='controller.list();'>Cancel</a>"+
 "</div>";
 jQuery.template("userForm", this.userForm);
}

UsersView.prototype.renderUsersList=function(model)
{
 jQuery("#usersList").html("");
 jQuery("#list").show();
 jQuery("#form").hide();

 for(var i=0;i<model.users.length;i++)
 {
  jQuery.tmpl("userLine", { user: model.users[i] }).appendTo("#usersList");
 }
};

UsersView.prototype.renderAdd=function()
{
 var user={fullName:"",username:"",password:"",email:"" };
 this.renderForm(user);
};

UsersView.prototype.renderEdit=function(user)
{ 
 this.renderForm(user);
};


UsersView.prototype.renderForm=function(user)
{
 jQuery("#form").html("").show();
 jQuery("#list").hide();

 jQuery.tmpl("userForm", { user: user}).appendTo("#form");
};

Conclusion

As you can see above, implementing MVC in javascript can be very simple and doesn't require any sort of frameworks. Once MVC is implemented the code is well organized depending on concerns: who commands - controller, where by one look you can see what the entire code does (list, add, save, edit) the model who holds the data and exchanges it with the server and the view which actually renders it in html when asked by the controller.Simple to implement, well organized, easy to follow the code, very extensible.