Using Composed UI Controllers In The Context Of Javascript Inheritance

Yesterday, I blogged about using composed Javascript UI controllers to help manage the state and event handling of a single UI component. I liked where that was going; however, at the same time, I couldn't figure out how to work inheritance into the model. Since the root class constructor was responsible for initializing its composed elements, I couldn't figure out how to extend both the root class and the composed class while still being able to call the root class' super constructor. After tossing and turning in bed, going over and over the problem, what I finally decided to do was factor out the composed element initialization into a class method.

If you look at my original ListController constructor, it looks more or less like this:

// I am the list controller class.

function ListController( target ){

..... stuff .....

..... stuff .....

..... stuff .....

this.target.children().each(

function( index, listItemNode ){

new ListItemController( self, listItemNode );

}

);

}

Here, you can see that as part of the constructor logic, the ListController class is gathering up its children and then iterating over them, using each child to initialize an instance of the ListItemController class. The problem with this approach is that the composed class - ListItemController - is now part of the root class' constructor logic. As such, I would never be able to call this constructor without using the ListItemController class to define the composed elements.

The problem with that is that if I want to extend the ListController class, it is very likely that I will also want to extend the ListItemController class. But with the tight coupling of the root class to its composed class, getting a root sub-class to use a different composed class is just about impossible.

To remove this class coupling, I factored the composed element initialization out into its own class method:

// I am the list controller class.

function ListController( target ){

..... stuff .....

..... stuff .....

..... stuff .....

this.target.children().each(

function( index, listItemNode ){

// Factor-out composed element initialization.

self.initChildElement( listItemNode );

}

);

}

Now, rather than defining the composed class as part of the constructor, the composed class is defined in the initChildElement() method. This means that I can now easily extend both the root controller and the composed controller by overriding the initChildElement() method.

To demonstrate this Javascript class inheritance, I've take my previous demo and separated out the Click and Hover behaviors. Now, the base controller - ListController - only implements click behavior for activating list items. A new sub-class, HoverListController, then extends the ListController class to add the hover behavior on top of the root-class-provided click behavior. In the following demo, I am making use of both the ListController and the HoverListController to manage two separate UL elements on the page:

<!DOCTYPE html>

<html>

<head>

<title>Using Composed UI Controllers And Inheritance</title>

<style type="text/css">

ul {

height: 105px ;

list-style-type: none ;

margin: 0px 0px 20px 0px ;

padding: 0px 0px 0px 0px ;

}

li {

background-color: #F0F0F0 ;

border: 2px solid #CCCCCC ;

cursor: pointer ;

float: left ;

font-size: 18px ;

height: 100px ;

line-height: 100px ;

margin: 0px 10px 0px 0px ;

padding: 0px 0px 0px 0px ;

text-align: center ;

width: 125px ;

}

li.hover {

border-color: #CC0000 ;

}

li.active {

background-color: #FFF0F0 ;

border-color: #CC0000 ;

font-weight: bold ;

}

</style>

<script type="text/javascript" src="./jquery-1.4.2.js"></script>

</head>

<body>

<h1>

Using Composed UI Controllers And Inheritance

</h1>

<h2>

Click-Only List

</h2>

<!--

This UL will be controlled using our base ListController

class (that does not have any hover behavior).

--->

<ul id="girls">

<li>Sarah</li>

<li>Tricia</li>

<li>Katie</li>

<li>Jill</li>

</ul>

<h2>

Click And Hover List

</h2>

<!--

This UL will be controlled using our base HoverListController

class that adds hover behavior to the basic click behavior.

--->

<ul id="girlsHover">

<li>Sarah</li>

<li>Tricia</li>

<li>Katie</li>

<li>Jill</li>

</ul>

<!-- When the DOM is ready (ie. now), setup scripts. -->

<script type="text/javascript">

// I am the list controller class.

function ListController( target ){

var self = this;

// Store the target collection for our list controller.

this.target = $( target );

// Store this controller with the DOM node. This way,

// we can get the controller from DOM collections.

this.target.data( "controller", this );

// I flag whether or not the list is interactive - that

// is, whether or not it will respond to user input.

this.isInteractive = true;

// I am the currently active list item.

this.activeListItem = null;

// Create a controller for each of the child list items

// in this list.

this.target.children().each(

function( index, listItemNode ){

// Create the list item controller, but don't

// store it; since each controller is associated

// with a DOM node, we can offload the burden of

// aggregation to the DOM itself.

self.initChildElement( listItemNode );

}

);

}

// Define the class methods.

ListController.prototype = {

// I initialize the given child element, creating the

// appropriate child sub-class of Controller.

initChildElement: function( childNode ){

new ListItemController( this, childNode );

},

// I handle requests to make the given list item active.

makeListItemActive: function( listItem ){

// If the list is not interactive, ignore request.

if (!this.isInteractive){

return;

}

// Check to see if we have a currently active item.

if (this.activeListItem){

// Deactivate the currently active list item.

this.activeListItem.deactivate();

}

// Store the new active list item.

this.activeListItem = listItem;

// Activate the given list item.

listItem.activate();

}

};

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// I am the list item controller class.

