Experimenting With A Stateful Class For Stateful User Interface (UI) Widgets
After going back and forth with Dan G. Switzer, II in the comments of my blog post on Finite State Machines and UI widgets, I wanted to try and factor out the "stateful" aspects of my code into some sort of reusable, lightweight framework. Furthermore, I wanted to create a way for states to have access to more static configuration; that is, rather than binding and unbind every event handler with every state change, I wanted some states to be able to permanently bind event handlers if possible. And, while this would cut down on processing overhead, I found that it was not so applicable to my particular experiment.
As a testing context for this refactoring, I wanted to create a button that could be either clicked or dragged. I thought this was a good demo since the mouse-up gesture would put the button into two completely different states depending on the user's previous actions: A mouse-up after a drag would simply stop the drag; a mouse-up after a mouse-down would alert a message for the user.
To make the code more reusable, I encapsulated the state management within a state machine class, Stateful(). The Stateful class has two public methods:
defineState( name, options )
gotoState( name )
Each state is defined by a set of options (object / hash). While no one option is required, the Stateful() instance will check for three different properties:
init() - This gets called once when the state is first defined. It provides the state with a one-time setup event in which the state can cache DOM elements and bind static event handlers.
setup() - This gets called every time the state machine gets put in the given state. This can be used to setup state-specific event bindings and DOM alterations.
teardown() - This gets called every time the state machine leaves the given state. This can be used to remove any state-specific event bindings and undo any DOM alterations.
This demo has five distinct states:
Default - The button is just siting there.
Hover - The user has moused into the button but has not performed any further action.
PreDrag - The user has depressed the mouse; at this point, it is unclear as to whether the user will start a drag motion or an activation motion.
Drag - The user has moved the mouse (in the PreDrag state). The button will now be moved in accordance with the mouse until the user releases the mouse button.
Activated - The user released the mouse button directly after pressing it. This alerts a message to the user. This state is transient and will move directly into the Default state after the message has been alerted.
When I was writing the code for this Stateful widget, I thought I would be able to statically bind more event handlers; that is, I thought I would be able to bind more event handlers once, regardless of the state of the UI widget. As it turns out, however, the complexity of the demo and the slow speed of mouse tracking (by the browser) required that I keep most event bindings state-specific. I did find out, however, that the init() method was a great place to cache DOM elements.
Anyway, let's take a look at the code:
<!DOCTYPE html>
<html>
<head>
<title>Stateful User Interface Widgets</title>
<style type="text/css">
/* DEFAULT state for button. */
div.draggable {
background-color: #F0F0F0 ;
border: 1px solid #999999 ;
border-radius: 5px 5px 5px 5px ;
cursor: pointer ;
font-size: 16px ;
left: 50px ;
padding: 10px 10px 10px 10px ;
position: absolute ;
top: 70px ;
}
/* HOVER state for button. */
div.draggableInHover {
background-color: #FFFFCC ;
border-color: #FFCC00 ;
}
/* PREDRAG state for button. */
div.draggableInPreDrag {
background-color: #FFFFCC ;
border-color: #FF9900 ;
}
/* DRAG state for button. */
div.draggableInDrag {
background-color: #FFFFCC ;
border-color: #FF9900 ;
}
/* ACTIVATED state for button. */
div.draggableInActivated {
background-color: #F0F0F0 ;
border-color: #999999 ;
}
</style>
</head>
<body>
<h1>
Stateful User Interface Widgets
</h1>
<!-- BEGIN: Draggable Widget. -->
<div class="draggable">
Drag or Click Me!
</div>
<!-- END: Draggable Widget. -->
<!-- Include JavaScript library. -->
<script type="text/javascript" src="./jquery-1.6.1.js"></script>
<script type="text/javascript">
// I am the stateful constructor.
function Stateful(){
// Create a hash of the available states, indexed by
// their state name.
this.states = {};
// Keep a reference to the current state.
this.currentState = null;
}
// Define the instance methods.
Stateful.prototype = {
// I define the given state (with the given name).
defineState: function( name, options ){
// Setup a blank DOM collection if non-existent.
options.dom = (options.dom || {});
// Add the state options to the state collection.
this.states[ name ] = options;
// Check to see if there is an init() method to be
// called on the state options.
if (options.init){
// Initialize the state instance.
options.init();
}
// Return the configured and appended state.
return( options );
},
// I goto the state with the specified name. Raises
// InvalidStateName if no matching state is found.
gotoState: function( name, setupArguments ){
// Make sure the given state exists.
if (!this.states.hasOwnProperty( name )){
// Invalid state for this state machine.
throw( new Error( "InvalidStateName" ) );
}
// Check to see if we have an existing state to
// teardown before configuring the new state.
if (
this.currentState &&
this.currentState.teardown
){
// Teardown the current state.
this.currentState.teardown();
}
// Store the current state.
this.currentState = this.states[ name ];
// Log the state name for debugging.
console.log( name );
// Check to see if the new state has a setup method
// for configuration.
if (this.currentState.setup){
// Configure the new state.
this.currentState.setup.apply(
this.currentState,
(setupArguments || [])
);
}
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// Create a sandbox for our button widget controller.
(function( $, button ){
// This widget is going to be a stateful widget with
// the following states possible:
//
// - Default: Button is just sitting there.
//
// - Hover: User has moused-over the button.
//
// - PreDrag: User has moused-down on the button but has
// not moved the mouse or released the mouse.
//
// - Drag: User has moved the mouse and the button will
// move with the mouse.
//
// - Activated: User has moused-up, causing an alert.
// Create a new Stateful instance to help define this
// widget's interactions.
var stateMachine = new Stateful();
// Define the Default state.
stateMachine.defineState(
"default",
{
// I initialize the state (only once).
init: function(){
// ...
},
// I configure the current state.
setup: function(){
// Listen for the mouse-enter event. We have
// to do this state-specifically since the
// browser can't track mouse movements that
// efficiently.
button.mouseenter(
function( event ){
// Goto the hover state.
stateMachine.gotoState( "hover" );
}
);
},
// I teardown the current state.
teardown: function(){
// Stop tracking the mouse enter.
button.unbind( "mouseenter" );
}
}
);
// Define the Hover state.
stateMachine.defineState(
"hover",
{
// I initialize the state (only once).
init: function(){
// Listen for the mouse-down event.
button.mousedown(
function( event ){
// Stop the default action in order
// to prevent text-selection.
event.preventDefault();
// Goto the PreDrag state. Since the
// PreDrag event involves a more
// complex mouse life-cycle, let's
// pass the mouse event onto the next
// state for setup.
stateMachine.gotoState(
"predrag",
[ event ]
);
}
);
},
// I configure the current state.
setup: function(){
// Change the class name.
button.addClass( "draggableInHover" );
// Listen for the mouse-leave event. We have
// to do this state-specifically since the
// browser tends to allow mouse-movements to
// be tracked less frequently.
button.mouseleave(
function( event ){
// Goto the Default state.
stateMachine.gotoState( "default" );
}
);
},
// I teardown the current state.
teardown: function(){
// Remove the class name.
button.removeClass( "draggableInHover" );
// Don't track the mouse leave.
button.unbind( "mouseleave" );
}
}
);
// Define the PreDrag state.
stateMachine.defineState(
"predrag",
{
// I initialize the state (only once).
init: function(){
// Store the document as the stage.
this.dom.stage = $( document );
},
// I configure the current state.
setup: function( mouseDownEvent ){
// Change the class name.
button.addClass( "draggableInPreDrag" );
// Listen for the mouse move.
this.dom.stage.mousemove(
function( event ){
// Goto the drag state. Since the drag
// state needs to be tracking mouse
// movements, let's pass along both
// event objects for the drag to use
// as part of the initial tracking.
stateMachine.gotoState(
"drag",
[ mouseDownEvent, event ]
);
}
);
// Listen for the mouse up event. If the
// user releases the mouse, they will not
// be dragging it - rather they will be
// activating it.
this.dom.stage.mouseup(
function( event ){
// Goto the activated state.
stateMachine.gotoState( "activated" );
}
);
},
// I teardown the current state.
teardown: function(){
// Remove the class name.
button.removeClass( "draggableInPreDrag" );
// Unbind the mouse move.
this.dom.stage.unbind( "mousemove" );
// Unbind the mouse up.
this.dom.stage.unbind( "mouseup" );
}
}
);
// Define the Drag state.
stateMachine.defineState(
"drag",
{
// I initialize the state (only once).
init: function(){
// Store the document as the stage.
this.dom.stage = $( document );
},
// I configure the current state.
setup: function( mouseDownEvent, mouseMoveEvent ){
var that = this;
// Change the class name.
button.addClass( "draggableInDrag" );
// As the user moves the mouse, we need to
// move the button. As such, let's store the
// initial X/Y of the mouse when the user
// first clicked.
this.initialPageX = mouseDownEvent.pageX;
this.initialPageY = mouseDownEvent.pageY;
// Since we want to keep the button under the
// mouse, we also need to determine the
// offset of the mouse in relationship to the
// button.
this.initialPosition = button.position();
// Get the internal offset of the mouse.
this.offsetX = (this.initialPageX - this.initialPosition.left);
this.offsetY = (this.initialPageY - this.initialPosition.top);
// Update the position of the button for this
// first mouse move.
button.offset({
left: (mouseMoveEvent.pageX - this.offsetX),
top: (mouseMoveEvent.pageY - this.offsetY)
});
// Now, let's buind to the mouse move event
// to continue updating the button position.
// For this, we want to bind to the document
// itself, rather than button, to make sure
// that we don't miss a move event.
this.dom.stage.mousemove(
function( event ){
// Update the button.
button.offset({
left: (event.pageX - that.offsetX),
top: (event.pageY - that.offsetY)
});
}
);
// Bind to the mouse-up event. When the user
// lifts their mouse, they no longer can drag
// the button without re-presseing.
this.dom.stage.mouseup(
function( event ){
// Return to the drag state.
stateMachine.gotoState( "hover" );
}
);
},
// I teardown the current state.
teardown: function(){
// Remove the class name.
button.removeClass( "draggableInDrag" );
// Stop tracking the mouse on the page.
this.dom.stage.unbind( "mousemove" );
// Unbind the mouse release.
this.dom.stage.unbind( "mouseup" );
}
}
);
// Define the Activated state.
stateMachine.defineState(
"activated",
{
// I initialize the state (only once).
init: function(){
// ...
},
// I configure the current state.
setup: function( mouseDownEvent ){
// Change the class name.
button.addClass( "draggableInActivated" );
// Alert activation message.
alert( "Ha ha, you touched me. Perv!" );
// Proceed back to the default state - the
// activated state is transient.
stateMachine.gotoState( "default" );
},
// I teardown the current state.
teardown: function(){
// Remove the class name.
button.removeClass( "draggableInActivated" );
}
}
);
// Start the widget in Default.
stateMachine.gotoState( "default" );
})( jQuery, jQuery( "div.draggable" ) );
</script>
</body>
</html>
There's not too much to explain here; mostly, this is just a refactoring of the ideas that I've covered previously in my last few posts on Finite State Machines. I did, however, enjoy the creation of a Stateful() class in order to abstract the state management. It felt cleaner and more cohesive. And, I liked that the complexity of user interactions and workflow in this particular demo seemed more appropriate to the obvious verbosity of a state machine approach.
Want to use code from this post? Check out the license.
Reader Comments
@Ben:
I do think this makes the creation of the states more manageable and easier to digest. However, there's a few things I'd change:
1) I think I'd change my possible state events to:
setup: Your current init() handler
begin: Your current setup() handler
end: Your current teardown() handler
teardown: Runs when you want to complete remove a state
Since I think you used setup/teardown terminology based upon jQuery's custom events, I think this make the meaning closer to jQuery's event handlers.
In my mind, "setup" occurs once at the initial creation and "teardown" occurs when the state would be completely removed. The "begin" and "end" for me are more intuitive in that they occur when a the state changes. You could also use enter/leave instead of begin/end.
2) I'd probably execute my state changes like this:
options.init.apply(this, [options]);
That way you can easily reference the options supplied to a state--for when you want to pass additional state values.
3) You might consider adding options to the Stateful() object itself. It seems like the odds are a single Stateful object would only refer to a single object your tracking, so it might make sense to store information about that object in a single object. You could then pass that State's options as an argument like in suggestion #2.
4) Also, if you wanted to be able to have something in multiple states, you could an option for each state which indicates whether or not it's state should be cancelled if the state changes. You'd obviously have to track multiple states, but it's definitely doable and something you can manage via your Stateful object.
5) Lastly, and admittedly more of a personal preference than anything, I'd leave off the "State" nomenclature from your methods. Since the object only deals with states to begin with--I think it's unnecessarily verbose.
To me this is perfectly and easily understandable:
stateMachine.define();
stateMachine.goto();
(Although I like "set" better than "goto" because it's more consistent with what you'd use in other objects and it's just as meaningful.)
@Dan,
Definitely, the term setup / teardown is from things that I have previous read or experimented with jQuery custom events. But, I wouldn't say that I applied it with any real discretion. I don't feel particularly attached to the naming and wouldn't be against changing any of it.
And, like you, I would probably just go with define() and goto(). I think I left State on for 1) fear that "define" and "goto" were some sort of reserved word (and, even though its ok as properties, my IDE sometimes gives me red-squiggles no matter what) and 2) it was a hold-over from when the methods were stand-alone and not part of a cohesive unit.
But, more than anything, I am stuck right now with the idea of storing additional options in the Stateful() instance itself. In my original vision, an object (ex. UI controller / delegate) would extend the Stateful() class to become "stateful."
But then, I ended up composing, rather than extending. So, there is a controller object which happens to have, as part of its internal properties, this state-management system.
So, on the one hand, I can see stateful-instance-options being nice; but on the other hand, I feel like maybe that should just be stored at the Controller level along with whatever controller-level properties would be necessary for proper functionality.
Right now, I'm trying to apply some of this to a non-trivial part of an existing project. This includes sandbox-based communication, as in this Script Junkie article:
http://msdn.microsoft.com/en-us/scriptjunkie/gg314983
Though, as I say that, I am realizing I haven't read that article in a LONG time... probably should go back and re-read it before I get too deep into this code.
Anyway, thanks for all the feedback!
Interesting rework,
Out of curiosity, how would youmcompare that model to something more generic like the dojo.stateful
Http://www.site pen.com/blog/2010/05/04/consistent-interaction-with-stateful-objects-in-dojo
Where you are listening to not events, but states - ie simple or complex values and doing the acts needed for "your part" based on them? ie draagable until dropped triggered by mouse over set to true
Guessing it would be easier to reuse draggable logic with a triggered listener as opposed to predefined state? Or would/could you just as easily configure the states to use predefined functions / libraries as custom code?
@Atleb,
To be honest, I don't know much about the other libraries and how they handle things like this. Already, I am seeing some problems with these ideas as I have applied them to the work I am currently doing. Some of these are simply architectural decisions that probably have little to do with the underlying concept; but, clearly, this stuff needs to fit together nicely.
I think it's time I start looking at some good Backbone.js exmaples to see how they deal with statefulness and inter-module communication.
@Ben,
looking at it (backbone) a bit myself for the fun and learning (at least initially)
nice blog post over here http://backbonefu.com/2011/08/front-end-developer-to-backbone-js-what-you-need-to-know/
on what the use case is (single page app w/o serious backend architecture), and links to specific tuts to dive deeper
and Cody Lindley share this on twitter;
Backbone.js Screencast - Introduction and Views http://t.co/WTWU53b
50min, havent seen yet - but the intro says;
"basic introduction on how to bootstrap a new Backbone.js application and go in-depth on how to use Backbone Views in particular"
@Atleb,
Thanks for the links. Still top on my list of things to do, but haven't looked into it yet. Right now, I'm loving RequireJS for dependency management. I think this will all go towards making something like Backbone more digestible.