Experimenting With Composed Javascript UI Controllers

Posted October 12, 2010 at 11:00 AM by Ben Nadel

Tags: Javascript / DHTML

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

Oct 12, 2010 at 12:52 PM // reply »
1 Comments

Ben,

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.

Regards,

Chris


Oct 12, 2010 at 3:57 PM // reply »
10,743 Comments

@Chris,

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.


Oct 12, 2010 at 5:14 PM // reply »
1 Comments

Hey Ben,

Really interesting read!

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?


Oct 13, 2010 at 10:44 AM // reply »
10,743 Comments

@Frank,

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:

http://www.bennadel.com/blog/2033-Using-Composed-UI-Controllers-In-The-Context-Of-Javascript-Inheritance.htm

I had to factor out the selection of the composed-class type in order to be able to override it.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 16, 2012 at 8:18 PM
Best Of ColdFusion 10 Contest Entry - HTML Email Utility
Just found this, looks good! I'm trying to run it on local, it's the 64bit version and I'm experiencing horrible lag. On average the generate.cfm processes the content change in 60-90 seconds. I've ... read »
May 16, 2012 at 6:40 PM
Maintaining Sessions Across Multiple ColdFusion CFHttp Requests
I am trying to integrate this CFHTTPsession into an application that will log into zeekrewards.com to post ads and I am not having any luck. The code works perfectly for logging into other websites, ... read »
May 16, 2012 at 2:44 PM
Creating A Sometimes-Fixed-Position Element With jQuery
Thank you, very useful technique! Worked like a charm. ... read »
May 16, 2012 at 1:58 PM
Movies As A Religious Experience
Acting can, in a way, ruin the movie-goer's experience. I used to be able to get so caught up in movies and their plots, and totally engaged. But lately, I haven't been able to as much with a lot o ... read »
May 16, 2012 at 1:52 PM
The Science Of Optimal Post-Exercise Nutrition
children of this age eat very less vegetables so u can opt for salads they will like it also carrot ,cucumber,onion and as far as pulses are concerned u can boil them ,give him along with mashed rice ... read »
May 16, 2012 at 1:34 PM
Strange ColdFusion JRUN Stack Overflow Error
Hey, Recently I updated my jrun4 using the latest updater 7 and now i am having memory issues :(:(:( any help is appreciated ... read »
May 16, 2012 at 9:56 AM
ColdFusion 10 Beta, Apache Tomcat, And Symbolic Links On Mac OSX
Hi, Now that ColdFusion 10 is out I have stumbled over this as well and I cannot figure out the proper solution. We're running virtual hosts via Apache2; the ColdFusion-applications store their fil ... read »
May 15, 2012 at 6:03 PM
Movies As A Religious Experience
@Ben, I don't know whether you'd consider this a religious observation, but it seems to me, in a sense, movies multiply how many lives we get to have. Each movie is like a little extra life we get ... read »