Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2009) with:

Using Composed Javascript UI Controllers With DOM Augmentation

By Ben Nadel on

Over the past couple of days, I've been iterating through examples of using composed Javascript UI controllers in order to help manage user interface interactions. Now, in addition to simply altering the state of existing UI elements, I wanted to look into augmenting the DOM tree; that is, I wanted to explore the ability to both add and remove DOM nodes to and from the UI module being controlled by our Javascript classes.

 
 
 
 
 
 
 
 
 
 

Before I get into the add/remove features, I wanted to talk about the ListItemController class. In my previous demoes, I've coupled the ListController class to the ListItemController class; that is, I've made direct references to the ListItemController class from within the ListController class methods. With this tight coupling in place, I did make allowances for sub-classing by breaking out the direct class references into separate methods that could be easily extended. This time, however, I wanted to try something new.

After my conversation last week with Danilo Celic, I wanted to try passing in the ListItemController class definition into the ListController as part of the ListController instantiation. I don't like that this approach presupposes that the calling context understands the relationship between the ListController and the ListItemController; however, I do like that the relationship between the two classes is no longer hard-coded.

What might be nice, however, is to give this behavior only to the base class of list controllers. In this way, I could then have my sub-classes hard-code the association and only pass in the appropriate ListItemController class when calling the super constructor:

  • function MyListController( target ){
  • this.super.call( this, target, MyListItemController );
  • }

As you can see here, the base controller, as represented by the the "super" reference, still accepts a list item controller class as part of its instantiation; however, the sub-class, MyListController, is able to encapsulate the relationship between itself and its composed controller, MyListItemController. That would make me feel a bit more comfortable with the idea of using constructor injection.

But enough about class references, let's talk about our DOM augmentation. In this demo, I've added the ability to both add and remove list items to and from the list respectively. To add a list item, you can call the addListItem(name) method on the ListController. To remove a list item, you can double-click it.

When adding a list item, three things need to happen:

  1. A DOM node needs to be created.
  2. The DOM node needs to be injected into the list.
  3. A list item controller needs to be created for the new list item.

To create the new DOM node, I've created a static method on the ListItemController class, fromHTML(). The fromHTML() static method will take the raw HTML string representation of our LI and a data collection and merge the two into a jQuery collection containing the new, still detached, DOM node. This static method will be invoked by the ListController class as part of the "add" execution. I'm off-loading the actual DOM creation to the ListItemController class because it feels like a responsibility that the root controller shouldn't have to know about. As you'll see in the following demo, however, once the DOM node is created, the ListController takes care of injecting and initializing it.

Removing a list item will be done through event delegation in the same way that list item activation is done. The only difference is that once the list item is removed from the list, a teardown() method is called on the associated ListItemController. This teardown() method gives the ListItemController instance a chance to clean up after itself, breaking any references that might prove irksome for the browser's garbage collection routine.