function ListItemController( listController, target ){

// Store the parent controller.

this.listController = listController;

// Store the target collection for our list item.

this.target = $( target );

// Store this controller with the DOM node. This way,

// we can get the controller from DOM collections.

this.target.data( "controller", this );

// Set up click bindings. Proxy the context so that they

// execut in THIS controller context (not the DOM node).

this.target.click(

$.proxy( this, "handleClick" )

);

}

// Define the class methods.

ListItemController.prototype = {

// I activate this list item.

activate: function(){

this.target.addClass( "active" );

},

// I deactivate this list item.

deactivate: function(){

this.target.removeClass( "active" );

},

// I handle the mouse click event.

handleClick: function( event ){

// We want to make THIS list item active; but, that

// decision is not part of the local business logic.

// This needs to be passed up to the parent

// controller.

this.listController.makeListItemActive( this );

}

};

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// Create our list controller.

var listController = new ListController(

$( "#girls" )

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -- Hover List Controller Sub-Class --------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// I am the list controller class. I extend the base

// ListController class.

function HoverListController( target ){

// Call the super constructor.

this.super.call( this, target );

}

// Define the class methods.

HoverListController.prototype = {

// I initialize the given child element, creating the

// appropriate child sub-class of Controller.

initChildElement: function( childNode ){

new HoverListItemController( this, childNode );

},

// I handle requests to put the given list in hover mode.

makeListItemHover: function( listItem ){

// If the list is not interactive, ignore request.

if (!this.isInteractive){

return;

}

// If the list is not currently active, add the hover

// class to the target.

if (!listItem.isActive()){

// Tell the list item to go into hover mode.

listItem.hover();

}

},

// I handle requests to put the given list in normal mode.

makeListItemNormal: function( listItem ){

// If the list is not interactive, ignore request.

if (!this.isInteractive){

return;

}

// If the list is currently in hover state, put it

// back into the normal state.

if (listItem.isHover()){

// Tell the list item to return to normal mode.

listItem.unhover();

}

}

};

// Because the HoverListController inherits from the

// ListController, we have to extend the base class methods.

//

// NOTE: This logic can be baked right into the original

// prototype definition - but I am finding that to be

// ugly and adding extra indentation.

HoverListController.prototype = $.extend(

{

super: ListController

},

ListController.prototype,

HoverListController.prototype

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// I am the list item controller class. I extend the base

// ListItemController class.

function HoverListItemController( listController, target ){

// Call the super constructor.

this.super.call( this, listController, target );

// Set up the hover bindings. Proxy the context so that

// they execute in THIS controller context( not the DOM).

this.target.hover(

$.proxy( this, "handleMouseEnter" ),

$.proxy( this, "handleMouseLeave" )

);

}

// Define the class methods.

HoverListItemController.prototype = {

// I handle the mouse enter event.

handleMouseEnter: function(){

// We want to put THIS list item into the hover

// state; but, that decision is not part of the local

// business logic. This needs to be passed up to the

// parent controller.

this.listController.makeListItemHover( this );

},

// I handle the mouse leave event.

handleMouseLeave: function(){

// We want to take THIS list item out of the hover

// state; but, that decision is not part of the local

// business logic. This needs to be passed up to the

// parent controller.

this.listController.makeListItemNormal( this );

},

// I move the list item to the hover state.

hover: function(){

this.target.addClass( "hover" );

},

// I determine if the list item is active.

isActive: function(){

return( this.target.is( ".active" ) );

},

// I determine if the list item is in the hover state.

isHover: function(){

return( this.target.is( ".hover" ) );

},

// I move the list item out of the hover state.

unhover: function(){

this.target.removeClass( "hover" );

}

};

// Because the HoverListItemController inherits from the

// ListItemController, we have to extend the base class

// methods.

//

// NOTE: This logic can be baked right into the original

// prototype definition - but I am finding that to be

// ugly and adding extra indentation.

HoverListItemController.prototype = $.extend(

{

super: ListItemController

},

ListItemController.prototype,

HoverListItemController.prototype

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// Create our list controller.

var hoverListController = new HoverListController(

$( "#girlsHover" )

);

</script>

</body>

</html>

I know there's a lot of code to read through here, which is why I tried to use a good amount of horizontal commenting to separate the individual sections. Ultimately though, I have two sets of classes - my base classes:

ListController

ListItemController

... and my sub-classes:

HoverListController [extends ListController]

HoverListItemController [extends ListItemController]

Each of the sub-classes extends one of the root classes and augments the root class behavior to include hovering.

I'm not a great OOP programmer, so I am not sure if this is the best approach. I think that the locating of child elements could, itself, be factored out into a class methods. But, in any case, decoupling the root class constructor and the composed class definition now allows for both the root class and the composed class to be sub-classed (extended).