Click here to Skip to main content
15,886,741 members
Articles / Web Development / HTML

JavaScript MVC Style Framework in Less Than 100 Lines of Code

Rate me:
Please Sign up or sign in to vote.
4.93/5 (7 votes)
2 Feb 2015MIT7 min read 29.6K   241   15  
Learn how to create your own MVC-style JavaScript framework
This article will show you how to create your own MVC-style JavaScript Framework in less than 100 lines of code.

Introduction

Anyone who has ever worked on JavaScript frameworks like AngularJS, Backbone or Ember is familiar with how the concept of MVC works on the UI. These frameworks make it very easy for us to implement custom views based on the URL in a single page application. In fact, that is the core of the model-view-controller terminology, and that is to have a controller to handle incoming requests, a view to show the information and the model for everything else from business rules to data-access.

Keeping the above information in mind, when we need to create such an application in which there is a requirement to switch views in a single page, then we normally use one of the above mentioned libraries/frameworks. But what if we only want a URL based view-switching framework and not the extra goodies and features that come bundled along with that like they do in Angular and Ember. This article is an attempt to address that specific requirement and to have a solution which is both simple and useful at the same time.

The Concept

The code utilizes the pound(#) based URLs to implement MVC style navigation in the application. The application starts with a default pound URL, and based on the hash value, the code loads the applicable view and applies the object-model to the template of the view.

The format of the URL will be like following:
http://Domain Name/index.html#/Route Name

The view content must bind values with object model's properties in the form of {{Property-Name}}. The code will then look for this specific template format and will then replace them with the value of their enclosing property name in the object model.

The views are loaded asynchronously using Ajax are put in a placeholder on the HTML page. The view placeholder can be any element (ideally should be a Div) but it must have a specific attribute so that it can be identified by the code, and this also helps in achieving unobstrusive code implementation. When the URL is changed, then this cycle repeats and another view is loaded. Sounds simple, right! The following flowchart explains the information flow in this particular implementation.

Image 1

Writing the Code

We will start off with the basic modular design pattern and will expose our library to the global scope through an Object facade at the end.

JavaScript
; (function (w, d, undefined) { //rest of the code })(window, document);

We need to store the view element in a variable so that it can be used multiple times.

JavaScript
var _viewElement = null; //element that will be used to render the view  

We will need a default route if no route information can be found in the URL, so that the default view can be loaded instead of a blank screen.

JavaScript
var _defaultRoute = null;

Now it's time to create the constructor function of our main MVC object. We will be storing the information of the routes in an object called _routeMap.

JavaScript
var jsMvc = function () {
    //mapping object for the routes
    this._routeMap = {};
}

After this, it's time to create the route object in which we will store the information about the route, template and the controller.

JavaScript
var routeObj = function (c, r, t) {
    this.controller = c;
    this.route = r;
    this.template = t;
}

There will be a separate route object routeObj for every different URL. All of those objects will be added to our _routeMap object so that we can later retrieve them by means of key-value pair associations.

To add routes to the MVC library, we will need to expose a function from the library facade. So let's create a function that can be used to add new routes with their respective controllers.

JavaScript
jsMvc.prototype.AddRoute = function (controller, route, template) {
    this._routeMap[route] = new routeObj(controller, route, template);
}

The function AddRoute accepts three arguments; controller, route and template. They are:

  • controller: The reference to the controller function that will be called whenever this particular route is accessed.
  • route: Path of the route. This is simply the part that we expect after the pound(#) sign in the URL.
  • template: This is the external HTML file which will be loaded as a view for this route.

Now, we need an entry point for our library to start parsing the URL and serving the associated HTML templates to the page. To do that, we will need a function. Initialize function is doing the following things:

  1. Get the reference of the view element initially. The code expects an element with the attribute view so that it can be searched in the HTML page.
  2. Set the default route.
  3. Validate the view element.
  4. Bind the window hash change event so that views can be updated properly in the event of a different hash value in the URL.
  5. Finally, start the MVC support.
JavaScript
//Initialize the MVC manager object to start functioning
jsMvc.prototype.Initialize = function () {
    var startMvcDelegate = startMvc.bind(this);

    //get the HTML element that will be used to render the view  
    _viewElement = d.querySelector('[view]');        
    if (!_viewElement) return; //do nothing if view element is not found    

    //Set the default route
    _defaultRoute = this._routeMap[Object.getOwnPropertyNames(this._routeMap)[0]];    

    //start the MVC manager
    w.onhashchange = startMvcDelegate;
    startMvcDelegate();
}

In the above code, we are creating a function delegate startMvcDelegate from startMvc function. This delegate will then be called every time the hash value changes. Following is the sequence of steps that we need to perform every time the hash value is changed:

  1. Get the hash value.
  2. Get the route value from the hash.
  3. Get the route object routeObj from the route map object _routeMap.
  4. Get the default route object if no route is found in the URL.
  5. Finally, call the controller associated with the route and serve the required view in the view element.

All the above steps are done in the following startMvc function code:

JavaScript
//function to start the mvc support
function startMvc() {
    var pageHash = w.location.hash.replace('#', ''),
        routeName = null,
        routeObj = null;                
        
    routeName = pageHash.replace('/', '');          //get the name of the route from the hash  
    routeObj = this._routeMap[routeName];           //get the route object    

    //Set to default route object if no route found
    if (!routeObj)
        routeObj = _defaultRoute;
    
    loadTemplate(routeObj, _viewElement, pageHash); //fetch and set the view of the route
}

Next, we need to load the appropriate view asynchronously using XML Http Request. We will pass the values of route object and the view element to the function loadTemplate for that purpose.

JavaScript
//Function to load external html data
function loadTemplate(routeObject, view) {
    var xmlhttp;
    if (window.XMLHttpRequest) {
        // code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp = new XMLHttpRequest();
    }
    else {
        // code for IE6, IE5
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
    }
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
            loadView(routeObject, view, xmlhttp.responseText);
        }
    }
    xmlhttp.open('GET', routeObject.template, true);
    xmlhttp.send();
}

All that is left now is to load the view and bind the object model with the view template. We will create an empty model object and then will invoke the route's controller function along with passing the model reference to that function. The updated model object will be then binded with the HTML template loaded previously in the XHR call.

loadView function will be used to call the controller function and to prepare the model object.
replaceToken function will be used to bind the model with the HTML template.

JavaScript
//Function to load the view with the template
function loadView(routeObject, viewElement, viewHtml) {
    var model = {}; 

    //get the resultant model from the controller of the current route  
    routeObject.controller(model); 

    //bind the model with the view    
    viewHtml = replaceToken(viewHtml, model); 
    
    //load the view into the view element
    viewElement.innerHTML = viewHtml; 
}

function replaceToken(viewHtml, model) {
    var modelProps = Object.getOwnPropertyNames(model),
        
    modelProps.forEach(function (element, index, array) {
        viewHtml = viewHtml.replace('{{' + element + '}}', model[element]);
    });
    return viewHtml;
}

Finally, we will expose the plugin to the outside world of JavaScript Global Scope.

JavaScript
//attach the mvc object to the window
w['jsMvc'] = new jsMvc();

Now it's time to use this MVC plugin in our single page application. In the next code snippet, the following is happening:

  1. Import the code in the web page.
  2. Add the routes with their controller and view template information.
  3. Create the controller functions.
  4. Finally, initialize the library.

Apart from the above, we will need links so that we can navigate to different routes, and a container element with the view attribute to contain the view template HTML.

HTML
<!DOCTYPE html>
<html>
<head>
    <title>JavaScript Mvc</title>
    <script src="jsMvc.js"></script>
    <!--[if lt IE 9]>
        <script src="jsMvc-ie8.js"></script>
    <![endif]-->
    
    <style type="text/css">
        .NavLinkContainer {
            padding: 5px;
            background-color: lightyellow;
        }

        .NavLink {
            background-color:black;
            color: white;
            font-weight:800;
            text-decoration:none;
            padding:5px;
            border-radius:4px;
        }
            .NavLink:hover {
                background-color:gray;
            }
    </style>
</head>
<body>
    <h3>Navigation Links</h3>
    <div class="NavLinkContainer">
        <a class="NavLink" href="index.html#/home">Home</a>&nbsp;
   
        <a class="NavLink" href="index.html#/contact">Contact</a>&nbsp;

        <a class="NavLink" href="index.html#/admin">Admin</a>&nbsp;
       
    </div>
    <br />
    <br />
    <h3>View</h3>
    <div view></div>
    <script>
        jsMvc.AddRoute(HomeController, 'home', 'Views/home.html');
        jsMvc.AddRoute(ContactController, 'contact', 'Views/contact.html');
        jsMvc.AddRoute(AdminController, 'admin', 'Views/admin.html');
        jsMvc.Initialize();

        function HomeController(model) {
            model.Message = 'Hello World';
        }

        function ContactController(model) {
            model.FirstName = "John";
            model.LastName = "Doe";
            model.Phone = '555-123456';
        }

        function AdminController(model) {
            model.UserName = "John";
            model.Password = "MyPassword";
        }
    </script>
</body>
</html>

In the above code, there is a conditional comment for Internet Explorer.

HTML
<!--[if lt IE 9]>
    <script src="jsMvc-ie8.js"></script>
<![endif]-->

If IE version is less than 9, then the properties like function.bind, Object.getOwnPropertyNames and Array.forEach will not be supported. So we need to fallback to the code supported by the browsers lower than IE 9.

The contents of home.html, contact.html and admin.html are as follows:

home.html

HTML
{{Message}}

Image 2

contact.html

HTML
{{FirstName}} {{LastName}}
<br />
{{Phone}}

Image 3

admin.html

HTML
<div style="padding:2px;margin:2px;text-align:left;">
    <label for="txtUserName">User Name</label>
    <input type="text" id="txtUserName" value="{{UserName}}" />
</div>
<div style="padding:2px;margin:2px;text-align:left;">
    <label for="txtPassword">Password</label>
    <input type="password" id="txtPassword" value="{{Password}}" />
</div>

Image 4

The code in its entirety can be downloaded from the given links.

How to Run the Code

Running the code is easy, we need to create a web application in the web server of our choice. The following example shows how to do that in IIS Manager:

First, add a new Web Application under Default Web Site.

Image 5

Set the required properties like Alias, Physical Path, Application Pool and User Credentials; click OK after that.

Image 6

Finally, open the web application contents and browse the HTML page you want.

Image 7

This is necessary because the code loads views stored in external files and browsers do not allow that if our code is not running in a dedicated hosted environment. As an alternative, if you are running Visual Studio, then right click on the HTML page and select 'View In Browser'.

Browser Support

Most of the modern browsers support this code, for IE 8 and lower, there is a separate script which unfortunately goes way more than 100 lines. However, it may not be 100% cross browser and you may need to tweak some areas in case you decide to use this in your project.

Points of Interest

This example demonstrates that we really don't need entire libraries and frameworks for very specific needs. A web application is resource intensive and it is much better to use only what is needed and discard the rest.

As of now, there is not much that can be done with this code. Stuff like web service calling, dynamic event binding, etc. cannot be done. I will soon provide an updated version to support more features.

History

  • 2nd Feb 2015: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
India India
Just a regular guy interesting in programming, gaming and a lot of other stuff Smile | :)

Please take a moment to visit my YouTube Channel and subscribe to it if you like its contents!
My YouTube Channel

Don't be a stranger! Say Hi!!

Cheers!

Comments and Discussions

 
-- There are no messages in this forum --