Now that we've talked about the adding and removing changes, let's finally take a look at some code:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Using Composed UI Controllers With DOM Augmentation</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 10px 0px ;
  • padding: 0px 0px 0px 0px ;
  • text-align: center ;
  • width: 125px ;
  • }
  •  
  • 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 With DOM Augmentation
  • </h1>
  •  
  • <!--
  • This UL will be controlled by our list controller and its
  • composed list item controller.
  • --->
  • <ul id="girls">
  • <li>Sarah</li>
  • <li>Tricia</li>
  • <li>Katie</li>
  • <li>Jill</li>
  • </ul>
  •  
  • <!--
  • I am the template used to create new list items for the
  • girls list. This could get quite complex, but we're going
  • to keep it simple for now.
  • -->
  • <script id="girlsListItemTemplate" type="text/template">
  • <li>
  • <!-- Name will go here. -->
  • </li>
  • </script>
  •  
  •  
  •  
  • <!-- When the DOM is ready (ie. now), setup scripts. -->
  • <script type="text/javascript">
  •  
  • // I am the list controller class. I require a reference
  • // to the list we are going to controll, the list item class
  • // we want to compose, and a reference to the HTML template
  • // that will be used to create new list item DOM nodes.
  • function ListController(
  • target,
  • listItemControllerClass,
  • listItemTemplate
  • ){
  • // Store the target collection for our list controller.
  • // This is the UL we are controlling.
  • 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 );
  •  
  • // Store the list item controller class.
  • this.listItemControllerClass = listItemControllerClass;
  •  
  • // Store the list item template.
  • this.listItemTemplate = listItemTemplate;
  •  
  • // I am the currently active list item.
  • this.activeListItem = null;
  •  
  • // I gather up and prepare the composed controllers.
  • this.initComposedElements();
  • }
  •  
  • // Define the class methods.
  • ListController.prototype = {
  •  
  • // I add and configure a new list item.
  • addListItem: function( name ){
  • // Create a DOM node from the given data.
  • var listItem = this.listItemControllerClass.fromHTML(
  • this.listItemTemplate.html(),
  • {
  • name: name
  • }
  • );
  •  
  • // Create a list item based on the resultant list
  • // item DOM node.
  • this.initComposedElement( listItem );
  •  
  • // Now that we have created out list item controller,
  • // let's append the node to the list.
  • this.target.append( listItem );
  • },
  •  
  •  
  • // I initialize the given composed element, creating
  • // the appropriate child sub-class of Controller.
  • initComposedElement: function( listItem ){
  • new this.listItemControllerClass( this, listItem );
  • },
  •  
  •  
  • // I gather up and prepare the composed elements for
  • // further initialization.
  • initComposedElements: function(){
  • var self = this;
  •  
  • // For this UL, we're going to collect each direct
  • // child LI and create a Controller instance for it.
  • 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.initComposedElement(
  • $( listItemNode )
  • );
  •  
  • }
  • );
  •  
  • // Now that we have created the composed controllers,
  • // let's set up event delegation for the clicks. This
  • // way, we can handle the clicks with a single event
  • // binding and still delegate to the composed UI
  • // components.
  • this.target.delegate(
  • "li",
  • "click",
  • function( event ){
  • // Get the composed controller.
  • var controller = $.data( this,"controller" );
  •  
  • // Pass off click-handling to the LI.
  • return(
  • controller.handleClick( event )
  • );
  • }
  • );
  •  
  • // Bind to the list to delegate the double-click
  • // event.
  • this.target.delegate(
  • "li",
  • "dblclick",
  • function( event ){
  • // Get the composed controller.
  • var controller = $.data( this,"controller" );
  •  
  • // Pass off click-handling to the LI.
  • return(
  • controller.handleDblClick( event )
  • );
  • }
  • );
  • },
  •  
  •  
  • // I handle requests to make the given list item active.
  • makeListItemActive: function( listItem ){
  • // 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 remove the given list item
  • // from teh list.
  • removeListItem: function( listItem ){
  • // Check to see if the given list item is the active
  • // list item (most likely since our Click event will
  • // make the list item the active one).
  • if (this.activeListItem == listItem){
  •  
  • // Clear the active list item since we are about
  • // to remove it from the list.
  • this.activeListItem = null;
  •  
  • }
  •  
  • // Remove the list item from the list. We are
  • // delegating this reponsability to the list
  • // controller rather than the list item controller
  • // since the list item doesn't know the context of
  • // its target.
  • listItem.target.remove();
  •  
  • // Ask the list item controller to tear down - taking
  • // any necessary steps to prevent memory leaks.
  • listItem.teardown();
  • }
  •  
  • };
  •  
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  •  
  • // 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 );
  •  
  • // NOTE: We are NOT going to set up any CLICK handling.
  • // The click event will be caught at the root controller
  • // level and then delegated to the appropriate list item.
  • }
  •  
  • // 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 double-click event.
  • handleDblClick: function( event ){
  • // We want to remove this list item from the list;
  • // but, that decision is not part of the local
  • // business logic. THis needs to be passed up to
  • // the parent controller.
  • this.listController.removeListItem( this );
  • },
  •  
  •  
  • // I teardown the this controller, taking any necessary
  • // steps to help prevent memory leaks.
  • teardown: function(){
  • // Delete the dom node association.
  • this.target.removeData( "controller" );
  •  
  • // Delete the reference to the target.
  • this.target = null;
  • }
  •  
  • };
  •  
  •  
  • // Define the static methods. These are methods that can be
  • // called without having to instantiate an actual list item.
  •  
  •  
  • // I create a DOM node (in a jQuery collection) that
  • // represents a list item based on the given HTML and
  • // data collection.
  • ListItemController.fromHTML = function( html, data ){
  • // Create a DOM node and jQuery collection from the html.
  • var node = $( html );
  •  
  • // Populate the DOM node with the appropriate data.
  • node.text( data.name );
  •  
  • // Return the populated DOM node / jQuery collection.
  • return( node );
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create our list controller. Pass in the target list that
  • // we want to control, the list item class that we want to
  • // use for our list items, and the HTML template for the
  • // list items contained within this list.
  • var listController = new ListController(
  • $( "#girls" ),
  • ListItemController,
  • $( "#girlsListItemTemplate" )
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

This is definitely starting to be a good amount of code; but, I believe that creating this robust controller layer between the user and the DOM will make for a much more usable, much more maintainable client layer in the long run.

As I'm adding functionality to this collection of demos, I'm trying my best to keep the separation of concerns as clean as possible. I've tried to keep all "collection"-oriented tasks in the ListController class and all the item-oriented tasks in the ListItemController. While this forces a bit more communication between the two classes, especially with event delegation, I really like where this is going. And as always, feedback is always appreciated - this kind of programming is very experimental for me.




Reader Comments

Hey Ben,

First of all, thank you for the overwhelming quantity/quality of the articles on your blog. I don't think I had seen better Javascript examples with such extensive code documentation before.
I'm having lots of fun through your jQuery OOP series. I come from Actionscript 3.0, so finding some ways to better organize my jQuery code is very welcome.

I have a question regarding this example:
Why not using jQuery.proxy function in the delegate click events setup to call a handler on the main controller (clickHandler)? And then avoiding a back & forth communication between the composed element and the root? Is there any advantage for having a 'handleClick' method in the composed controller class? (scalability, maintainability,..)

  • initComposedElements: function(){
  • [...]
  • this.target.delegate(
  • "li",
  • "click",
  • $.proxy( this.clickHandler, this )
  • );
  • [...]
  • },
  • clickHandler: function( event ){
  • this.makeListItemActive( $(event.target).data( "controller" ) );
  • },

Reply to this Comment

@Melvyn,

I really really appreciate the kind words. I find all of this stuff immensely fascinating and I'm just trying to get to the bottom of it all :)

Coming from an ActionScript background, I'd actually love your input on this kind of stuff. I'm mostly new to OO in general (hence all the exploration), and this kind of complex interaction is not something I've come to grips with completely.

You can definitely do this with proxy and delegate (how crazy awesome are those functions, right!); the reason for a composed class approach is simple to create a bit more separation between concerns. Maybe this is not a separation that needs to be done. I just thought it would be nice to have a class that deals with item-based interactions and one that deals with collection-based interactions.

Maybe trying to create a "simpler" solution just makes things too complex :)

Also, maybe this example just isn't complex enough to merit it? What if a list had different types of items in which each list item had a common interface, but different implementations? Perhaps you would need to get into that kind of complexity before the composed class model would become of any real value.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.