Click here to Skip to main content
14,980,646 members
Articles / Programming Languages / Javascript
Article
Posted 19 Dec 2013

Tagged as

Stats

19.1K views
117 downloads
11 bookmarked

Jooshe - JavaScript Object-Oriented Subclassing of HTML Elements

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
19 Dec 2013MIT7 min read
Jooshe is juicy!

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:

JavaScript
<script src="jooshe.js"></script>

or

JavaScript
<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:

JavaScript
function myClass(){}

A new instance of a class is created with the new operator:

JavaScript
var myInstance = new myClass;

A class is extended by adding functionality to its prototype:

JavaScript
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:

JavaScript
function mySubClass(){}
mySubClass.prototype = new myClass;                                    // [1]
mySubClass.prototype.constructor = mySubClass;                         // [2]
mySubClass.prototype.sayHi = function(){ console.log("Hi there!");     // [3]
mySubClass.prototype.sayBye = function(){ console.log("Goodbye."); };  // [4]

Notes:

  1. All properties and methods of myClass are now inherited
  2. Considered good form, no real value
  3. Overriding an inherited function
  4. 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:

  1. Creates a copy of the prototype.
  2. Calls the class function using the prototype copy as the this argument.
  3. Returns the prototype copy.

The following two class definitions are functionally equivalent:

JavaScript
function myClass(){}
function myClass(){ return this; }

But what happens if we return something other than this in the class definition?

JavaScript
function myClass(){ return undefined; }
myClass.prototype.sayHi = function(){ console.log("Hi!"); }

console.log(new myClass);  // myClass {sayHi: function}

There's no change when we return undefined. Let's try returning an object literal:

JavaScript
function myClass(){ return {a: "a", b: "b"}; }
myClass.prototype.sayHi = function(){ console.log("Hi!"); }

console.log(new myClass);  // Object {a: "a", b: "b"}

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");}  // returns a String object
function myClass(){return "test";}              // returns a String literal

We've replaced the instance but we've lost all its functionality:

JavaScript
(new myClass).sayHi();  // Uncaught TypeError: Object #<myclass> has no method 'sayHi' </myclass>

The method is gone because we've replaced this with our object literal. We can fix the problem by wrapping this onto our object:

JavaScript
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();  // Hi!

Does instanceof still work properly?

JavaScript
console.log((new myClass) instanceof myClass);  // false

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:

JavaScript
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();  // Hi!

We've subclassed an HTML element!

Now, let's create a simple class that's a bit more useful:

JavaScript
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();  // type into the input and watch the console log

How do we use an event listener in place of the event handler?

JavaScript
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:

JavaScript
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:

JavaScript
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!

JavaScript
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:

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
createClass("myClassName", fn, prototypeObject);  // use when not inheriting from a parent class

or

JavaScript
createClass("myClassName", fn, parentClass, prototypeObject);  // inherits from a parent class

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

JavaScript
function element(tag, a, b, c, d) {

  // a - style-level, b - 'this'-level, c - $-level, d - elemet-level

  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;

  // we'll do some 'CSS Reset'-like stuff here...
  //
  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

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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();   // I'm x!
myChild.$.y();   // I'm y!
myChild.$.xy();  // I'm 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:

JavaScript
createClass("myClass",function(){ return element("div", null, this); });
var o = new myClass;
console.log(o instanceof myClass);      // false - instanceof is broken
console.log(o.$.__class__ == myClass);  // true - effective workaround!

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:

JavaScript
<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) :)

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Brien Givens
Engineer Comprehend Systems
United States United States
I've been fiddling around with computers since my parents bought me a Commodore VIC-20 for Christmas in 1981.

I received a B.Sc. in Mechanical Engineering from the University of Waterloo, but have spent my career in software development.

I focused on FoxPro during my early career before switching over to .NET and JavaScript.

I created Jooshe and wrote a Code Project article on it.

I wrote the front-end (Jooshe & JavaScript) and middleware (.NET) of dbiScript.


Comments and Discussions

 
QuestionJooshe Pin
secretcode729-Dec-13 13:42
Membersecretcode729-Dec-13 13:42 
QuestionVery interesting article about the subclass HTML elements in JavaScript Pin
Volynsky Alex20-Dec-13 1:07
professionalVolynsky Alex20-Dec-13 1:07 
AnswerRe: Very interesting article about the subclass HTML elements in JavaScript Pin
Brien Givens20-Dec-13 1:39
MemberBrien Givens20-Dec-13 1:39 
GeneralRe: Very interesting article about the subclass HTML elements in JavaScript Pin
Volynsky Alex20-Dec-13 1:41
professionalVolynsky Alex20-Dec-13 1:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.