Who Controls The Data When Modules Don't Know The Whole Story

Posted September 14, 2011 at 11:03 AM by Ben Nadel

Tags: Javascript / DHTML

Lately, I've been doing a lot of thinking about modules in JavaScript. As my client-side code has become much more complex than it used to be, I'm seeing that proper JavaScript application architecture is critically important. As I've tried to decouple my modules using encapsulated code and pub/sub (publish and subscribe) communication, however, I'm finding that I don't really know what I'm doing; this is especially true, when modules are displaying data that doesn't necessarily represent the complete, underlying story.


 
 
 

 
  
 
 
 

As I've moved into a more modular JavaScript world, I've tried to learn from really bright people like Addy Osmani (Large-Scale Application Architecture), Rebecca Murphey (Pubsub Screencast), and Nicholas Zakas (Managing JavaScript Objects); but, I'm not yet able to tie all of the concepts together in a nice coherent way. One place where I keep tripping up is the unquestioning faith in "clean data" that allows the modules to remain so decoupled from the application.

In so much of what I've been reading, it seems that the Application - the glue that brings all of the independent modules together - is a rather unintelligent layer. Its job appears to be little more than registering modules and facilitating communication. But what happens when the data that powers the modules needs to be moderated or filtered? Where does the intermediary logic live, and how does this affect inter-module communication?

To explore this kind of a scenario, I've put together a tiny demo that consists of three modules:

  • Stats - Message stats.
  • Form - UI for user to enter message.
  • List - List of recently entered messages.

The Form allows the user to enter a message. This message then gets cached locally in the application and the Stats and List modules are updated in turn. The twist here, however, is that only the most recent N messages will be displayed in the List. Furthermore, the Stats module displays two values: the number of messages posted and the number of messages displayed. This means that two of the modules - Stats and List - depend on data that is outside their point of view.

To accomodate this meta-data about the collection of messages, I've chosen to use some direct, explicit invocation instead of event-based, implicit invocation. Rather than having the List controller simply "listen" for new messages, it has to be told by the application which messages to display. Likewise, the Stats module can't simply listen for new messages as the values that it needs to show are dependent upon rules governed by an external entity; it, therefore, also needs to be told explicitly what values to use.

