In this article, we will review the process of using JavaScript, from an MVC-based perspective, to manipulate the DOM. More specifically, we’ll engineer our JavaScript objects, their properties and methods, and their instantiations parallel to the intended behavior of our Views (what the user sees).
Consider Your Views As Objects, Not As Pages
At any point in the development of a web page, we are using a language that naturally promotes either class-based development or object-based development. In strongly-typed languages like Java and C#, we are usually writing our views in classes – giving them state, scope, and context. When we are working with languages like PHP or newer view engines, like Razor for ASP.NET, our views may simply be markup (HTML/CSS) mixed with templating. However, this does not mean we have to change our perception on how the view behaves as its own stateful entity.
Within Views, we are primarily working with HTML, which consists of nested elements; these elements have attributes which describe what their semantic purpose is or how they appear when rendered. These elements then have children or parent elements that inherit/provide cascading (through CSS) and block/inline behaviors. These elements can naturally be viewed from an OOP (Object-Oriented Programming) perspective. Consider, for example, the following markup:
div.container {
border: 1px solid #333;
padding: 5px;
color: red;
}
<div class="container">Result :
<h2>About Our Company</h2>
</div>
As you can see above, the header inherited its font color property from its parent container though the CSS behavior of cascading. This behavior is quite similar to the concept of inheritance in OOP. We can also see that the header is a child of the container, inheriting certain properties, based on the behavior of the element. When we see our elements from this perspective, we have a better definition of what we intend to do with our view elements and can encapsulate styles and functionality better.
Inside a view, we will have markup. However, this markup may have nested partial views like sidebars, a header, a footer, a right (or left) rail, and one or more content sections. All of these partial views should be viewed as their own entity, capable of having their own state, context, and scope.
Translating This Concept Into Your Styles and Scripts
Many developers tend to write JavaScript from a procedural or functional point of view, and often neglect to consider the natural tendencies offered in view-based development approaches and parallel instantiation (creating a new instance of the view as we create a new instance of a JavaScript object corresponding to that view) when working in MVC Frameworks. It’s often the case that I run into JavaScript files that are just one method after another. Though this behavior works, and is common, it is not very efficient for code maintenance, debugging, or extension of current or future code when you are working extensively with views.
To get away from this habit and begin writing better behavioral code, when you begin to lay out your View’s scripting and styles, follow these general rules:
Golden Rules of View-based JavaScript Development
- Every view that is rendered from an action on a controller should have its own JavaScript object.
- Every Partial View that is loaded inside a View should have its own JavaScript object.
- Name your objects the same as your views (or partial views). This will make more sense for you and everyone else that touches your code.
- Use Pascal case for all objects (i.e. About, Sidebar, etc.). Your views should already, so why not do the same for your JavaScript objects?
- All constants of these objects should be stored in the constructor. This means if your view has properties that will be used in multiple methods, these methods can all access these properties.
- All methods that will be called on a view (or partial view) should be bound to the prototype of the object that corresponds to that view.
- All event bindings for the view (or partial view) should be contained within their own event binding’s method, which is placed on the prototype.
Consider the following diagram:
I generally create view-specific scripts and styles and then grab what I need from the main stylesheets and script libraries I’ve created that would be used on many views. This also reduces the amount of code that is used.
Creating View-based Objects
In this article, we will be laying out the structure for the About Us page on an MVC-based site. To start, we will create the structure as shown above in the previous diagram. From there, we will create an About object, and begin adding methods to the prototype. First, consider the following visual layout:
This is a very logical and commonly used layout for a webpage. We can segment our page into seperate visual objects. For each of these views, we can create a logical object that will correspond to it. I generally omit the repetitive information in the filename or classname that is used by MVC to determine the URI from the route and instead stick with something that is easy to keep consistent.
For page views, I generally call my JavaScript objects by the name of the view. Here is an example of my AboutView Object:
// View Filename: AboutView.cs (.NET MVC 1.0), About.cshtml (.NET MVC 3.0), or AboutView.php (PHP)
var About = function(pageTitle) {
this.pageTitle = pageTitle;
// binding events as soon as the object is instantiated
this.bindEvents();
};
In the above example, we created a JavaScript object in the function format, giving it the capacity to serve as an Object constructor for all methods called for the about view. By choosing this format, we can instantiate a new instance of this, just as we do with our view Server-Side (by saying new AboutView();
). From here, we can assign properties and methods to this object. In order to assign methods to this object, we will need access to the object’s prototype.
JavaScript’s Prototype is your Friend
Developers are often thwarted by the elusiveness (and ambiguity) of JavaScript’s Object Prototype.
Developers are often thwarted by the elusiveness (and ambiguity) of JavaScript’s Object Prototype. For many, it can be confusing to use and understand and adds another dimension to coding. As JavaScript becomes more event-driven with HTML5, AJAX, and web 2.0 concepts, JavaScript tends to lean naturally to procedural development that is easy to develop but hard to maintain, scale, and replicate.
Think of the word Prototype as a misnomer for now. When I think Prototype, I think of a “rough draft” or a base for inheritance, but this isn’t exactly the case.
” In reality, the better perspective for Prototype would be the Object’s Pointer in memory.”
When we create an object, we then instantiate a new instance of it. When we do that, we create a place in memory that the object can be referenced (remember, Objects in JavaScript are reference types, not primitive types; creating another variable equal to that object and then changing its values will actually change the original object in the pointer). When we create an object, instantiate a new instance of it, and then modify its “Pointer,” or Prototype, we add fields and methods to that object in memory directly (obviously we want to add all of these things before instantiation).
Here’s an example of creating methods on the About
object’s prototype:
var About = function(pageTitle) {
this.pageTitle = pageTitle;
// binding events as soon as the object is instantiated
this.bindEvents();
};
var About.prototype.bindEvents = function() {
// Current context: 'this' is the About object
// Place all your event bindings in one place and call them out
// in their own methods as needed.
$('ul.menu').on('click', 'li.search', $.proxy(this.toggleSearch, this));
};
var About.prototype.toggleSearch = function(e) {
//Toggle the search feature on the page
};
As you can see above, we have contained the properties of the About object within the constructor, have created a single point of reference for binding events (in this case we are using jQuery to create the event bindings, but you can use any framework or JavaScript itself), and have placed the toggleSearch method on the prototype of the About object to contain that method to that object. We have also called the bindEvents()
method in the object so that it is called on instantiation.
Now, consider the following code for the Sidebar Partial View:
var pSidebar = function(pageTitle) {
this.pageTitle = pageTitle;
// call the bindEvents method on instantiation of the pSidebar object.
// this will bind the events to the object
this.bindEvents();
};
var pSidebar.prototype.bindEvents = function() {
//current context: 'this' is the Sidebar object
$('ul.menu').on('click', 'li.has-submenu', $.proxy(this.toggleSubMenu, this));
$('input#search').on('click', $.proxy(this.openSearch, this));
};
var pSidebar.prototype.toggleSubMenu = function(e) {
// toggle the submenus
// current context: 'this' is the pSidebar obj
};
NOTE: I called the object pSidebar
because this is a partial view, not a full view. This is my preference to distinguish between the two, but makes things clearer.
The beauty of using this approach is – we can use the same method names we used in the About object and we will have no conflicts. This is because these methods are bound to the object’s prototype itself, not the global namespace. This simplifies our code and allows for a sort of “templating” for future scripting.
Instantiate Only as Needed
Once you have created your objects, calling them is simple. No longer do you need to depend on your framework to fire events when your document is loaded or ready. Now, you can simply instantiate your object and its events will be bound and executed as needed. So, let’s instantiate our About
object:
Inside your view where you would call your view specific scripts (dependent upon your templating language), simply call a new instance of your object and include the file as follows:
<script src="/path/to/scripts/views/about.js"></script>
<script>
new About("About Us");
</script>
As you can see, I passed in the page title for the view (which can be any argument for any need – even Model Data. This gives you excellent context over your model data and allows you to manipulate that data in JavaScript very easily.
Just like your About
Object, calling your partial views is just as easy. I would highly recommend calling new instances of your partial view JavaScript objects within the object’s constructor – this ensures that you are only calling these as needed and that they are collectively in one place.
var About = function(pageTitle) {
this.pageTitle = pageTitle;
//assigning a new instance of the Sidebar Partial View to be referenced later
this.sidebar = new pSidebar(pageTitle);
//NOTE: If you don't need to reference a partial view after the fact,
//you can simply instantiate an instance of it without assigning it within the object's constructor, as so:
new pSidebar(pageTitle);
//doing the same for the Partial Footer View
this.footer = new pFooter();
// binding events as soon as the object is instantiated
this.bindEvents();
};
As you can see, by referencing the Sidebar object as a local property of the About object, we now bind that instance, which is a very natural behavior – this instance is now the About Page’s Sidebar.
If you don’t need to reference a partial view after the fact, you can simply instantiate an instance of it without assigning it within the object’s constructor, as so:
var About = function(pageTitle) {
this.pageTitle = pageTitle;
new pSidebar(pageTitle);
// binding events as soon as the object is instantiated
this.bindEvents();
};
From here, all we need to do is add another script to our scripts called in our view:
<script src="/path/to/scripts/views/about.js"></script>
<script src="/path/to/scripts/partials/sidebar.js"></script>
<script>
new About("About Us");
</script>
Why This Technique is Beneficial
Once this structure is in place, we can then tailor our JavaScript object to match our view and apply the needed methods to that object to maintain scope. By creating a view-parallel object and working off that object’s prototype, we see the following benefits:
- The nomenclature makes it easier to navigate through code
- We naturally namespace our objects, reducing the need for long method names and too much use of anonymous closure.
- Little to no conflict in other code because our methods are on the prototype of the object, not on the global level
- When instantiating our partial views within our View’s object constructor and assigning them to a local variable reference, we effectively create a locally bound copy of that Partial View’s object.
- We have a firm definition of context and are able to use the keyword ‘this’ without worry.
- Debugging becomes clear because all methods shown in the stack are bound in one place.
Conclusion
As the MVC Design Pattern continues to become more popular in the design world, the development of JavaScript Objects to accompany DOM Element manipulation will change to be more tailored towards view-specific and event-specific manipulation. By tailoring our JavaScript objects to instantiate in parallel with our Views, we can have a hand-in-hand stateful relationship between the two – one that is symantically in good taste, easy to step through, simple to maintain, and perfect for expansion as the view grows or changes, creating a permeable and expandable relationship between markup and scripting.
By utilizing an Object’s Prototype, we are able to maintain a precise context on our View’s scripting object and expand that object with a repetitive development frame-of-mind. We can then replicate this format through our partial views, saving us time, brain power, and risk of bugs and unexpected behavior.
No comments:
Post a Comment