Introduction
For one of our customers I'm in the process of building an offline web app. The target for this (at the moment) is the iPad.
Since I cannot expose the source for that specific project to you,
I've decided to create a series of articles that highlight some of the HTML5 features used to create such an app.
For this I came up with a simple app that you can use to hold contact information about people you know.
The goal here is to actually have offline views, models, and JavaScript. And to deal with them in a similar way ASP.NET MVC deals with them.
There are different parts of the whole app that I will show you in a series of articles. I'm not sure in what order and how many articles the whole thing will fall
into.
But together they should cover the following;
- Create a JavaScript MVC structure
- Connect and interact with the WebSql database
- Take the whole thing offline through ApplicationCache
Prerequisites
The demo and article rely on to earlier articles:
Roll-your-own-A-JavaScript-router and
JavaScript namespacing.
Although the last one isn't really important for the actual working of this code. It's more that you know what I mean when you see;
namespace("Demo.Controllers", function() {
The router code however I'll need to assume you know.
Next to that this will use jQuery (of course) and it will use the Mustache template engine. I've been using it for a while now and it does the job.
I could of tried to screw around with knockouts two way binding magic, but I didn't feel it would clarify things. I use Mustache as ASP.NET MVC uses
Razor.
There will be two namespaces: Core
and Demo
. Core will hold all the clue to create the MVC magic. And demo will be the implementation.
In the folder you'll also find core.js. This is just the start of the Core namespace and holds some helper functions. The most important one in there is
Core.apply
.
This function has been in my toolbox for some time now and it's proven to be very
useful. I stole this from the Ext library and what it does is copy one object into the other.
Controller and View
In this artice I'll take you on the tour of creating the controller and the view code.
I don't mind complicated code, but the controllers that will make the multiple views work, should be is simple as possible.
A controller in ASP.NET MVC looks really simple. All the complexity has been hidden inside the Controller class they inherit from.
That's the exact same approach we'll be taking.
The end result of what our home controller should end up looking like
namespace("Demo.Controllers", function(Namespace) {
var Home=function() {
Core.apply(this, Core.Controller.prototype);
return Core.Controller.apply(this, arguments);
};
Home.prototype.index=function(action, id) {
this.viewBag.datetime=new Date();
this.view();
}
Namespace["Home"]=Home;
})
You'll hopefully see a resemblance to ASP.NET MVC. The viewBag is what our Mustache template will consume. And the statement
this.view
will show us the result.
The mess in the constructor I will explain further down.
Our view template could look like this
<h2>Home</h2>
<div>It's now {{datetime}}<div>
Directory structure
In order to get the views the directory has a mandatory structure. At least for where the views should be.
- assets (holds the core code and CSS etc.)
- controllers
- views
- index.html
So a route "#home/index" will use "controllers/home.js" to get the view "views/home/index.html".
Core.Controller class
In the constructor of our Home controller we saw some weird statements. First Core.apply(this, Core.Controller.prototype);
and than
return Core.Controller.apply(this, arguments);
The first is an alternative to inheritance. Instead of the normal way Home.prototype=new Core.Controller();
, this copies the prototype into itself.
"Why?", you say? Because the constructor of Core.Controller
doesn't return itself. But it returns a function. If you've haven't read the router article by now you should......... Done? The router deals with functions, not classes.
It starts a function on basis of the route presented. And I feel it should stay that way. If we'd feed actual object instances, it would have know how to start the right function inside them.
I don't want to go there. But I do want it to start an instance. Let me clarify things by showing you the way the route to our home controller is set up in the main
index.html.
$(document).ready(function() {
router.registerRoute("home/:action:/:id:", new Demo.Controllers.Home("home", "wrapper"));
});
router.registerRoute
takes a function as its argument. But here it's also creating an instance of the Home controller. The secret is in the
Core.Controller
class.
namespace("Core", function() {
Core.Controller=function() {
this.viewBasePath=arguments[0];
this.container=arguments[1]? "#"+arguments[1]: "body";
this.viewBag={};
this.viewTemplates={};
var _self=this;
return function() {
var controllerFunction=arguments[0] ? arguments[0] : "index";
_self.viewSubPath=controllerFunction;
if(typeof _self[controllerFunction] == "function") {
_self[controllerFunction].apply(_self, arguments);
}
else {
_self.unknownView();
}
}
}
........
As you can see it actually returns a function. This is the function the router will start.
It is passed on down through the Home controller by this statement:
return Core.Controller.apply(this, arguments);
The trick in keeping this function inline with the class instance is caused by the function scope of
JavaScript.
Because we cached _self
before. JavaScript will move up the function scope the find _self. It will end up in
the constructor of Core.Controller
. Where because of return Core.Controller.apply(this, arguments);
the scope is now
our Home controller which has the whole prototype of Core.Controller
.
Maybe you'll like to lay down at this point. I won't blame you
So let's recap:
The router starts an instance of our Demo.Controllers.Home
class, while he needs a function.
The Demo.Controllers.Home
instance copies the prototype of
Core.Controller
into itself and returns a call to the constructor of the
Core.Controller
prototype.
The Core.Controller
instance sets some variables, stores a reference to itself and returns a function instead if it's own instance down to the router.
This is a nice trick which allows you to pass a instance of something to something else that will just accept a function. The router shouldn't be burdened with all of this. He is a one trick pony and he should stay that way.
I used my own router here, so I could of changed it, but if I'd rather use a third party router like
Crossroad.js, I can still use it this way.
Using a function as the return value of a constructor also gives you the possibility to make a class that has private functions and variables. But that's for a different article.
However there is a drawback. You can't use the classic inheritance like Home.prototype=new Core.Controller();
.
Why not make it a singleton pattern?
There will be only one home route so why not make the home controller a singleton? The reason is that home will not be the only route and I'd like to hide the complexity in
Core.Controller
.
So I'll need multiple instances of him.
Dive in to the return/router function
Let's look at the actual function Core.Controller
returns:
var controllerFunction=arguments[0] ? arguments[0] : "index";
We've asked the router to look for
"home/:action:/:id:"
So action could be empty. In that case we'll make it "index".
_self.viewSubPath=controllerFunction;
Stored for later reference.
if(typeof _self[controllerFunction] == "function") {
_self[controllerFunction].apply(_self, arguments);
}
else {
_self.unknownView();
}
If we have a function that has the same name as the action variable the router passed. Start that. This will lead to:
Home.prototype.index=function(action, id) {
this.viewBag.datetime=new Date();
this.view();
}
The view function
Now the part it was al about. We want to get views/home/index.html and stick it into the page.
So the Core.Controller
has the view function. This will get the template via
AJAX if it hadn't already stored it.
And then start the renderView
function, where Mustache will stick the viewBag into the template.
The variable this.container
was passed in the constructor when we initially set up the route.
It's wrapper here and that should obviously be some div in the main index.html to stick the template in.
Core.Controller.prototype.view=function() {
if(this.viewTemplates.hasOwnProperty(subView)) {
this.renderView(subView);
}
else {
var _self=this;
$.ajax({
url: "views/"+this.viewBasePath+"/"+subView+".html",
success: function(data) {
_self.viewTemplates[subView]=data;
_self.renderView(subView);
}
});
}
}
Core.Controller.prototype.renderView=function(subView) {
$(this.container).html(Mustache.render(this.viewTemplates[subView], this.viewBag));
}
What's in the download package
At least one more controller. The contacts controller. Which has more than one view.
And also handles data. (sort of).
So it should clarify things some more.
Putting all of it in the article would make it way to long. Download it. It's the only way to really understand this article.
If you've read this far you should.
Conclusion
I have to admit that I haven't got any idea if this explanation has been clear enough. But please download the code and run it, read it, change it.
In the next few articles we'll expand on all this making the use of the
Core.Controller
class even more clear.
The next one will be about making the model part of the equation.