Introduction
Object-Oriented JavaScript development lacks one critical feature - the ability to subclass HTML elements.
This article presents a solution to the problem.
Using Jooshe
Load the Jooshe JavaScript library:
<script src="jooshe.js"></script>
or
<script src="jooshe.min.js"></script>
The src
attribute of the <script>
element must point to a copy of Jooshe.
A copy of Jooshe is provided in the download along with all the following test code.
Compatibility
All major browsers, IE 9+
Review of Object-Oriented JavaScript
We'll begin with a quick review of object oriented JavaScript. The most basic class in JavaScript is a function:
function myClass(){}
A new instance of a class is created with the new operator:
var myInstance = new myClass;
A class is extended by adding functionality to its prototype:
function myClass(){}
myClass.prototype.sayHi = function(){ console.log("Hi!"); };
var myInstance = new myClass;
myInstance.sayHi();
Inheritance
A sub-class inherits all the properties and methods of its parent class:
function mySubClass(){}
mySubClass.prototype = new myClass;
mySubClass.prototype.constructor = mySubClass;
mySubClass.prototype.sayHi = function(){ console.log("Hi there!");
mySubClass.prototype.sayBye = function(){ console.log("Goodbye."); };
Notes:
- All properties and methods of myClass are now inherited
- Considered good form, no real value
- Overriding an inherited function
- Extending the new class
Internals of Instantiation
It's important to understand that when JavaScript creates an instance of a class it does three things:
- Creates a copy of the prototype.
- Calls the class function using the prototype copy as the
this
argument. - Returns the prototype copy.
The following two class definitions are functionally equivalent:
function myClass(){}
function myClass(){ return this; }
But what happens if we return something other than this
in the class definition?
function myClass(){ return undefined; }
myClass.prototype.sayHi = function(){ console.log("Hi!"); }
console.log(new myClass);
There's no change when we return undefined. Let's try returning an object literal:
function myClass(){ return {a: "a", b: "b"}; }
myClass.prototype.sayHi = function(){ console.log("Hi!"); }
console.log(new myClass);
We've replaced the instance with the object literal!
This is critical for the method to work with subclassing HTML elements.
This works with any object type. For example, in the following code it works with the first class but not the second.
function myClass(){return new String("test");}
function myClass(){return "test";}
We've replaced the instance but we've lost all its functionality:
(new myClass).sayHi();
The method is gone because we've replaced this
with our object literal. We can fix the problem by wrapping this
onto our object:
function myClass(){
var i, me = {a: "a", b: "b"};
for(i in this) me[i] = this[i];
return me;
}
myClass.prototype.sayHi = function(){ console.log("Hi!"); };
(new myClass).sayHi();
Does instanceof still work properly?
console.log((new myClass) instanceof myClass);
No, instanceof is broken. Returning any object other than this
from the class function will break instanceof. Don't worry, there's a workaround for it... we'll get back to that later.
Now, let's return an HTML element instead of the object literal:
function myClass(){
var i, me = document.createElement("div");
for(i in this) me[i] = this[i];
return me;
}
myClass.prototype.sayHi = function(){ console.log("Hi!"); };
(new myClass).sayHi();
We've subclassed an HTML element!
Now, let's create a simple class that's a bit more useful:
function myClass(){
var i, me = document.createElement("input");
me.type = "text";
for(i in this) me[i] = this[i];
return me;
}
myClass.prototype.onkeyup = function(){ console.log(this.value); };
document.body.appendChild(new myClass).focus();
How do we use an event listener in place of the event handler?
function myClass(){
var me = document.createElement("input");
me.type = "text";
me.addEventListener("keyup", this.keyupHandler);
return me;
}
myClass.prototype.keyupHandler = function(){ console.log(this.value); };
document.body.appendChild(new myClass).focus();
That was just to show the functionality working. Now, let's expand on the concept:
function myClass(){
var i, me = document.createElement("input");
me.type = "text";
for(i in this.listeners) me.addEventListener(i, this.listeners[i]);
return me;
}
myClass.prototype.listeners = {
keydown: function(){ console.log("down", this.value); },
keyup: function(){ console.log("up", this.value); }
};
document.body.appendChild(new myClass).focus();
Now, let's expand further:
function myClass(){
var i, j, me = document.createElement("input");
me.type = "text";
for(i in this)
if(i == "listeners") for(j in this[i]) me.addEventListener(j, this[i][j]);
else me[i] = this[i];
return me;
}
myClass.prototype.onkeyup = function(){ console.log("handle up", this.value); };
myClass.prototype.listeners = {
keydown: function(){ console.log("listen down", this.value); },
keyup: function(){ console.log("listen up", this.value); }
};
document.body.appendChild(new myClass).focus();
Now, let's add some style!
function myClass(){
var i, j, me = document.createElement("input");
me.type = "text";
for(i in this)
if(i == "listeners") for(j in this[i]) me.addEventListener(j, this[i][j]);
else if(i == "style") for(j in this[i]) me[i][j] = this[i][j];
else me[i] = this[i];
return me;
}
myClass.prototype.onkeyup = function(){ console.log("handle up", this.value); };
myClass.prototype.listeners = {
keydown: function(){ console.log("listen down", this.value); },
keyup: function(){ console.log("listen up", this.value); }
};
myClass.prototype.style = { border: "2px solid black", borderRadius: "8px", padding: "4px" };
document.body.appendChild(new myClass).focus();
Now, let's decide what to do with any custom functions we might want to add to the class. We can easily add an attribute to an element using dot notation, e.g. myElement.newAttribute = value
. Since we need a strategy that will handle any attribute name, we have to consider the case of unintentional conflicts with existing attributes. The simple solution is to namespace the custom attributes, e.g. myElement.namespace.newAttribute
. It'll be helpful to keep the namespace short and memorable, so let's go with $
, i.e. myElement.$.newAttribute
:
function myClass(){
var i, j, me = document.createElement("input");
me.type = "text";
for(i in this)
if(i == "listeners") for(j in this[i]) me.addEventListener(j, this[i][j]);
else if(i == "style") for(j in this[i]) me[i][j] = this[i][j];
else if(i == "$") { me.$ = this.$; me.$.el = me; }
else me[i] = this[i];
return me;
}
myClass.prototype.onkeyup = function(){ this.$.logIt("handle up"); };
myClass.prototype.listeners = {
keydown: function(){ this.$.logIt("listen down"); },
keyup: function(){ this.$.logIt("listen up"); }
};
myClass.prototype.style = { border: "2px solid black", borderRadius: "8px", padding: "4px" };
myClass.prototype.$ = {
logIt: function(type){ console.log(type, this.el.value); }
};
document.body.appendChild(new myClass).focus();
One important note at this step is that the custom functions are scoped to the namespace. In other words, when you use this
within a custom function, it (as always) refers to the owner of the function, which is the $
object. The code me.$.el = me;
creates a reference back to the element. If you need to access the element from within a custom function, the syntax is this.el.attribute
.
Now that we've covered all the basic functionality of subclassing an HTML element, let's simplify things. We'll create a function called createClass
to do the grunt work for us along with a helper function called element
.
Jooshe Components
Jooshe consists of a namespace and two functions.
The namespace currently consists of a single helper function, but it may expand in the future.
Jooshe Namespace
var J$ = {
wrap:
function(o,p){
if(p) for(var i in p)
if(Object.prototype.toString.call(p[i]) == "[object Object]")
{ if(!(i in o)) o[i] = {}; J$.wrap(o[i],p[i]); }
else o[i] = p[i];
}
};
Jooshe createClass Function
function createClass(className,fn,o,p) {
fn = fn || function(){};
window[className] = fn;
var q = fn.prototype, w = J$.wrap;
if(p) w(q, o.prototype);
if(o) w(q, p || o);
if(!("$" in q)) q.$ = {};
q.$.__class__ = fn;
q.$.__parentClass__ = p ? o : null;
}
Usage
createClass("myClassName", fn, prototypeObject);
or
createClass("myClassName", fn, parentClass, prototypeObject);
When subclassing an HTML element, the fn
parameter must be a JavaScript function which returns a Jooshe element. If the fn
parameter is falsy, createClass
will use a generic empty function.
The createClass function does not return a value (well, technically it returns undefined).
Jooshe element Function
function element(tag, a, b, c, d) {
var i, j, k, me = document.createElement(tag), o = me.style,
f = function(a){ return Object.prototype.toString.call(a) == "[object Array]" ? a : [a]; },
w = J$.wrap;
o.boxSizing = "borderBox";
o.margin = 0;
if(tag == "button") o.whiteSpace = "nowrap";
else if(tag == "table") { me.cellPadding = 0; me.cellSpacing = 0; }
a = f(a);
for(i=0;i<a.length;i++) w(o,a[i]);
me.$ = {el: me};
b = f(b);
for(i=0;i<b.length;i++) if(b[i]) for(j in b[i])
if(j == "$") w(me.$, b[i].$);
else if(j == "listeners") for(k in b[i][j]) me.addEventListener(k, b[i][j][k]);
else if(j == "style") w(o, b[i][j]);
else me[j] = b[i][j];
c = f(c);
for(i=0;i<c.length;i++) w(me.$,c[i]);
d = f(d);
for(i=0;i<d.length;i++) w(me, d[i]);
return me;
}
Usage
var el = element("tagName" [, style-level [, 'this'-level [, $-level [, element-level ]]]]);
Each of the style-level
, 'this'-level
, $-level
, and element-level
parameters can be either an object or an array of objects. Arrays are allowed because I found myself needing to pass in more than one object for some of the more advanced classes I built for dbiScript.
The element function includes separate parameters for specifying style-level, this-level, $-level, and element-level attributes in order to provide flexibility and simplify the process of creating an element which is not based on a class. This type of element is useful for creating a child element to append to a class-based parent element, for example.
Example
Let's redo the earlier example using Jooshe:
createClass("myClass",
function(){ return element("input", null, this); },
{
onkeyup: function(){ this.$.logIt("handle up"); },
listeners: {
keydown: function(){ this.$.logIt("listen down"); },
keyup: function(){ this.$.logIt("listen up"); }
},
style: { border: "2px solid black", borderRadius: "8px", padding: "4px" },
type: "text",
$: { logIt: function(type){ console.log(type, this.el.value); } }
}
);
document.body.appendChild(new myClass).focus();
Alternatively, we could refactor the example to:
createClass("myClass",
function(){ return element("input", { border: "2px solid black", borderRadius: "8px", padding: "4px" }, this, null, {type: "text"}); },
{
onkeyup: function(){ this.$.logIt("handle up"); },
listeners: {
keydown: function(){ this.$.logIt("listen down"); },
keyup: function(){ this.$.logIt("listen up"); }
},
$: { logIt: function(type){ console.log(type, this.el.value); } }
}
);
document.body.appendChild(new myClass).focus();
When creating a Jooshe class, it's critical to pass this
as the second argument of the element function - don't forget!
The structure of the first example will work better if that class is subclassed - the child classes will inherit the style and type ("text") of the parent class. I find that I prefer the coding style of second example because I like to limit the prototype to functionality. Jooshe is flexible - use the format that works for you!
Before we get into inheritance, let's look at a few more examples. Let's add a custom property to our class and demonstrate its use:
createClass("myClass",
function(i){ return element("input", null, this, {index: i}); },
{
onkeyup: function(){ this.$.logIt("handle up"); },
listeners: {
keydown: function(){ this.$.logIt("listen down"); },
keyup: function(){ this.$.logIt("listen up"); }
},
style: { border: "2px solid black", borderRadius: "8px", padding: "4px" },
type: "text",
$: { logIt: function(type){ console.log(type, this.index, this.el.value); } }
}
);
for(var i=0;i<10;i++) document.body.appendChild(new myClass(i)).focus();
There are three changes from the previous example, all highlighted in bold text. We pass the index of the loop into the new instance and store it in a custom property. When a key is pressed in the input, we access the stored property and send it to the console.
Inheritance
Now, let's look at an example of subclassing Jooshe classes:
createClass("myClass", function(){ return element("input", null, this); },
{
style: { border: "2px solid black", borderRadius: "8px", padding: "4px" },
type: "text"
}
);
createClass("mySubClass", function(){ return element("input", null, this); }, myClass,
{
style: { background: "rgba(209,42,42,.1)", borderColor: "red" },
type: "text"
}
);
document.body.appendChild(new myClass);
document.body.appendChild(new mySubClass).focus();
To inherit from a parent class, you simply specify the parent class as the second parameter of Jooshe's createClass function, as illustrated in bold text in the example above.
In this example, the subclass retains the borderWidth (2px), borderStyle (solid), borderRadius (8px), and padding (4px) of its parent class while overriding the borderColor and adding a backgroundColor.
Selective Inheritance
It's possible to selectively inherit specific attributes of one or more parent classes:
createClass("momClass", null,
{ $: { x: function(){ console.log("I'm x"); } } }
);
createClass("dadClass", null,
{ $: { y: function(){ console.log("I'm y"); } } }
);
createClass("childClass", null,
{
$: {
x: momClass.prototype.$.x,
y: dadClass.prototype.$.y,
xy: function(){ console.log("I'm xy!"); }
}
}
);
var myChild = new childClass;
myChild.$.x();
myChild.$.y();
myChild.$.xy();
Classes vs. Elements
You may have noticed that it's not strictly necessary to use createClass; it's possible to do the same work using just the element function. My rule of thumb is to use a class whenever I want an element to have custom functionality. In other words, if your element needs any event handlers, event listeners, or other custom functionality then create a class for it. The browser is better able to optimize memory when these functions are stored in the class prototype (as opposed to inlining them in the element). It also makes for cleaner and more easily maintainable code. On the other hand, if your element does not need any custom functionality, then it's perfectly fine to just use the element function. I set the style-level attributes as first parameter of the element function due to the prevalence of this type of element in my development of dbiScript.
The instanceof Workaround
The workaround is quite simple:
createClass("myClass",function(){ return element("div", null, this); });
var o = new myClass;
console.log(o instanceof myClass);
console.log(o.$.__class__ == myClass);
While effective, the Jooshe __class__
property doesn't navigate the chain of inheritance the way instanceof does. Jooshe's __parentClass__
property can be used to navigate the chain of inheritance.
Footprint
The Jooshe source code has a tiny footprint. The minified version is 1058 bytes.
Web Application Development
CSS and Jooshe
When developing web applications:
- Encapsulate style in a Jooshe class instead of using CSS's selector-based approach of attaching style to elements.
In my Jooshe development, the only CSS-styling I still use is:
<style>
body, td, input, select, textarea {font: 10pt Verdana, Arial, Helvetica, sans-serif}
</style>
While that could also be moved into the 'CSS Reset' section of the Jooshe element function, leaving it in CSS makes it easier to change the font from one Jooshe application to the next.
Jooshe and CSS can be used together - keep in mind that Jooshe styling has the highest specificity (aside from !important).
jQuery and Jooshe
When developing web applications:
- Encapsulate functionality in a Jooshe class instead of using jQuery's selector-based approach of attaching functionality to elements.
- Ajax is critical for web application development - use jQuery.ajax().
Maintainability
I hope you'll find that Jooshe not only simplifies web application development, it also dramatically improves application maintainability by encapsulating structure, style and functionality into the Jooshe class.
FOUC
In addition to improved performance and maintainability, a Jooshe app is not susceptible to the dreaded FOUC.
Points of Interest
- I developed Jooshe several years ago to handle the dynamic requirements of dbiScript.
- dbiScript currently consists of 350 Jooshe classes.
- If you're curious as to how well a major Jooshe application performs, download dbiScript and see for yourself.
Jooshe on the Web
Help Get the Word Out
Agree that Jooshe will improve web application development?
- Add your vote, tweet, plus, and like to this page (buttons up top) :)