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 »
11,314 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
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Jun 19, 2013 at 9:41 AM
Working With Inherited Collections In AngularJS
I actually just ran into this same situation with a demo I was putting together. Your implementation of multi-lvl $scope's > Mine :) ... read »
Jun 19, 2013 at 8:17 AM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
@Prateek, to match a word or text you should use .toContain('word') that's a jasmine reference. website is : http://pivotal.github.io/jasmine/ ... read »
Jun 19, 2013 at 8:10 AM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
Hi Guys, Actually i am doing e2e test of angular js of my project but i am not getting one thing that is how to press enter key through the test when my form is filled as i am not using a button but ... read »
Jun 18, 2013 at 9:20 PM
Mapping AngularJS Routes Onto URL Parameters And Client-Side Events
I couldn't find examples of passing multiple arguments using the when() routing statement so figured out through trial and error that you can pass multiple arguments using the following format: .whe ... read »
Jun 18, 2013 at 3:39 PM
Experimenting With The Amazon Simple Storage Service (S3) API Using ColdFusion
Hi Ben, THANKS! While not bleeding edge, it is new to me & I like learning new things every day! ... read »
Jun 18, 2013 at 12:30 PM
Disabling Auto-Correct And Auto-Capitalize Features On iPhone Inputs
Also spellcheck="false" should be mentioned as part of html5 specs ... read »
Jun 18, 2013 at 8:40 AM
Using Named Functions Within Self-Executing Function Blocks In Javascript
Hi Ben, you forgot to mention the most important thing for named self-executing functions - they can be referenced by name ONLY inside their execution context (which is parens in this case), it mean ... read »
dee
Jun 18, 2013 at 7:01 AM
My Safari Browser SQLite Database Hello World Example
hai ben, this program is really good i could understand the concept but i dint know how to save it and how to open it as you have done in the video can u give that details pls ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools