FLEX On jQuery: Decouple Components With Event Listeners
In my continuing effort to explore Adobe FLEX as a means to find applicable jQuery / Javascript "best practices", I had a short exchange with Joe Rinehart the other day on FaceBook. It was a very brief conversation, so I didn't have much to go on; but basically, it came down to keeping components loosely coupled through the means of some intermediary.
Ben Nadel:
Basically, I wanted to have a conversation about how the FLEX app would cancel a view state change based on the state of other elements. I am trying to learn theory of FLEX to see how it might apply to jQuery.
Example: I have a menu bar with several nav items on it. I am currently looking at one section (of the nav bar) that has a data grid that has inline editing. Assume the datagrid has a row currently being edited (for argument's sake). The user clicks on a different nav item in the menu bar. How would the app capture that state change in the menu bar and "Cancel" it due to the "currently editing" state of the datagrid?
Joe Rinehart:
I'd have an instance of what is known as a "presentation model" (models the state of the view tier in sort of "semantic terms") with a property called "allowNavigationChange." On the grid, a listener for the "edit starting" event would set allowNavigationChange to false. The click handler on the nav bar would then check the state of allowNavigationChange before switching the view.
Unfortunately, that was the extent of our quick discussion; I'm not completely clear on what Joe meant (as I'm never completely clear on anything I hear about FLEX). But, what he said definitely left me feeling inspired. A while back, I played around with creating UI elements with low coupling; at the time, I ended up altering properties on the target UI components (setting their "responsive" property to true or false) in reaction to announced "state changes." In a way, I think this is sort of what Joe was talking about - only, I was checking an internal property - "responsive" - not an external property - "allowNavigationChange."
Rather than trying this again with the same explicit-property-reference approach, I thought I might try it with custom events that can be cancelled by any subscribing event listeners. One of the most beautiful (and unexplored) features of jQuery (for me) is the ability to harness jQuery's powerful custom event creation and propagation framework. In the same way that we can bind-to and cancel default Anchor-click events in HTML, we can also bind-to and cancel the behavior of custom events.
For this exploration, I am going to have a navigational element with two links, each of which corresponds to a "view" element (for lack of a better term). Only one of the view elements will be visible at any one time; and, clicking on a navigational link will cause the appropriate view element to be shown:
If the user clicks on the view element itself, it will toggle the "state" of the view element between inactive and active. A view element in an active state will turn red:
When the view element is in the active state, it is meant to symbolize that something critical is happening within the view component. As such, we don't want the user to haphazardly navigate away from the current view without finalizing the view state. This is where our loosely coupled UI elements come into play - when a view component is considered active, we don't want the user to be able to click the links within the navigation component. And, if they do, we want to alert them that navigation is not currently possible:
This behavior will be enabled through the use of custom events triggered with jQuery. In this example, the navigation component has two events that it will announce:
navclick - This is the event that gets triggered when a user clicks on one of the links in the navigation component. The cool thing about this event is that it doesn't just mindlessly announce itself to any subscribed event listeners; rather, it keeps track of the actual jQuery event object in-use and checks to see if any of the event listeners requested that the event prevent its own default behavior (either by returning "false" or calling event.preventDefault()). If so - if the default behavior was indeed prevented - this click event will not kick off its intended work flow (ie. changing the state of the navigation component).
navchange - This is the event that gets triggered after a link within the navigation component is made active. If the user clicks on a link and the greater application does not cancel that click's default behavior, it will precipitate a state change, which will, in turn, trigger this navchange event.
Each of the view components has a single event for simplicity of this exploration:
statechange - This is the event that gets triggered when a user toggles the view component between an inactive and an active state.
Now that we see what kind of custom events we have to work with, how can we go about wiring up the appropriate business logic? We're going to fall back on Joe's idea of having some sort of property that defines whether or not we can execute a navigation change. Except, rather than, "allowNavigationChange," which the navigation component would need to know about explicitly, we're going to have a variable, "isViewActive," that only the core application "component" needs to know about.
When a user activates either of the view components, this isViewActive variable will become true. Then, when the application is alerted (via an event handler) that the navigation component has triggered a "navclick" event, the application will check the isViewActive value. If it is true, indicating that a view component is currently in a critical state, the application will prevent the default behavior of the "navclick" event, which will, in turn, prevent the state change of the navigation component.
<!DOCTYPE HTML>
<html>
<head>
<title>FLEX On jQuery: Loosely Couple Components With Event Listeners</title>
<style type="text/css">
#view-one,
#view-two {
cursor: pointer ;
height: 100px ;
padding: 15px 15px 15px 15px ;
width: 300px ;
}
div.inactive {
background-color: #F0F0F0 ;
border: 1px solid #CCCCCC ;
}
div.active {
background-color: #FFCCCC ;
border: 1px solid #F00000 ;
}
a.inactive {}
a.active {
font-weight: bold ;
}
</style>
<script type="text/javascript" src="../jquery-1.4.2.js"></script>
<script type="text/javascript">
// I am the controller for the nav component.
function NavController( target ){
var self = this;
// Store the target of the controller as this
// component's UI aspect.
this.ui = target;
// Bind to click events on the UI so we can monitor
// any attempts to change the state.
this.ui.click(
function( event ){
// Prevent any default action - there is no
// implicit navigation from any clicking.
event.preventDefault();
// Define the target variable.
var target = $( event.target ).closest( "a" );
// Check to see if the closest target for the
// click event is a nav item link.
if (target.size()){
// Blur the link.
target.blur();
// Create a new event to indicate a nav
// click event (this can be cancelled by
// any event listeners). We are creating
// an actual event object so we can keep
// track of its properties.
var navClickEvent = new $.Event(
"navclick"
);
// Trigger the event - this is our custom
// event type and custom event data
// indicating which nav element was clicked.
$( self ).trigger(
navClickEvent,
[{
navItem: target.attr( "rel" )
}]
);
// Check to see if the event was prevented
// by any of the event listeners.
if (!navClickEvent.isDefaultPrevented()){
// Nobody tried to stop the default
// event so execute the nav change.
self.setNav( target );
}
}
}
);
};
// Define the NavController class methods.
NavController.prototype = {
// I set the new nav item.
setNav: function( navItem ){
// Turn off all nav items.
this.ui.find( "a" ).removeClass( "active" );
// Turn on the given nav item.
navItem.addClass( "active" );
// Create a new event to indicate the change in
// navigational item.
$( this ).trigger(
"navchange",
[{
navItem: navItem.attr( "rel" )
}]
);
},
// I set the new nav item by index.
setNavByIndex: function( index ){
this.setNav(
this.ui.find( "a" ).eq( index )
);
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// I am the controller for the View Component.
function ViewController( target ){
var self = this;
// Store the target of the controller as this
// component's UI aspect.
this.ui = target;
// Bind to click events on the UI so we can monitor
// any attempts to change the state.
this.ui.click(
function( event ){
// Toggle the active state of view.
self.toggleActiveState();
}
);
};
// Define the ViewController class methods.
ViewController.prototype = {
// I hide the component.
hide: function(){
this.ui.hide();
},
// I return whether or not the current view is active.
isActive: function(){
return( this.ui.is( ".active" ) );
},
// I show the component.
show: function(){
this.ui.show();
},
// I toggle the active state.
toggleActiveState: function(){
// Toggle the active class on the UI.
this.ui.toggleClass( "inactive active" );
// Trigger state change.
$( this ).trigger(
"statechange",
[ this.isActive() ]
);
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// When the DOM is ready, initialize the scripts.
$(function(){
// Create the controllers.
var navController = new NavController( $( "#nav" ) );
var viewController1 = new ViewController( $( "#view-one" ) );
var viewController2 = new ViewController( $( "#view-two" ) );
// Turn on the first nav item.
navController.setNavByIndex( 0 );
// Hide the second view.
viewController2.hide();
// I am a flag to determine if the current view is
// active.
var isViewActive = false;
// -------------- //
// -------------- //
// Bind an event listener to both of the view
// controllers to see when their state changes.
$( [ viewController1, viewController2 ] ).bind(
"statechange",
function( event, isActive ){
// Store the state of the view.
isViewActive = isActive;
}
);
// Bind the nav click event on the navigation to see
// if a nav change can be made. The nav can only be
// changed if the current view is NOT active.
$( navController ).bind(
"navclick",
function( event, navData ){
// Check to see if the current view is active.
if (isViewActive){
// Alert the user that they cannot change
// the current nav.
alert( "You cannot navigate away!" );
// Since the current view is active, we
// need to prevent this event from carrying
// out its default action.
return( false );
}
}
);
// Bind the nav change event on the navigation to see
// how we might need to alter the display list to
// reflect the nav state.
$( navController ).bind(
"navchange",
function( event, navData ){
// Check to see which nav item was activated.
if (navData.navItem == "view-one"){
// Show the first view.
viewController1.show();
viewController2.hide();
} else {
// Show the second view.
viewController1.hide();
viewController2.show();
}
}
);
});
</script>
</head>
<body>
<h1>
FLEX On jQuery: Loosely Couple Components With Event Listeners
</h1>
<ul id="nav">
<li>
<a href="##" rel="view-one">View One</a>
</li>
<li>
<a href="##" rel="view-two">View Two</a>
</li>
</ul>
<div id="view-one" class="inactive">
This is view ONE. When RED, I am active.
</div>
<div id="view-two" class="inactive">
This is view TWO. When RED, I am active.
</div>
</body>
</html>
I know there's a lot of code here to absorb, but look at the click event binding in the NavController class. You'll see that if a navigation link was clicked, the NavController component triggers a custom "navclick" event. But, rather than simply triggering an event of type, "navclick," it actually uses the Event class constructor to create a new jQuery event object. It then can use this event object reference to determine if any of the subscribed event listeners cancelled the event's default behavior.
// Create a new event to indicate a nav
// click event (this can be cancelled by
// any event listeners). We are creating
// an actual event object so we can keep
// track of its properties.
var navClickEvent = new $.Event(
"navclick"
);
// Trigger the event - this is our custom
// event type and custom event data
// indicating which nav element was clicked.
$( self ).trigger(
navClickEvent,
[{
navItem: target.attr( "rel" )
}]
);
// Check to see if the event was prevented
// by any of the event listeners.
if (!navClickEvent.isDefaultPrevented()){
// Nobody tried to stop the default
// event so execute the nav change.
self.setNav( target );
}
If none of the event listeners did cancel the default behavior, this "navclick" event changes the state of the navigation element (which triggers a "navchange" event). Using this approach, the navigation component does not have to know anything about its parent application; it only has to agree with the parent application that one of its events can be programmatically cancelled.
By using event listeners to monitor the navigation component and the two view components, only the core application "component" needs to know about how the two sets of UI components should interact. Something about this feels really nice; by relying on events that can be cancelled by event listeners, we can create a way for components to communicate without any Dependency Injection (DI) or Inversion of Control (IoC) in which a component might be forced to explicitly refer to an outside variable. This is truly communication without coupling.
Want to use code from this post? Check out the license.
Reader Comments
This is an excellent example of loose coupling. I've been meaning to learn more about Flex and I think seeing how you've applied it to your example may be just what I need to push me over the edge and get out there and learn it.
@Tyson,
Sounds awesome. I know very little about FLEX itself, other than these few interactions I've had with FLEX developers. As you start to learn stuff, I'd to hear how it influences your Javascript development.
Whew, that's a lot of JavaScript code to maintain!
Although I like the direction you're going here (and have used similar techniques in the past), this seems excessively code-intensive to me.
As a general suggestion, I'd suggest trying out this idea on a few different projects, and then trying to create a re-usable library that encapsulates the commonalities (and maybe raises the level of abstraction).
Personally, I no longer have an appetite for writing code like this. That's because stuff like this (which takes dozens to hundreds of lines of code in JavaScript) is trivial to code in Flex. (With Flex data binding, sometimes I need less than one line of code!
Or, if I couldn't use Flex, I'd use GWT and let Google's crack compiler (Engineers) write that code for me.
YMMV -- Good luck!
@Banned,
When you write it in FLEX, it probably has hundreds, if not thousands, of lines of code running... it's just that someone else wrote most of it for you :) Not to say you don't reap tremendous benefits - just don't want to dimish the amount of code that each solutions requires.
Ideally, I would assume that, as you are saying, you can eventually get a Javascript library out of enough coding that becomes a reusable asset in the same vein that the FLEX framework is a reusable asset.
Right now, I'm just trying to learn to think in a different mind set. This kind of event-driven stuff is very new.
Actually, if you'd be up for it, I'd love to talk to you about FLEX. It sounds like you have some good thoughts on the matter.
Also, what is GWT?
You should probably compare to YUI's Attribute Provider concept:
http://developer.yahoo.com/yui/3/attribute/
I've had good results in the past using attribute providers as the basis for a client-side presentation model and mentally banning direct intermanipulation between scriptlets attached to controls. It's honestly the one aspect of YUI that I miss after jumping over to jQuery.
@Matt,
Thanks for the link. After a long hiatus, I am about to start getting back into R&D for more JavaScript-oriented applications. I think it's about time I really wrap my head around this stuff!
@Ben,
Enjoy the deep dive.
I loved what your Flex friend said about supporting a presentation model - classic bit of desktop GUI architecture that fell by the wayside when JS was in its adolescence. Too many client-side frameworks think that event registration between controls constitutes "decoupling".
Problem is, the controls still need to know a lot about each other for that to work, from the fact that the other controls exist down to the format of their input.
My favorite example is postal codes - there are a dozen reasonable ways to enter a postal code (direct text, picklist based on a city selection, from a map, etc) and many dozens of ways for other page elements to react to a change (local advertising, deals, locations, etc). Using a presentation model as the intermediary radically reduces the complexity of the script, as everything either mutates or reacts to the single canonical value.
YUI's got the component pieces of a good model, but is pretty clunky for simple cases. jQuery never got it. Dunno about Dojo or Prototype, but I suspect the former has to have some degree of support. I'm mostly interested in how HTML5's native data interactions make this all easier -- this week's R&D :)