Experimenting With Composed Javascript UI Controllers

In the past, when I've wanted to create complex UI interactions with a collection of elements, I would have created a single Javascript controller that handled the interaction logic for all of the elements. As my user interfaces have gotten more robust, however, this single point of processing has become somewhat unwieldy to program and maintain. As such, I wanted to start experimenting with using composed Javascript UI controllers in which there is a single high-level controller for the entire collection as well as a low-level controller for each item within the target collection.

To experiment with this composed controller architecture, I wanted to start with a simple unordered list (UL). As the user interacts with the list, a few things can happen:

If the user hovers over and out of an inactive list item, the list item enters and exits a "hover" state respectively.

If the user hovers over and out of an active list item, no action is taken.

If the user clicks on a list item, the current list item is activated. Any previously active list item is automatically deactivated.

If the list is flagged as being inactive, none of the user interactions will have any effect.

Based on these rules of interaction, it quickly becomes clear that neither the root controller nor any of the individual list item controllers know enough on their own to be able to handle events entirely by themselves. The root controller doesn't store the state of each list item controller; and, the list item controllers don't know enough about the greater list (interactivity, existing active items) to be able to handle state transition.

What this means is that as the user interacts with the list, there necessarily must be bi-directional communication between the root controller and its composed list item controllers. As the user interacts with the individual list items, the associated list item controller must defer action to the root controller where the business logic and bigger picture are maintained. The root controller can then get targeted list item controllers to perform the necessary actions.

In the following demo, try to follow the user interactions. Notice that as the mouse activity is captured at the list item level, the list item controller asks the root controller to perform an action, which, in turn, can trigger actions on zero or more of the composed list items.

<!DOCTYPE html>

<html>

<head>

<title>Using Composed UI Controllers</title>

<style type="text/css">

ul {

list-style-type: none ;

margin: 0px 0px 0px 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

</h1>

<!--

This is the UI component that will be composed within

out controller.

--->

<ul id="girls">

<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.

new ListItemController( self, listItemNode );

}

);

}

// Define the class methods.

ListController.prototype = {

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

}

}

};

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

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

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

// 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" )

);

// 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.

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 );

},

// 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" );

}

};

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

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

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

// Create our list controller.

var listController = new ListController( $( "ul" ) );

</script>

</body>

</html>

As you can see, the individual list item controllers don't carry out user interactions on their own; rather, they capture user interactions and then ask the root controller to act on them. At first, I was afraid that this was making my list item controller class very anemic; I was worried that the list item controller class should be more independent. But, then I realized that the list item controller needed to defer to the root controller because the root controller was the only place that knew enough about the environment at large to be able to control the UI's response.

As I was building this, the hardest part was trying to figure out what the list item controller could do on its own. At first, I thought it could do a lot independently; but, then as I added features like the ability to change the interactivity of the list and the ability to maintain an "active" item after click, I realized that the low-level list item controllers simply didn't have enough information to do things unmoderated. That's when I moved to more of a model where the list item controllers knew how to modify themselves, but not necessarily when to carry out those modifications.

Part of the reason that I always liked a single, root UI controller was because it both reduced the number of event handlers that needed to be bound and it made the augmenting of composed UI elements rather easy; since no event handling was done at the composed-element level, new items didn't have to be initialized in any way - they were plug-n-play. As my human-computer interactions become more robust, however, this single point of delegation is becoming unmanageable. Using both a root UI controller and composed item controllers seems to bring the situation back under control. This is, of course, at the cost of increased event binding and configuration. The benefits, however, might very quickly come to outweigh the cost.

I would love to hear about FLEX and how ActionScript developers handle this kind of stuff; you guys have years of UI-intense experience that, I am sure, would offer a tremendous amount of insight.

Reader Comments

Great article. I've been working on something very similar to this, and like the way you've structured the root controller. I have a question about this - how would you deal with the requirement of multiple root controllers? In other words, what if you needed to call<pre>var listController = new ListController( $( "ul" ) );</pre>for multiple elements? Let's say I have a page with 3 <div>s on it, each with the same class, and I want to do something like <pre>$('.div-class').each(function() { // what would you do here? // since you can't do var myController = new myController(this); // you would use an array to store the myController instances? // You create new "factory" object to handle the creation of the myController instances?});</pre>

I have many other questions about this, but will shut up now.

Thanks for everything you contribute to the community. I have learned a lot from you.

I think you are on the right track - you would iterate over the collection and create a new controller instance for each target element. That's kind of what I'm doing at the next layer down for my composed controllers; notice that when I create the list controller, I am creating a listItem controller for each LI in the list.

I assume this same logic could be taken up one layer and the same work flow could be implemented.

But then again, this is really just experimental for me. This starts to get into real OOP, which I am still just learning.

The one thing I keep thinking about is how to improve the issue of "increased event binding". This can most certainly be an issue as the list grows in size.

The possible method to alleviate the memory consumption would be to continue to use a delegate on the ListController and simply call the "handle" methods on the ListItem instances. In this case, there's _always_ a back-and-forth, but with the added benefit that you only ever have a single binding on the parent.

Does that make sense (without showing it w/ code)? What are your thoughts?

Yeah, that makes a lot of sense, and was also something I was rolling over in my head. Event binding can be processing-intense the more elements are on a page (or so I am told). I think you are right on the money: delegate to the root controller and have it call the "handle" methods on the appropriate child element.

Perhaps another fun idea to play around with.

@All,

I have refactored this demo to allow for easier sub-classing of both the root class and the composed classes: