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.
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.
; (function (w, d, undefined) {
We need to store the view
element in a variable so that it can be used multiple times.
var _viewElement = null;
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.
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
.
var jsMvc = function () {
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
.
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.
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:
- 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. - Set the default route.
- Validate the view element.
- Bind the window hash change event so that views can be updated properly in the event of a different hash value in the URL.
- Finally, start the MVC support.
jsMvc.prototype.Initialize = function () {
var startMvcDelegate = startMvc.bind(this);
_viewElement = d.querySelector('[view]');
if (!_viewElement) return;
_defaultRoute = this._routeMap[Object.getOwnPropertyNames(this._routeMap)[0]];
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:
- Get the hash value.
- Get the route value from the hash.
- Get the route object
routeObj
from the route map object _routeMap
. - Get the default route object if no route is found in the URL.
- 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:
function startMvc() {
var pageHash = w.location.hash.replace('#', ''),
routeName = null,
routeObj = null;
routeName = pageHash.replace('/', '');
routeObj = this._routeMap[routeName];
if (!routeObj)
routeObj = _defaultRoute;
loadTemplate(routeObj, _viewElement, pageHash);
}
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.
function loadTemplate(routeObject, view) {
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
}
else {
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.
function loadView(routeObject, viewElement, viewHtml) {
var model = {};
routeObject.controller(model);
viewHtml = replaceToken(viewHtml, model);
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.
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:
- Import the code in the web page.
- Add the routes with their controller and view template information.
- Create the controller functions.
- 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.
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Mvc</title>
<script src="jsMvc.js"></script>
<!--
<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>
<a class="NavLink" href="index.html#/contact">Contact</a>
<a class="NavLink" href="index.html#/admin">Admin</a>
</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.
<!--
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
{{Message}}
contact.html
{{FirstName}} {{LastName}}
<br />
{{Phone}}
admin.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>
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.
Set the required properties like Alias, Physical Path, Application Pool and User Credentials; click OK after that.
Finally, open the web application contents and browse the HTML page you want.
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