To allow for this moderated data, I'm using a combination of event-based communication and direct module-API method invocation:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>
  • Who Controls The Data When Modules Don't Know The Whole Story
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Who Controls The Data When Modules Don't Know The Whole Story
  • </h1>
  •  
  •  
  • <!-- This displays stats about the page. -->
  • <div class="stats">
  •  
  • <p class="displaying">
  • Displaying <span class="value">0</span> messages.
  • </p>
  •  
  • <p class="posted">
  • Posted <span class="value">0</span> messages.
  • </p>
  •  
  • </div>
  •  
  •  
  • <!-- This allows user interaction. -->
  • <form class="message">
  •  
  • <p>
  • <input type="text" name="message" value="" />
  • <input type="submit" value="Submit" />
  • </p>
  •  
  • </form>
  •  
  • <p>
  • Previous messages:
  • </p>
  •  
  • <!-- This displays messages submitted by the user. -->
  • <ul class="messages">
  • <!-- To be populated later. -->
  • </ul>
  •  
  •  
  • <!-- --------------------------------------------------- -->
  • <!-- --------------------------------------------------- -->
  •  
  •  
  • <script type="text/javascript" src="../jquery-1.6.3.js"></script>
  • <script id="app" type="text/javascript">
  •  
  •  
  • // NOTE: For this demo, we'll consider the "GLOBAL" scope to
  • // represent the "APP". I'm not creating it as a self-
  • // contained module so as to not take up more space.
  •  
  •  
  • // I am the messages that the user has submitted via the
  • // form interface.
  • var messages = [];
  •  
  • // I am the maximum number of messages to display on the
  • // screen (most recent messages).
  • var maxMessages = 3;
  •  
  • // I am the beacon to which events can be bound. This allows
  • // bi-directional communication between modules as well as
  • // between the "app" and the modules.
  • var sandbox = $( "script#app" );
  •  
  •  
  • // Bind the form submission event coming from the controller
  • // so that we can manage our messages.
  • sandbox.bind(
  • "message:sent",
  • function( event, message ){
  •  
  • // Add the message the top of our message queue.
  • messages.unshift( message );
  •  
  • // Update the message list using the top X of the
  • // message queue.
  • listController.setMessages(
  • messages.slice( 0, maxMessages )
  • );
  •  
  • // Update the stats.
  • statsController.setStats(
  • Math.min( messages.length, maxMessages ),
  • messages.length
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the stats controller.
  • var statsController = (function( sandbox, container ){
  •  
  • // Cache some DOM elements.
  • var dom = {};
  • dom.container = container;
  • dom.displaying = dom.container.find( "p.displaying span.value" );
  • dom.posted = dom.container.find( "p.posted span.value" );
  •  
  •  
  • // Return an API for updating the values.
  • return({
  •  
  • // I update the stats values.
  • setStats: function( displaying, posted ){
  •  
  • // Update the DOM values.
  • dom.displaying.text( displaying );
  • dom.posted.text( posted );
  •  
  • }
  •  
  • });
  •  
  • })( sandbox, $( "div.stats" ) );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the form controller.
  • var formController = (function( sandbox, container ){
  •  
  • // Cache some DOM elements.
  • var dom = {};
  • dom.container = container;
  • dom.message = dom.container.find( "input[ name = 'message' ]" );
  •  
  • // Bind to the submit action to make sure the form
  • // doesn't truly submit.
  • dom.container.submit(
  • function( event ){
  •  
  • // Cancel the submit event - we'll handle this
  • // on the client-side.
  • event.preventDefault();
  •  
  • // Check to see if there is a value entered (if
  • // not, then this is not a valid submission).
  • if (!dom.message.val().length){
  •  
  • // There is no message to submit.
  • return;
  •  
  • }
  •  
  • // Announce the message event.
  • sandbox.trigger(
  • "message:sent",
  • dom.message.val()
  • );
  •  
  • // Clear and refocus the form.
  • dom.message
  • .val( "" )
  • .focus()
  • ;
  •  
  • }
  • );
  •  
  •  
  • // Return the controller API.
  • return({
  • // ...
  • })
  •  
  • })( sandbox, $( "form.message" ) );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the message list controller.
  • var listController = (function( sandbox, container ){
  •  
  • // Cache some DOM elements.
  • var dom = {};
  • dom.container = container;
  •  
  • // I update the message list, re-drawing the list based
  • // on the given messages.
  • function updateList( messages ){
  •  
  • // Clear the message list.
  • dom.container.empty();
  •  
  • // Loop over each message to add it to the list.
  • for (var i = 0 ; i < messages.length ; i++){
  •  
  • // Create a new list item.
  • dom.container.append(
  • "<li>" + messages[ i ] + "</li>"
  • );
  •  
  • }
  •  
  • }
  •  
  •  
  • // Return a controller API that allows the messages to
  • // be repopulated.
  • return({
  •  
  • // I set the messages to be displayed.
  • setMessages: function( messages ){
  •  
  • // Update the message list.
  • updateList( messages );
  •  
  • }
  •  
  • })
  •  
  • })( sandbox, $( "ul.messages" ) );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, I'm using a combination of Pub/Sub and direct invocation. When a module needs to interact with the application, it can either trigger an event (as with the Form submission) or use other Sandbox methods (not shown in this demo). When the application needs to interact with a specific module, however, it relies on the module's API rather than using event-based invocation. This allows the application to filter / moderate / augment the data that is being piped into the target modules.

If I wanted to go back to a more strictly event-based architecture, I suppose that I could move some of the application logic into the individual modules. So, for example, instead of thinking of the messages list as a "List" module, perhaps I could think of it as a "Limited List" module. Then, when it gets initialized, it could be given a sense of the max number of items to show. In this way, it could simply subscribe to the "message" event and filter its own list internally.

I'm sure there are other ways to go about this; like I said before, this is all relatively new to me and I'm definitely struggling to tie the concepts together in a clean mental model. All I know right now is that as I've attempted to decouple my modules, I've found myself with an application that doesn't have a good sense of itself. Hopefully, moving some logic into the "Application" and relying on direct invocation over event-based communication (when sensible) will help me over some hurdles.




Reader Comments

Sep 14, 2011 at 12:44 PM // reply »
2 Comments

good article.

backbone.js solves all of this for me in a much more structured way - all the issues you describe are non issues for me with backbone.

have you looked into it?


Sep 14, 2011 at 2:08 PM // reply »
10,743 Comments

@Luke,

I've read up on Backbone and Spine a bit in the JavaScript Web Applications book by Alex McCaw. They definitely seem like compelling frameworks, which I will hopefully start digging into very soon. I've been trying to knock around these ideas on my own a bit so that I can try to think deeply about them.

I'm feeling good that it sounds like this kind of stuff has been solved very well already.


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 21, 2012 at 1:58 AM
Updated: Converting A ColdFusion Query To CSV Using QueryToCSV()
Hi Ben, why do you need to have so many double quotes when adding the field and field name to the row data? ----------------------------------------- <cfset LOCAL.RowData[ LOCAL.ColumnIndex ] = ... read »
AXL
May 21, 2012 at 1:24 AM
URL Rewriting And ColdFusion's WriteToBrowser Image Functionality (CFFileServlet)
@Mounir, Open your lower case URL Rewrite rule and add the following condition. Condition input: {REQUEST_URI} Check if input string: Does Not Match the Pattern Pattern: ^/CFFileServlet/_cf_ca ... read »
May 20, 2012 at 4:28 AM
Understanding The Complex And Circular Relationships Between Objects In JavaScript
@Will Vaughn I tried your javascript example but got this error:- foo.print is not a function ... read »
May 19, 2012 at 5:37 AM
A Graphical Explanation Of Javascript Closures In A jQuery Context
Thanks for this article, but I fear you missed an important point. If variables in the outer context change, these changes affect the inner anonymous functions as well. That means: if you change the ... read »
May 18, 2012 at 3:39 PM
Parsing CSV Data With An Input Stream And A Finite State Machine
Can you use file upload button with this? and read live? or does the file have to already be on the server saved? ... read »
May 18, 2012 at 1:06 AM
VIRGO (Aug. 23-Sept. 22): Dead On The Money!
A friend of mine and I were arguing about astrology and she told me that he believes in astrology. She hasn't provided me with any evidence that the belief makes any sense to me. She she been telling ... read »
May 17, 2012 at 11:32 PM
Using ColdFusion to Handle 404 Errors (Page Not Found) On Development Server
Very easy the configuration. I read a lot pages and I can't find the solution. I open the administrator and change this Administrator/server settings/Error Handlers/Missing Template Handler and p ... read »
May 17, 2012 at 3:13 PM
LOCAL Variables Scope Conflicts With ColdFusion Query of Queries
I never cease to be amazed that almost EVERY random CF issue I come across lands me on your site. Thank you for documenting your findings for the world. ... read »