Reason
It’s an undeniable fact that JavaScript is becoming as important to the functionality of modern websites as the .NET code that Sitecore developers write on a daily basis, but more often than not JavaScript seems to be written hastily and with very little structure, if any.
The following article describes two simple patterns found in Learning JavaScript Design Patterns which I’ve found helpful in improving the maintainability and reuse of JavaScript. Some examples use jQuery 1.7.2.
Code
JavaScript namespaces
Due to the dynamic nature of JavaScript, namespacing simply entails creating a “top level object” and nesting other objects and functions within it, as shown below.
The level of namespace nesting is a matter of personal preference; the following example simply creates a top level namespace called “Customer” in which an object called “Viewport” is nested, which in turn contains some helper functions.
Example
// When using the same namespace across multiple files, make sure only to create it once. if(typeof Customer == "undefined") Customer = {}; Customer.Viewport = { currentWidth: window.innerWidth, smallDeviceWidth: 640, tabletDeviceWidth: 1536, isSmallDevice: function(){ // Consider possible scope issues when using the "this" keyword instead of the "fully qualified name". return Customer.Viewport.currentWidth <= Customer.Viewport.smallDeviceWidth; }, isTabletDevice: function(){ return !Customer.Viewport.isSmallDevice() && Customer.Viewport.currentWidth <= Customer.Viewport.tabletDeviceWidth; } }
Code
JavaScript modules
This technique has certain similarities to the factory and decorator pattern popularized by GoF. In the example shown below, the namespacing technique described earlier is used to define two functions (“Gallery” and “GalleryElement”) within the namespace “Customer”. Each function has a single parameter, intended to be a HTML element.
Within each of these “factory methods”, various variables and functions are created which exist within the scope of the enclosing function (e.g. “Customer.Gallery”), allowing private or public access depending on whether or not they’re “decorating” the HTML element (i.e. “element.fu = function(){…}” vs. “function fu(){…}” and “element.overlaySize = 20” vs. “var overlaySize = 20”).
When accustomed to working with C# and similar languages, the use of scope to create “private” fields and functions (and the ever changing answer to “what does this point to?”) can be confusing – a bit of googling can help to clear things up.
Example
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>Organizing JavaScript</title> <style type="text/css"> .gallery { width: 640px; margin-left: auto; margin-right: auto; } .gallery-elements { list-style-type: none; height: 130px; } .gallery-elements li { float: left; margin-right: 15px; cursor: pointer; padding: 2px; } .gallery-elements .selected { border: 3px dashed Gray; } .gallery-elements .full-size-image { max-height: 125px; } .gallery-elements .thumbnail-image { max-height: 75px; } .gallery-content { clear: both; text-align: center; } .gallery-content .full-size-image { max-height: 300px; } </style> <script type="text/javascript" src="http://code.jquery.com/jquery-1.7.2.min.js"></script> <script type="text/javascript"> if(typeof Customer == "undefined") Customer = {}; Customer.Gallery = function(container){ // Private fields var $ = jQuery; var gallery = container; var galleryContent = $(".gallery-content", gallery); var galleryElements = $(".gallery-elements li", gallery).each(createGalleryElement); // Private functions function createGalleryElement(){ return Customer.GalleryElement(this); }; function setSelected(galleryElement){ galleryElements.removeClass("selected"); $(galleryElement).addClass("selected"); }; // Public functions gallery.showFirstImage = function(){ if(galleryElements.length == 0) return; var firstGalleryElement = galleryElements[0]; firstGalleryElement.showImageIn(galleryContent); setSelected(firstGalleryElement); }; // Set events galleryElements.click(function(){ this.showImageIn(galleryContent); setSelected(this); }); return gallery; } Customer.GalleryElement = function(container){ // Private fields var $ = jQuery; var galleryElement = container; var thumbnailImage = $(".thumbnail-image", galleryElement); var fullSizeImage = $(".full-size-image", galleryElement); // Initialize fullSizeImage.hide(); // Private functions function showThumbnailImage(){ fullSizeImage.hide(); thumbnailImage.show(); }; function showFullSizeImage(){ thumbnailImage.hide(); fullSizeImage.show(); }; // Public functions galleryElement.showImageIn = function(element){ $(element).empty(); var image = fullSizeImage.clone(); $(element).append(image); image.show(); }; // Set events $(galleryElement).hover(showFullSizeImage, showThumbnailImage); return galleryElement; } </script> </head> <body> <section class="gallery"> <ul class="gallery-elements"> <li> <img class="full-size-image" alt="Image" src="http://1.bp.blogspot.com/-2dAGkAm7bnA/TelKXKl62DI/AAAAAAAABaE/6PION-vwGD8/s400/cute-puppy-pictures1.jpg"/> <img class="thumbnail-image" alt="Thumbnail" src="http://1.bp.blogspot.com/-2dAGkAm7bnA/TelKXKl62DI/AAAAAAAABaE/6PION-vwGD8/s400/cute-puppy-pictures1.jpg"/> </li> <li> <img class="full-size-image" alt="Image" src="http://4.bp.blogspot.com/-_zMPmxGw2os/TelKZWp4AtI/AAAAAAAABaI/pMWFvpkAogQ/s1600/cute-puppy-pictures2.png"/> <img class="thumbnail-image" alt="Thumbnail" src="http://4.bp.blogspot.com/-_zMPmxGw2os/TelKZWp4AtI/AAAAAAAABaI/pMWFvpkAogQ/s1600/cute-puppy-pictures2.png"/> </li> <li> <img class="full-size-image" alt="Image" src="http://2.bp.blogspot.com/-D_q1hUzwEiU/TelKex4hq6I/AAAAAAAABaM/1jyECRVfqHI/s1600/cute-puppy-pictures3.jpg"/> <img class="thumbnail-image" alt="Thumbnail" src="http://2.bp.blogspot.com/-D_q1hUzwEiU/TelKex4hq6I/AAAAAAAABaM/1jyECRVfqHI/s1600/cute-puppy-pictures3.jpg"/> </li> <li> <img class="full-size-image" alt="Image" src="http://3.bp.blogspot.com/-BAmdzP3o288/TelKjerfBkI/AAAAAAAABaY/HB_Cy-KH2Po/s1600/cute-puppy-pictures4.jpg"/> <img class="thumbnail-image" alt="Thumbnail" src="http://3.bp.blogspot.com/-BAmdzP3o288/TelKjerfBkI/AAAAAAAABaY/HB_Cy-KH2Po/s1600/cute-puppy-pictures4.jpg"/> </li> </ul> <div class="gallery-content"> </div> </section> <hr /> <section class="gallery"> <ul class="gallery-elements"> <li> <img class="full-size-image" alt="Image" src="http://alltheragefaces.com/img/faces/png/troll-troll-face.png"/> <img class="thumbnail-image" alt="Thumbnail" src="http://alltheragefaces.com/img/faces/png/troll-troll-face.png"/> </li> <li> <img class="full-size-image" alt="Image" src="http://alltheragefaces.com/img/faces/png/determined-challenge-accepted.png"/> <img class="thumbnail-image" alt="Thumbnail" src="http://alltheragefaces.com/img/faces/png/determined-challenge-accepted.png"/> </li> <li> <img class="full-size-image" alt="Image" src="http://alltheragefaces.com/img/faces/png/me-gusta-me-gusta.png"/> <img class="thumbnail-image" alt="Thumbnail" src="http://alltheragefaces.com/img/faces/png/me-gusta-me-gusta.png"/> </li> </ul> <div class="gallery-content"> </div> </section> <script type="text/javascript"> $(document).ready(function(){ $(".gallery").each(function(){ var gallery = Customer.Gallery(this); gallery.showFirstImage(); }); }); </script> </body> </html>
this code doesn’t take advantages of prototype
function init() {
var data, customer;
data = {
name:’jim’,
ele: document.querySelectorAll(‘.gallery-elements li’)
};
customer = new Customer(data);
customer.observer();
}
function Customer (data) {
this.name = data.name;
this.clickables = data.ele;
}
Customer.prototype.observe() {
var i;
// your event listeners here
for (i = 0; i < this.clickables.length; i += 1) {
this.clickables[i].onclick = function (e) {
// go ninjas ; )
}
}
}