Creating UI Elements With Low-Coupling And Conditional Event Handling
For the past couple of days, I've been building some very exciting internal tools for the company (specifically for prototyping). While these tools are straightforward at the server level, they're becoming quite complex at the User Interface level. At first, I was doing fine, slowly adding features; but now, the interactions between the various user interface components are becoming a bit too much for me to manage. As such, I thought I'd take some time to "white page" an idea and hopefully get your feedback on it.
The concept that I want to explore in this post is the creation of independent user interface elements that are not coupled to each other; but, despite this non-coupling, I still want each element to react, at least in part, based on the other's functional state. To demonstrate what I mean and why I want it, take a look at this video:
In this video, I have two independent UI elements, A and B. Each of these elements slides over to take up a majority of the visual space. They do this whenever they are clicked by the user, regardless of anything else that is going on. Here is the code behind this video:
<!DOCTYPE HTML>
<html>
<head>
<title>Related UI Elements With Conditional Events</title>
<style type="text/css">
#a {
background-color: #CCCC00 ;
border: 2px solid #999900 ;
cursor: pointer ;
font-size: 100px ;
height: 300px ;
line-height: 300px ;
padding: 0px 20px 0px 0px ;
position: fixed ;
text-align: right ;
top: 100px ;
width: 2000px ;
z-index: 100 ;
}
#b {
background-color: #FFCC00 ;
border: 2px solid #CC9900 ;
cursor: pointer ;
font-size: 100px ;
height: 300px ;
line-height: 300px ;
padding: 0px 0px 0px 20px ;
position: fixed ;
top: 100px ;
width: 2000px ;
z-index: 100 ;
}
</style>
<script type="text/javascript" src="../jquery-1.4.js"></script>
<script type="text/javascript">
// When the DOM is ready, initialize page.
jQuery(function( $ ){
// Get both elements.
var a = $( "#a" );
var b = $( "#b" );
var win = $( window );
// Store the startLeft and endLeft positions of the
// first element to be used in animation.
a.data({
startLeft: (-a.outerWidth() + 100),
endLeft: (win.width() - 110 - a.outerWidth())
});
// Store the startLeft and endLeft positions of the
// second element to be used in animation.
b.data({
startLeft: (win.width() - 100),
endLeft: 110
});
// Set the default positions.
a.css( "left", (a.data( "startLeft" ) + "px") );
b.css( "left", (b.data( "startLeft" ) + "px") );
// Bind the click event for A.
a.click(
function(){
// Stop any current animation.
a.stop();
// Get the current left of the element.
var currentLeft = parseInt( a.css( "left" ) );
// Check to see if A is in the start position
// to determine which direction to animate.
if (currentLeft == a.data( "startLeft" )){
// Animate to end position.
a.animate(
{
left: a.data( "endLeft" )
},
300
);
} else {
// Animate to start position.
a.animate(
{
left: a.data( "startLeft" )
},
300
);
}
}
);
// Bind the click event for B.
b.click(
function(){
// Stop any current animation.
b.stop();
// Get the current left of the element.
var currentLeft = parseInt( b.css( "left" ) );
// Check to see if B is in the start position
// to determine which direction to animate.
if (currentLeft == b.data( "startLeft" )){
// Animate to end position.
b.animate(
{
left: b.data( "endLeft" )
},
350
);
} else {
// Animate to start position.
b.animate(
{
left: b.data( "startLeft" )
},
350
);
}
}
);
});
</script>
</head>
<body>
<h1>
Related UI Elements With Conditional Events
</h1>
<a id="a">A</a>
<a id="b">B</a>
</body>
</html>
While these two elements are operating independently, I don't like that they might overlap. To prevent this from happening, I need the click event on each element to somehow be activated or deactivated based on the slide-state of the non-clicked element. An easy way to do this would be to use direct object references within the click event handlers of each object.
In this video, the two UI elements still slide back and forth when clicked; however, this time, only one UI element is allowed to slide at a time. Here is the code behind this video:
<!DOCTYPE HTML>
<html>
<head>
<title>Related UI Elements With Conditional Events</title>
<style type="text/css">
#a {
background-color: #CCCC00 ;
border: 2px solid #999900 ;
cursor: pointer ;
font-size: 100px ;
height: 300px ;
line-height: 300px ;
padding: 0px 20px 0px 0px ;
position: fixed ;
text-align: right ;
top: 100px ;
width: 2000px ;
z-index: 100 ;
}
#b {
background-color: #FFCC00 ;
border: 2px solid #CC9900 ;
cursor: pointer ;
font-size: 100px ;
height: 300px ;
line-height: 300px ;
padding: 0px 0px 0px 20px ;
position: fixed ;
top: 100px ;
width: 2000px ;
z-index: 100 ;
}
</style>
<script type="text/javascript" src="../jquery-1.4.js"></script>
<script type="text/javascript">
// When the DOM is ready, initialize page.
jQuery(function( $ ){
// Get both elements.
var a = $( "#a" );
var b = $( "#b" );
var win = $( window );
// Store the startLeft and endLeft positions of the
// first element to be used in animation.
a.data({
startLeft: (-a.outerWidth() + 100),
endLeft: (win.width() - 110 - a.outerWidth())
});
// Store the startLeft and endLeft positions of the
// second element to be used in animation.
b.data({
startLeft: (win.width() - 100),
endLeft: 110
});
// Set the default positions.
a.css( "left", (a.data( "startLeft" ) + "px") );
b.css( "left", (b.data( "startLeft" ) + "px") );
// ---------------------------------------------- //
// ---------------------------------------------- //
// This time, we want the UI elements (A and B) to not
// just fire any old time; we want them to fire only
// when the overall system is in a state that wants
// them to fire. As such, each element is going to get
// a new data point, "responsive".
//
// By default, both UI elements will be in the
// responsive state so they can respond to UI input.
a.data( "responsive", true );
b.data( "responsive", true );
// ---------------------------------------------- //
// ---------------------------------------------- //
// Bind the click event for A.
a.click(
function(){
// Check to see if the current element is in
// a response state. If not, then immediately
// dismiss the click.
if (!a.data( "responsive" )){
// Element is not responsive.
return;
}
// Stop any current animation.
a.stop();
// Get the current left of the element.
var currentLeft = parseInt( a.css( "left" ) );
// Check to see if A is in the start position
// to determine which direction to animate.
if (currentLeft == a.data( "startLeft" )){
// Animate to end position. Because we
// are about to put the "A" into an
// "activated" state, let's mark the
// other UI elements as inactive.
b.data( "responsive", false );
// Animate to end position.
a.animate(
{
left: a.data( "endLeft" )
},
300
);
} else {
// Animate back the to start position.
// Because we moving A back into a non-
// "activated" state, when the animation
// is complete, we'll re-activate B.
a.animate(
{
left: a.data( "startLeft" )
},
{
duration: 300,
// When this animation ends, put
// the other UI elements back
// into a responsive state.
complete: function(){
b.data( "responsive", true );
}
}
);
}
}
);
// Bind the click event for B.
b.click(
function(){
// Check to see if the current element is in
// a response state. If not, then immediately
// dismiss the click.
if (!b.data( "responsive" )){
// Element is not responsive.
return;
}
// Stop any current animation.
b.stop();
// Get the current left of the element.
var currentLeft = parseInt( b.css( "left" ) );
// Check to see if B is in the start position
// to determine which direction to animate.
if (currentLeft == b.data( "startLeft" )){
// Animate to end position. Because we
// are about to put the "B" into an
// "activated" state, let's mark the
// other UI elements as inactive.
a.data( "responsive", false );
// Animate to end position.
b.animate(
{
left: b.data( "endLeft" )
},
300
);
} else {
// Animate back the to start position.
// Because we moving B back into a non-
// "activated" state, when the animation
// is complete, we'll re-activate A.
b.animate(
{
left: b.data( "startLeft" )
},
{
duration: 300,
// When this animation ends, put
// the other UI elements back
// into a responsive state.
complete: function(){
a.data( "responsive", true );
}
}
);
}
}
);
});
</script>
</head>
<body>
<h1>
Related UI Elements With Conditional Events
</h1>
<a id="a">A</a>
<a id="b">B</a>
</body>
</html>
As you can see, each UI element is given the Boolean property, "responsive." The value of this property then directly affects the execution of the given element's click event handler such that if an element is flagged as not "responsive," the click event handler will immediately terminate. Then, as each element's click event handler executes, it flags the other element as not responsive (effectively disabling its click event). This methodology works in so much as these two elements now function somewhat in the context of one another; but, with addition of direct object references, the two elements have now become quite highly coupled.
While this might seem like a completely reasonable solution, keep in mind that this demo is a very simple, "white page" user interface; as soon as we move this into a working application and then try to build additional features on top of it, this high coupling is going to come back and bite us hard (yeaaaah baby). As such, let's see if we can accomplish the same contextual execution without requiring that the individual UI elements know about each other.
In this video, we have the same visual effect as before - both elements can slide over so long as the other one is not already in an expanded state. We also have the same "responsive" property that dictates whether or not a click event handler will be executed on the given element. This time however, neither UI element knows anything about the other one. In lieu of direct object references, we now have our objects "announcing" their "state" changes. To listen to these state changes, we've added another object - the Controller - which monitors the state of all elements and conditionally disables them as necessary.
Here is the code behind this video:
<!DOCTYPE HTML>
<html>
<head>
<title>Related UI Elements With Conditional Events</title>
<style type="text/css">
#a {
background-color: #CCCC00 ;
border: 2px solid #999900 ;
cursor: pointer ;
font-size: 100px ;
height: 300px ;
line-height: 300px ;
padding: 0px 20px 0px 0px ;
position: fixed ;
text-align: right ;
top: 100px ;
width: 2000px ;
z-index: 100 ;
}
#b {
background-color: #FFCC00 ;
border: 2px solid #CC9900 ;
cursor: pointer ;
font-size: 100px ;
height: 300px ;
line-height: 300px ;
padding: 0px 0px 0px 20px ;
position: fixed ;
top: 100px ;
width: 2000px ;
z-index: 100 ;
}
</style>
<script type="text/javascript" src="../jquery-1.4.js"></script>
<script type="text/javascript">
// When the DOM is ready, initialize page.
jQuery(function( $ ){
// Get both elements.
var a = $( "#a" );
var b = $( "#b" );
var win = $( window );
// Store the startLeft and endLeft positions of the
// first element to be used in animation.
a.data({
startLeft: (-a.outerWidth() + 100),
endLeft: (win.width() - 110 - a.outerWidth())
});
// Store the startLeft and endLeft positions of the
// second element to be used in animation.
b.data({
startLeft: (win.width() - 100),
endLeft: 110
});
// Set the default positions.
a.css( "left", (a.data( "startLeft" ) + "px") );
b.css( "left", (b.data( "startLeft" ) + "px") );
// ---------------------------------------------- //
// ---------------------------------------------- //
// This time, we want the UI elements (A and B) to not
// just fire any old time; we want them to fire only
// when the overall system is in a state that wants
// them to fire. As such, each element is going to get
// a new data point, "responsive".
//
// By default, both UI elements will be in the
// responsive state so they can respond to UI input.
a.data( "responsive", true );
b.data( "responsive", true );
// ---------------------------------------------- //
// ---------------------------------------------- //
// This time, rather than having A and B reference
// each other directly, we're going to have them
// announce their "state" change; This change will be
// monitoried by the following "Controller" that will
// respond to the state changes by altering the UI
// elements.
// Set the default states.
a.data( "state", "default" );
b.data( "state", "default" );
// ---------------------------------------------- //
// ---------------------------------------------- //
// This is the controller that will be listening for
// the state changes on the individual elements.
//
// This is a *really* tiny controller, which is, in
// reality, just a function for our demo.
var controller = function( event ){
// Get the target element.
var target = $( event.target );
// Check to see what the state of the current
// element is. If it is "Default" then we are
// going to "activate" all the elements.
if (target.data( "state" ) == "default"){
// Activate all elements - set them to be
// responsive to UI input.
a.data( "responsive", true );
b.data( "responsive", true );
// If the current target is not in a default
// state, then we have to deactivate all of the
// other elements.
} else {
// Check to see if the taret is A; if not,
// then deactivate A.
if (target[ 0 ] != a[ 0 ]){
a.data( "responsive", false );
}
// Check to see if the taret is B; if not,
// then deactivate B.
if (target[ 0 ] != b[ 0 ]){
b.data( "responsive", false );
}
}
};
// Bind the Controller to listen for state change on
// both the A and B elements.
a.bind( "statechange", controller );
b.bind( "statechange", controller );
// ---------------------------------------------- //
// ---------------------------------------------- //
// Bind the click event for A.
a.click(
function(){
// Check to see if the current element is in
// a response state. If not, then immediately
// dismiss the click.
if (!a.data( "responsive" )){
// Element is not responsive.
return;
}
// Since we are about to animate in some
// direction, whether it be to the start OR
// end position, we are about to enter a
// transition.
a.data( "state", "transition" );
// Announce the state change event.
a.trigger( "statechange" );
// Stop any current animation.
a.stop();
// Get the current left of the element.
var currentLeft = parseInt( a.css( "left" ) );
// Check to see if A is in the start position
// to determine which direction to animate.
if (currentLeft == a.data( "startLeft" )){
// Animate to end position.
a.animate(
{
left: a.data( "endLeft" )
},
{
duration: 300,
// This time, when the animation
// is done, change the state and
// announce the event.
complete: function(){
a.data( "state", "expanded" );
a.trigger( "statechange" );
}
}
);
} else {
// Animate back the to start position.
a.animate(
{
left: a.data( "startLeft" )
},
{
duration: 300,
// This time, when the animation
// is done, change the state and
// announce the event.
complete: function(){
a.data( "state", "default" );
a.trigger( "statechange" );
}
}
);
}
}
);
// Bind the click event for B.
b.click(
function(){
// Check to see if the current element is in
// a response state. If not, then immediately
// dismiss the click.
if (!b.data( "responsive" )){
// Element is not responsive.
return;
}
// Since we are about to animate in some
// direction, whether it be to the start OR
// end position, we are about to enter a
// transition.
b.data( "state", "transition" );
// Announce the state change event.
b.trigger( "statechange" );
// Stop any current animation.
b.stop();
// Get the current left of the element.
var currentLeft = parseInt( b.css( "left" ) );
// Check to see if B is in the start position
// to determine which direction to animate.
if (currentLeft == b.data( "startLeft" )){
// Animate to end position.
b.animate(
{
left: b.data( "endLeft" )
},
{
duration: 300,
// This time, when the animation
// is done, change the state and
// announce the event.
complete: function(){
b.data( "state", "expanded" );
b.trigger( "statechange" );
}
}
);
} else {
// Animate back the to start position.
b.animate(
{
left: b.data( "startLeft" )
},
{
duration: 300,
// This time, when the animation
// is done, change the state and
// announce the event.
complete: function(){
b.data( "state", "default" );
b.trigger( "statechange" );
}
}
);
}
}
);
});
</script>
</head>
<body>
<h1>
Related UI Elements With Conditional Events
</h1>
<a id="a">A</a>
<a id="b">B</a>
</body>
</html>
As you can see, in addition the previously added property, "responsive," the UI elements in this demo now have a new property called, "state". This property is meant to represent the functional condition of the element. Every time this value is changed, the element triggers a "statechange" event on itself; this will announce the state change to any object, such as the Controller, that has subscribed to events of this type. As these events are announced, the subscribing objects can then change the state of the interface as necessary. In our case, anytime one element leaves the "default" state, the Controller disables the other UI element.
This event-based communication creates a low amount of coupling between the UI elements, which is exactly what we wanted. But, that doesn't mean we're out of the woods just yet! All it means is that we have shifted the responsibility of view-state maintenance onto a different object - the Controller. The hope, however, is that this centralized Controller will make state management of the individual elements easier to maintain in an independent but cooperative way.
As I have started to build complex user interfaces using jQuery, my inexperience with event-driven applications is becoming a problem. Often times, I get the urge to put some serious effort into learning FLEX / Flash since they're probably the ultimate in event-driven architectures. My hope there, of course, would be that the understanding I gain in FLEX / Flash would be directly applicable to rich, jQuery-powered, event-driven web applications. Until then, and as always, I would really love to get some feedback on the concept explored in this post as it's a very new subject matter for me.
Want to use code from this post? Check out the license.
Reader Comments
Sounds like your interface needs Flex. Have you considered the benefits of a full fledged OOP language for your front end. I know this sounds like a smart ass comment, but I read your blog all the time and it seems like Flex would be a better fit. As some one who wants to learn Flex and Actionscript to build my front ends, and also enjoys reading your posts, I'd love to hear your thoughts.
@Ben
Great post! What you mentioned in your conclusion was what I had wanted to since I first learned Flex back in 2006. It was a much more productive environment since you had completed components whereas with HTML, you have to bring CSS and JS starting from scratch to create a component and its very time consuming and painful. For a yearI couldn't work in Flex right after learning it but I took everything I learned and applied it to HTML, CSS and JS and started creating various, yet small simple components. Much like what you have here.
There are several ways you can go about this. The first would be having that in a list but my guess is since these are positioned most likely other content will remain within the 2 links. The list would act as the container and controller thus having the logic and maintaining state between the 2 links.
The better approach is what you have done by storing data and dispatching custom events. When I first started making simple components like this back with Mootools and jQuery we didn't have that (gosh I feel old!) so we had to use classes or other misc attributes to store "data". A suitable way since you could have used a reusable class name that would group both links so a script would treat them as if they were one.. if one was already expanded and you wanted the other to expand now, the first one would collapse. But with the support for custom events makes a better fit especially if you want to eventually learn Flex! It's all event driven! Word! :)
Excellent post as usual Ben. I think that your use of the controller is the most appropriate way to decouple the individual objects. The controller will have to take on the role of a "global register" or "UI Manager", where each object that is added to the UI registers. Once they are registered, any time an event transition is fired the manager would check and update the states of all objects. This approach would have performance issues when many objects are added to the UI though because an internal reference to each object would be necessary, however this approach would allow each object to act independently of all others.
@Mark,
Javascript is an amazing language that is pretty much OO as well (but not quite as good with the inheritance of a traditional language). I am convinced that the theory behind the framework in FLEX should be the exact same as in the Javascript - the trick is just to understand what that theory is.
If you can tell me how FLEX is working (in a way that will add value), I would love to hear it, because it would be a fun challenge to translate it into Javascript.
@Javier,
This is the kind of stuff that I was, in a way, hassling your about at the last CFUG :) Getting two seemingly unrelated elements to act in an aligned way.
It sounds like you are down with the event-driven approach. I think this works well at this level; my concern (perhaps unfounded) is that as more UI elements are added, the logic in the controller will get very complicated very quickly. Of course, I may find that as I add things, it's much easier than anticipated.
@Donnie,
Thanks my man :) The application doesn't have too many UI elements, so I'm not too concerned about the reference count. It's just that the individual UI elements have a lot of states.
I think the biggest challenge might be coming up with meaningful state names and ways to announce things. Thankfully, jQuery makes a lot of the event delegation easy as apple pie.
Hi there Ben. I think the Controller approach is clearly the best choice, but, the way you handled things on the controller isn't quite the best, because even though "a" and "b" are complete strangers and unaware of each other, the Controller is not, the Controller remains highly coupled to "a" and "b", which isn't good and scalable in the future.
The Controller should have a data entry named "sliders" for instance, and then each slider you create, would register itself to the controller. Then inside the controller's code you'd check all the registered sliders and all the logic you need for them to coexist normally.
But, nonetheless, excellent article!
Then is pretty much the approach I've taken with the UI of my latest project. If you think about the UI as a hierarchy of containers, the basic rule is that any UI element can't have a reference to the container, but a container has a reference to it's direct children. This means elements will fire events to broadcast state changes, which the container, or sibling components can subscribe to.
A 'generic' reusable UI component (ie. a button, a list, etc.) generally won't listen for events, since this would couple the component to specific events. A component that is specific to the application (ie. UserList) will have handlers, it's acceptable for these to have a 'tighter' coupling to the application architecture.
I agree there is not need to use Flex for something like this, as javascript is a powerful language for event-based architectures. I like the YUI library, as it provides some OO functionality that fits with the way I think (the ability to extend or augment classes based on prototype and scope correction in event handlers has made my life much easier).
Here's a blog post by Christian Heilmann from YAHOO! about event-driven web applications. It's a bit old (2007) and focuses on using YUI, but still a good read on the subject http://yuiblog.com/blog/2007/01/17/event-plan/
@Ricardo,
I am not sure that there is anything wrong with the Controller being tightly coupled to the UI elements. I think, in order for the Controller to be able to "control" the view effectively, it definitely needs to know about all of its UI elements.
As far a "Sliders" instance, I think that actually makes too much of an assumption about the "likeness" of UI elements A and B. What happens when I add element C that comes on the screen diagonal? Would that be part of "sliders"; and, if it does, does it necessitate that its behavior impact A and B?
I think if we try to abstract it out too much, we'll actually start more problems... but again, this is all just theory for me at this point.
@Ryan,
Thanks for the link; I'll definitely be checking it out. And, as far as the FLEX vs. Javascript goes, I can't really see much technical difference. I think in general, anything that can be done in FLEX (outside of pixel-based manipulation) can also be done in Javascript.
@Ben,
if the C element doesn't necessitate that it's behavior impact on A and B, then it would not be added to the "Sliders", but to the another data item.
I'd design a controller that would join the elements that interact with each other in the same data item.
This way the controller could be as abstract as it needs to be, not decoupled, but not tightly coupled as well.
I don't think UI elements really need to "announce" themselves to the controller. This has already been done, in a way, by the DOM. Essentially giving each element congruent class name announces to the DOM that they are of that class. Your controller can then poll on members of that class, for example:
$('.class:not(:animated)').data('key', 'value')
But I suppose that's just coupling via the class attribute.
I understand that this is a simplified version, but I think it's actually MORE difficult to see what it is you're trying to accomplish this way (because your dealing with primarily DOM elements and not objects).
The goal from what I see is twofold. Create an application with objects that are decoupled from each other, yet react when an object event occurs.
You can do this with a publisher/subscriber method. Think of each of your UI elements as an object in a larger application object. The UI elements would subscribe to, and trigger events occurring in the application.
So an element event (eg click) might trigger a custom event (eg statechange) in the application, that some or all of the other UI elements would "subscribe" to.
When doing this with jQuery you might set up these custom events on a global object like the window, BODY, or your namespace object. When triggered, this would basically "broadcast" or fire whatever handlers were attached to that event. Obviously you can have multiple handlers, so each object could potentially have its own handler that fires when the custom event is triggered. A simplified/stubbed version of a subscribing object might look like:
( Don't know how this will format )
var myObj = {
obj: null,
Init: function() {
var self = this;
self.obj = $( "#myObject" );
var obj = self.obj;
// set up any object defaults
// Bind the click event for Object
obj.bind('click', self.handleClick );
// Bind the stateChange event for window
$(window).bind( 'stateChange', self.handleStateChange );
},
handleClick: function() {
// handle click event
// Announce the state change event.
$(window).trigger( "stateChange", { /* send any additional data */ } );
},
handleStateChange: function(e, data) {
// handle stateChange event
}
};
With this, each object is decoupled from other objects, and simply "listens" or reacts to events happening in the application.
Google some jQuery pub/sub plugins, or just publisher/subscriber to see how other people use the idea.
(I'm not even gonna talk about a finite state machine - blech! ack! yuck!)
@Ricardo,
I see what you are saying. I am not sure that I yet have enough UI elements to add an additional container in a way that it will add value; of course, should I need to add more UI elements, that might make it easier to scale. I don't know enough yet to say.
@Colin,
I don't think there is any need to decouple the Controller from the UI. In fact, I would say that almost goes against the intent of the UI; it is there to control the look / feel / behavior of the UI and the user experience. I would say that a high-coupling to the UI would be required for that to happen.
@Michael,
Working with objects is going to be easier in complex situations; however, I tried to keep this exploration as SIMPLE as possible because I am not very comfortable with the concept yet. In reality (when I applied this thought experiment to my actual software), each UI "component" has its own controller / Javascript file; furthermore, all the module controllers extend a base controller to factor out common features and properties.
These controllers even have custom bind / trigger methods; however, I went in the other direction - rather than having the UI components subscribe to the application, the application subscribes to state change events on the UI components.
I feel that this approach allows the UI components to be coded "in a box" where they don't have to worry about their environment. This way, it leaves all the "teamwork" between UI components up to the application / master controller.
@Ben Then maybe instead of a pub/sub pattern, what you're looking to use is event pooling?
http://bit.ly/7IzTRm
http://bit.ly/7OTnN8
Ah, I think I see where you were going with this (sorry if I was a bit slow).
You want to use an event manager (controller) to globally set (enable/disable) and potentially fire element/object event handlers.
I would think in the controller scenario, you'd want to register elements/objects with the event manager (controller) and make it more loosely coupled (or completely decoupled).
And I still don't know that I would bind/trigger the state change event on the element/object itself. It's 6-of-1, but it feels like the object says to the application, "I've changed, now you change!", and I would typically want it to read and feel like, "an event occurred in the application" and then handle it by having the application broadcast/update state to any registered elements/objects.
The more I think about it though, the "controller" ends up sounding like a finite state machine in my head, and I still don't wanna go there. hehe
@Michael,
Broadcasting by the application makes sense IF the individual UI elements know what they are listening for; but, I think the kind of situation I have is that one UI element doesn't necessarily know that it needs to respond to another UI's state.
For example, let's say I have 2 buttons on the screen. Let's say I also have a label that says "Heck Yeah". Now, let's say the label (heck yeah) starts out hidden. Then, when the user clicks one of the button (but not the other), the Controller shows the label.
In this case, the only correlation between the buttons and the label are contained within the UX logic of the Controller. The label cannot listen for a "buttonClicked" event because not every button will trigger the label show.
When the Controller contains exclusive knowledge as to how items can / must relate, I think it makes sense that events get funnels through it.
@Michael,
Also, I think it is sort of like a finite state machine, as is anything that has conditions to the way it response; but, I don't think you have to think about it like that.
Hi Ben!
I agree with Michael, your approach is a bit confusing for me.
In regards to labels and buttonClicked event. I'd say you have to broadcast message showLabel(Info/Description/etc) instead. ButtonClicked event is already tracked by DOM and you have to issue application specific messages, I guess...
Just my 2 cents
@Alex,
I don't think you'd want to broadcast a showLabel event. This couples the event to the handling control. What happens when you want more than one control to react to the event (show a label, disable another button, etc.). Events should announce what just happened, as opposed to what should happen next.
@Ryan
Well, I might be not expressed my thoughts correctly. What I've had in mind is showLabelClicked event. This way you can distinguish message sender, and message dispatcher will correctly send event to subscribers.
@Alex,
I see, showLabel was the name of the control clicked in this instance. Makes more sense now :)
@Ben,
This was sort of what I meant when I said it's hard to see if you're looking at it from the DOM perspective. I'm no expert at it either, but if you're going to use a 'controller' then you should go the extra step and look at it from the application (controller) perspective, and not the individual UI elements themselves (and I know how hard it is to change that thinking).
All events would/should be funneled through the controller. What you'd essentially be passing around (even with a UI element event) is the application's "state". The controller would then decide, based on its "state" what functions to perform, and even what elements of the application are active/inactive. the missing piece is to come up with the different states for the application, and what occurs in a particular state.
So in this case with the controller, the UI elements aren't listening, and the controller isn't broadcasting, but what's happening is you're firing events, and putting the application into different states which then affect the elements that are part of the UI.
For me, if you want to build an app from the UI elements perspective, the controller doesn't fit. You're also firing off 2 events/functions - the individual elements click handler, AND the controller (or application's event handler) - which could, in some scenarios, lead to a race condition. I don't think you want to have the elements broadcast their state to the controller, then have the controller broadcast state to the other elements in the UI. You either want the individual elements to broadcast their state and have the other elements listen for that, or you have a controller that tells all the UI elements what to do based on its state.
So clicking on a button is not the state, but an event that triggers a state. Maybe that state is "editingA", and the action/event that fires it is clicking on button A. So clicking on button A fires the controller method (with any relevant data), and based on that, the app goes into a particular state, and runs any applicable functions for that state. In this scenario, clicking button A animates A to open position, shows a label, and puts all other elements in an unresponsive state. That should all be handled by the controller (app), and the individual elements themselves shouldn't have any handlers attached to them other than alerting the controller to an event.
So now the app is in a state of "editingA", and clicking on it again would put the application back into its "default" state, where everything reverses.
You can handle the response of any of the individual elements in a number of ways. With the controller idea, you don't have to set any data() on the elements themselves, because the controller could/would decide what state and actions can be performed, and would route accordingly.
So all event handlers never do anything but:
$('#someElement').bind('click', [data], controller);
or something like
$('#someElement').bind('someState', [data], controller);
@Michael,
I am not sure I like the Controller having to handle every single interaction and then pass it off to the UI elements. This seems, in some way, like the wrong distribution of responsibilities. How can you possibly be able to build reusable UI elements if the Controller is always responsible for every single interaction?
I think the Controller should only handle the events that it subscribes to. This way, the UI elements can be build in black-boxes with a given API and set of events that it is *known* to broadcast. With such an API in place, the Controller can then act in response to UI elements and alter the UI as necessary.
>> I don't think you want to have the elements broadcast their state to the controller, then have the controller broadcast state to the other elements in the UI.
... I am not having the Controller broadcast any state in my demo. The Controller listens to state changes on UI elements; then, in reaction, it calls class methods on the UI elements as it sees fit. If you look in my last demo with the Controller, the UI elements never subscribe to the Controller.
If you are referring to these lines:
a.bind( "statechange", controller );
b.bind( "statechange", controller );
... you might just be confused with the syntax here. While the bind() method is being called on the A and B elements, it's actually the Controller that is the handler. It might look like A and B are some how binding to the Controller, but it's actually the other way around.
It's confusing because in my very simple demo, the Controller is a function, not an "Object" in the traditional sense.
once again, I learn a lot from you Ben !
I guess this website teach me more than jQuery's forum itself...
thank you thank you Ben
Hi Ben,
super blog. Your solution sounds a bit like an implementation of the mediator design pattern without classes to me. Here is an interesting link:
http://flexblog.faratasystems.com/2007/09/26/applying-the-mediator-design-pattern-in-flex
finty
@Muhammadiq,
Thanks :) That's awesome to hear to comparison - very flattering!
@Finty,
I'll have to give that a read.
@Ryan,
I finally got around to reading that YUI blog post you linked (it took me a fews months, but I did it). I wanted to ask you something (rather than on his only cause it is so old), but from what it seemed, he was subscribing to actual Event object, not event handlers? Am I crazy, or is that what was happening?
@Ben,
Yes, that's exactly what's happening. That's the approach YUI has taken with custom events, you subscribe the handler to the Event object itself. I really like the event system in YUI, it's one of the reasons I generally choose to use it over jQuery. A feature I like that's demonstrated in that post is scope correction. It's possible to define an object to take the 'this' scope in the handler, rather than the jQuery approach which is to make the DOM element have the 'this' scope. Obviously that doesn't work for custom events (since there isn't necessarily an element associated), and makes it easier to have an object's instance methods handle an event without having to rely on closures.
@Ryan,
It's definitely an interesting approach. I'm used to binding to objects / elements that the idea of binding to an event object is a bit much to wrap my head around just yet. As far as the THIS scope goes, jQuery 1.4 now has the ability to define the callback context - just an FYI.