Over my career as a Web Developer I’ve come up with some pretty clever ways to achieve things (sometimes too clever, as a colleague would say :). But the following process is something I’d definitely use again, it’s thoroughly tested, critiqued by peers, and relatively clean.
Many JavaScript libraries that extend DOM (Document Object Model) functionality, also provide out-of-the-box components (sometimes called widgets or objects). Often, these libraries will ship with HTML markup which is used throughout their components. These components parse and use this markup to create or shape existing nodes in the model. Together these nodes compose and enhance UI components (menus, buttons, etc..) which make for an all-a-round more sophisticated webpage and user experience.
These frameworks often have an abstraction layer that makes it fairly easy to create, extend, and use components. Generally speaking, one would use the library’s methods to create objects and attach them to elements in your markup structure using selectors (often IDs). While this sounds trivial, it’s also repetitive. In practice, the use of these libraries tend to support the idea of stuffing markup into JavaScript for UI enhancements. This practice, when applied to custom written libraries, can subtract from the knowledge a server already has gained by rendering HTML markup structure in the first place.
I’ve developed JavaScript libraries using a different idea which allows the server to render most or all the markup needed for an enhancement, so that JavaScript, in many ways is reduced to binding/handling events and displaying a server-shipped structure. I think having markup which is primary sourced from the server encourages the re-usability of markup by the server.
At the root this process is an invocation layer I call an AutoInitter, (which could be a name for a lot of things), but the job of this code is to construct JavaScript objects, (or invoke functions) using class and data attributes provided by an HTML element.
Here’s an example of how it works:
Lets say you have some HTML element and you’d like to have some action – maybe a toggle of some sort. And when the page loads you want some content to be showing (state = “open”).
I might structure my HTML markup like so:
1 2 |
<div class='auto-init toggle' data-function='Lib.Toggler' data-arguments='{state: "open"}' data-instantiate='true'>Click Me!</div> <div class='content'>Here I am :) ...some content... </div> |
And my javascript might look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
var Lib = {}; Lib.Toggler = function(options){ // Note - our object, when instantiated using the AutoInitter, // constructs with the property "primaryTarget", // which is a reference to the html element this instance is tied to. this.clickMe = this.primaryTarget; this.content = $(this.primaryTarget).next(); //Using jQuery here this.state = options.state; // On construction, what to do when the page first loads? if(options.state == 'open') { this.open(); } else { this.close(); } // Toggling - setup click event handlers var clickMe = this; $(this.clickMe).click(function(event){ if(clickMe.state == "open") clickMe.close(); else clickMe.open(); }); }; Lib.Toggler.prototype.open = function(){ this.content.fadeIn(); //haha were just using jQuery :) this.state = "open"; } Lib.Toggler.prototype.close = function() { this.content.fadeOut(); this.state = "closed" } |
In Action:
So, what’s nice about this setup, is the logic above is all I have to think about. The trivial (but repetitive) stuff involved with actually finding my HTML elements, in JavaScript, disappears and is abstracted away. Infact, there is rarely a need to define an element ID attribute.
So how is the above code fired off? Like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
/** * Function: Instantiate With Scope * * This function instantiates a function with a given context (scope). Unfortuantly, the javascript "new" keyword doesn't allow you to do this: * var instance = new PseudoClass.apply(scope,args); * * So we have to recreate the functionality of the "new" keyword. * With ECMAScript 5, we could use following method: * But it's not cross browser, and we can't gurantee everyone has 5 or greater. * * var object = Object.create(PseudoClass,{ * 'id' : { * value: 'value1', * enumerable:true // writable:false, configurable(deletable):false by default * }, * 'name': { * value: 'value2', * enumerable: true * } * }); * * So, without further ado: * * @author Shaun Moen <shaun@dittyjamz.com> * @param constructor - a function to instantiate * @param args - an array of arguments to pass to the constructor * @param scope - the scope to pass in, which becomes "this" in the instaniated function **/ Lib.instantiateWithScope = function (constructor, args, scope) { PseudoClass.prototype = constructor.prototype; function PseudoClass() { $.extend(this,scope); return constructor.apply(this, args); } return new PseudoClass(); }; /** * Object Literal: AutoInitter * * Given a selector, the following logic finds all elements with the "auto-init" class * and calls a function (or constructor) using the provided HTML attributes: * * "data-function" * "data-arguments" * "data-instantiate". * * For each function called, a context or scope is created, which contains * the property "primaryTarget" that references the * DOM Element invoking it. * * @author Shaun Moen <shaun@dittyjamz.com> */ Lib.AutoInitter = {}; Lib.AutoInitter.completedCount = 0; Lib.AutoInitter.init = function(selector) { // Find and iterate over .auto-init class elements $(selector).find('.auto-init').each(function(){ var self = $(this).get(0); //Return the underlying dom element var key = ($(this).attr('id'))? $(this).attr('id') : 'auto-gen'; // Get the requirements to call or instantiate a function(s). // The variable "arguments" is reserved by javascript // so we have to use another name, I picked $arguments. // Note - there's nothing special about the dollar sign in javascript, it's just a letter. // both jQuery and Prototype use it as a function name, by iteself :) var functions = ($(this).attr('data-function'))? $(this).attr('data-function').split(',') : "[]"; var $arguments = ($(this).attr('data-arguments'))? $.parseJSON("[" + $(this).attr('data-arguments') + "]") : "[]"; var instantiates = ($(this).attr('data-instantiate'))? $(this).attr('data-instantiate').split(',') : "[]"; var instances = []; // Iterate over each function for the current HTML element. // Elements can invoke multiple functions (and respective arguments), comma delimited. $.each(functions,function(index,funcName) { var args = $arguments[index]; var instantiate = JSON.parse(instantiates[index]); //Convert truthy/falsey values to Boolean var instance = null; // Force an array for arguments if not already one if(!$.isArray(args)) { args = []; } // We have to locate the function by name using the window object // because you can't use a string literal to call the function in javascript. var funcRef = 'window["' + funcName.replace(/[.]/g,'"]["') + '"]'; // This is a time to use eval :) eval("var func = " + funcRef + ";"); // Make sure the function exists if(typeof func === 'function') { // Should we construct an object with the keyword "new" or just call the function if(instantiate) { instance = Lib.instantiateWithScope(func,args,{ primaryTarget : self }); } else { // Just calling a function here // I'm using a modified window context so I can call native functions // but user defined functions should work too. window.primaryTarget = self; func.apply(window, args); instance = func; } // Register it in our registry so we can help keep track of them. if(key) { $(self).data('instance', instances[0]); //Always point to first instance (for convenience) $(self).data('instances', instances); } } }); }); // Trigger an event saying that we are done insantiating all of our objects. Lib.AutoInitter.completedCount++; var event = jQuery.Event("autoInitsComplete"); event.completedCount = Lib.AutoInitter.completedCount; $(document).trigger(event); }; $(document).ready(function() { Lib.AutoInitter.init(this); }); |
This kinda of markup-invocation approach promotes an object oriented library design and keeps both JavaScript and HTML in their places. JavaScript code also executes only when needed (which includes binding events).
Even more, there is power gained with AJAX using the HTML response type because new functionality can be easily added (or removed) by responding with a new .auto-init element and respective data.
But I love JSON you might say!!!!? No problem, just pass your data through as an argument!
So, this means I can send an AJAX request to the server and replace the toggle’s content block with totally different markup and evolve the user experience on-the-fly, by invoking new functionality with markup provided by the server.
Here is an example (click the button multiple times):
The above code integrates seamlessly on wordpress, but you can view an isolated example here:
To see the PHP code you’ll have to download full sample here:
As you might gather, this markup-invocation approach becomes particular useful when chaining several HTML forms together. AJAX server-side error checking can easily replace most, or all of client side error checking which eliminates the need for redundant logic.
Things can be said about HTML degradability as well. While this idea is becoming increasingly dated, with this approach, you will find HTML degradability to be as natural as it is graceful.
Enjoy :)
Shaun Moen