Click here to Skip to main content
Click here to Skip to main content

HTML5 offline MVC: Part 1

, 6 Apr 2013
Rate this:
Please Sign up or sign in to vote.
Offline web app in MVC.

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()    {
        //The construtor
        
        Core.apply(this, Core.Controller.prototype);
        
        return Core.Controller.apply(this, arguments);
    };

    Home.prototype.index=function(action, id)    {
        //home/index
        //starts home/index.html
        
        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
    • contacts.js
    • home.js
  • views
    • contacts
      • contact.html
      • index.html
    • home
      • index.html
  • 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()  {
        
        //what's the head part of our route?
        //this will be "home" coming from our constructor arguments!
        this.viewBasePath=arguments[0];
        
        //did we get a container id or should we just use body
        //e.g. the div to render the view in
        //this will be "wrapper" coming from our constructor arguments!
        this.container=arguments[1]? "#"+arguments[1]: "body";
        
        //the future data to be passed into the template
        this.viewBag={};
        
        //the view templates, we will cash them here to prevent http request
        this.viewTemplates={};
        
        //reference to ourself for the return function!
        var _self=this;
        
        return function()   {
            //we'll get the first argument out of our router function! Not the constructor!
            var controllerFunction=arguments[0] ? arguments[0] : "index";
            
            //So now we'll have the controller(_self.viewBasePath) and the function within that controller
            _self.viewSubPath=controllerFunction;
            
            //Do we actually have this function
            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 Smile | :)

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.

//Do we actually have this function
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)    {
    //home/index
    //starts home/index.html
    
    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)) {
        //if we've already stored the template use that
        this.renderView(subView); 
    }
    else    {
        //we didn't have this template, so we'll have to get it with jQuery
        var _self=this;
        $.ajax({
            url: "views/"+this.viewBasePath+"/"+subView+".html",
            success: function(data) {
                _self.viewTemplates[subView]=data;
                _self.renderView(subView); 
            }
        });
    }    
}

//Render the template
Core.Controller.prototype.renderView=function(subView)   {
    //The container is #wrapper, which came from the contructor function
    //you could check here if this was alreay an jQuery object or just an id, but jQuery does that for you and I'm lazy
    $(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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Sebastiaan Meijerink
Software Developer (Senior) none
Netherlands Netherlands
I'm a developer with 12+ years of experience. Starting of on a MVS mainframe, moving to building big multi-tier ERP systems with unix backends, to building web-based BI-Portals. I've seen a lot of different languages, environments and paradigmes.
 
At this point my main interest is webdevelopment. Mainly javascript and ASP.NET. But I also like getting my hands dirty on some PHP.
 
I've been a member of CodeProject for many years, but only recently started writing some articles. I hope someone will enjoy them.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 7 Apr 2013
Article Copyright 2013 by Sebastiaan Meijerink
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid