Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Bernardo Sana
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Bernardo Sana

Treating Complex User Interface (UI) Widgets Like Finite State Machines

By
Published in Comments (9)

Yesterday, I looked at treating user interface (UI) widgets like Finite State Machines. In a simple widget, this is rather straightforward as the state of the widget easily defines the morphology of the user interface. In a more complex user interface widget, however, the relationship between the states of the widget and the morphology of its UI is not quite so "finite." Take, for example, a Menu Bar. Just because a menu bar is in an "active" state, it doesn't mean we know which menu is actually open. Sure, we could create a state for every single menu; but this instinctively feels less than optimal. So, how do we fit complex user interface widgets into a state machine model? Perhaps all we need to do is pass along some additional information as we transition from state to state (like the additional data we pass in an Event object).

In this exploration of user interface (UI) widgets and state machines, we'll be looking at a menu bar. This menu bar has two menus in it. As you mouse over the root of each menu, its menu items appear (closing any other unrelated menu items if necessary). If you mouse out of a menu, there is a pause before the menu items close, so as to give the user a little bit of wiggle room.

The states in this widget are as follows:

  • Default - All menu items are closed.

  • Active - One of the menus in the menu bar is active, displaying its menu items.

  • Pre-Close - The user has moused out of an active menu. This state waits one second (1000 milliseconds) before closing the related menu items so as to give the user a chance to mouse back into the menu.

Notice that the Pre-Close state actually relies on the previous state. That is, the Pre-Close state doesn't act on all menus - it only acts on the menu that was previously "Active." As such, we need a way to persist this menu across states.

Rather than storing an "active" menu reference as part of the widget property collection, I thought it would be cleaner to just pass the menu reference from state to state as part of the state transition. Remember the gotoState() method from yesterday's post:

function gotoState( newState ){ .. }

For this exploration, I've augmented it to allow additional arguments to be passed-through, along with the state:

function gotoState( newState /* [, data ] */ ){ .. }

The gotoState() transitional method now accepts N arguments. These arguments will be passed through to the setup() method of the new state. This way, we don't have to store state-related items outside of the actual state configurations.

To persist these arguments between the setup() and teardown() method calls of a single state, I've also added a "dom" property to each state. This allows the individual states to store a collection of DOM (Document Object Model) elements that are relevant to the current configuration. I think this keeps the states as clean and cohesive as possible.

Ok, let's take a look at the code. As you'll see below, this exploration of state machines is not all that much different. However, since the states are more complex and less "finite", the CSS (Cascading Style Sheets) does not play as big a role in defining the actual states of the widget; rather, with this level complexity, CSS is simply being applied as necessary to sub-trees of the widget's DOM (ie. the menu bar doesn't have an "Active" CSS class, but menus within the menu bar do).

<!DOCTYPE html>
<html>
<head>
	<title>Treating Complex UI Widgets Like State Machines</title>

	<style type="text/css">

		ol.menuBar {
			border: 1px solid #999999 ;
			border-left-width: 0px ;
			height: 30px ;
			list-style-type: none ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 0px 0px 0px ;
			width: 400px ;
			}

		ol.menuBar > li.menu {
			float: left ;
			height: 30px ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 0px 0px 0px ;
			position: relative ;
			width: 200px ;
			}

		ol.menuBar a.header {
			background-color: #F0F0F0 ;
			border-left: 1px solid #999999 ;
			color: #333333 ;
			display: block ;
			height: 30px ;
			line-height: 30px ;
			padding: 0px 10px 0px 10px ;
			width: 179px ;
			}

		ol.menuBar ol.items {
			border: 1px solid #999999 ;
			border-width: 0px 1px 1px 1px ;
			display: none ;
			left: 0px ;
			list-style-type: none ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 0px 0px 0px ;
			position: absolute ;
			top: 30px ;
			width: 199px ;
			}

		ol.menuBar li.item {
			border-top: 1px solid #999999 ;
			cursor: pointer ;
			height: 30px ;
			line-height: 30px ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 10px 0px 10px ;
			}


		/* ACTIVE state for MENU. */

		ol.menuBar > li.menuInActive > a.header {
			border-bottom-width: 0px ;
			}

		ol.menuBar > li.menuInActive > ol.items {
			display: block ;
			}

		ol.menuBar > li.menuInActive li.item:hover {
			background-color: #F0F0F0 ;
			}

		/* PRECLOSE state for MENU. */

		ol.menuBar > li.menuInPreClose > a.header {
			border-bottom-width: 0px ;
			}

		ol.menuBar > li.menuInPreClose > ol.items {
			display: block ;
			}


	</style>
</head>
<body>

	<h1>
		Treating Complex UI Widgets Like State Machines
	</h1>


	<!-- BEGIN: Menu Bar Widget. -->
	<ol class="menuBar">

		<li class="menu">

			<a href="#" class="header">
				Foods
			</a>

			<ol class="items">
				<li class="item">
					Cuban Pork Chops
				</li>
				<li class="item">
					General Tso's Chicken
				</li>
				<li class="item">
					Buffalo Wings
				</li>
			</ol>

		</li>

		<li class="menu">

			<a href="#" class="header">
				Movies
			</a>

			<ol class="items">
				<li class="item">
					Better Than Chocolate
				</li>
				<li class="item">
					Terminator 2
				</li>
				<li class="item">
					The Family Man
				</li>
			</ol>

		</li>

	</ol>
	<!-- END: Menu Bar Widget. -->


	<!-- Include JavaScript library. -->
	<script type="text/javascript" src="./jquery-1.6.1.js"></script>
	<script type="text/javascript">

		// Create a sandbox for our menu bar widget controller.
		(function( $, menuBar ){


			// Cache DOM references for later use.
			var dom = {};
			dom.menuBar = menuBar;


			// This is the current state of the widget. Once the
			// states are defined, this will be further set. For this
			// widget, we have three defined states:
			//
			// - Default
			// - Active
			// - PreClose
			var currentState = null;


			// I fascilitate the transition from the current to
			// the target state.
			var gotoState = function( newState /* [, data ] */ ){

				// Check to see if the current state is available
				// and has a teardown method:
				if (
					currentState &&
					currentState.teardown
					){

					// Teardown the old state.
					currentState.teardown();

				}

				// Check to see if the new state has a setup method.
				if (newState.setup){

					// The new state transition may have data
					// being passed to it. As such, let's pop off
					// the first argument (the state) in order to
					// get the setup() argument list.
					var setupArguments = Array.prototype.slice.call(
						arguments,
						1
					);

					// Setup the new state with the arguments (if
					// any) that were passed to this transition.
					newState.setup.apply( newState, setupArguments );

				}

				// Store the new state.
				currentState = newState;

			};


			// Define the states for this widget. Each state is going
			// to have a setup and teardown state.


			// ---------------------------------------------- //
			// ---------------------------------------------- //


			var inDefault = {

				// I am the description of the state.
				description: "I am the state in which all of the menu items are hidden. Only the header of each menu is shown in the menu bar.",


				// I am the DOM elements that may be necessary for
				// the setup and teardown of this state.
				dom: {},


				// I setup the current state.
				setup: function(){

					// Add a mouse-enter event for the menu headers.
					// When the user mouses into the header, we need
					// to show the relevant menu items.
					dom.menuBar.delegate(
						"li.menu",
						"mouseenter",
						function( event ){

							// Go to the active state. Since this
							// menu bar has more than one menu in it,
							// we need to pass the relevant menu on
							// to the next state.
							gotoState( inActive, $( this ) );

						}
					);

				},


				// I teardown the current state.
				teardown: function(){

					// Remove the mouse-enter event.
					dom.menuBar.undelegate(
						"li.menu",
						"mouseenter"
					);

				}

			};


			// ---------------------------------------------- //
			// ---------------------------------------------- //


			var inActive = {

				// I am the description of the state.
				description: "I am the state in which the user has moused into a menu header and has caused the menu items for that menu to be shown.",


				// I am the DOM elements that may be necessary for
				// the setup and teardown of this state.
				dom: {},


				// I setup the current state.
				setup: function( menu ){

					// Store the dom elements for this state.
					inActive.dom.menu = menu;
					inActive.dom.header = menu.children( "a.header" );
					inActive.dom.items = menu.children( "ol.items" );

					// Change the menu class.
					inActive.dom.menu.addClass( "menuInActive" );

					// Catch the click handler to prevent any default
					// action from taking place.
					inActive.dom.header.click(
						function( event ){

							// Kill the default behavior - this isn't
							// a "real" link.
							event.preventDefault();

						}
					);

					// Delegate the click items on the menu to listen
					// for clicks to individual items.
					inActive.dom.items.delegate(
						"li.item",
						"click",
						function( event ){

							// Log for proof.
							console.log(
								"Clicked:",
								$.trim( $( this ).text() )
							);

						}
					);

					// Bind to the mouse-leave event. If the user
					// exits the menu, we need to close it.
					inActive.dom.menu.mouseleave(
						function( event ){

							// Go to the pre-close state. Pass along
							// the menu that is being closed.
							gotoState( inPreClose, menu );

						}
					);

				},


				// I teardown the current state.
				teardown: function(){

					// Change the menu class.
					inActive.dom.menu.removeClass( "menuInActive" );

					// Remove the click handler on the header.
					inActive.dom.header.unbind( "click" );

					// Undelegate the click event for menu items.
					inActive.dom.items.undelegate( "li.item", "click" );

				}

			};


			// ---------------------------------------------- //
			// ---------------------------------------------- //


			var inPreClose = {

				// I am the description of the state.
				description: "I am the state in which the user has moused-out of the open menu and we need to close it. However, rather than closing it right away, we are going to put a small delay on it. Of course, if the user mouses into another menu, we will close the active one immediatley.",


				// I am the DOM elements that may be necessary for
				// the setup and teardown of this state.
				dom: {},


				// I setup the current state.
				setup: function( menu ){

					// Store the dom elements for this state.
					inPreClose.dom.menu = menu;

					// Change the menu class.
					inPreClose.dom.menu.addClass( "menuInPreClose" );

					// Start the timer for the close.
					inPreClose.timer = setTimeout(
						function(){

							// The user has not moved back into the
							// menu system. Let's close the menu and
							// move back into the default state.
							gotoState( inDefault );

						},
						(1 * 1000)
					);

					// Bind to the mouse entry of any of the menus.
					// Any re-entry will cause us to go to the active
					// state for that menu.
					dom.menuBar.delegate(
						"li.menu",
						"mouseenter",
						function( event ){

							// Go to the active state to show the
							// given menu.
							gotoState( inActive, $( this ) );

						}
					);

				},


				// I teardown the current state.
				teardown: function(){

					// Change the menu class.
					inPreClose.dom.menu.removeClass( "menuInPreClose" );

					// Clear the timer (this way, the menu doesn't
					// close if we mouse back over it).
					clearTimeout( inPreClose.timer );

					// Remove the mouse enter event for the menus.
					dom.menuBar.undelegate( "li.menu", "mouseenter" );

				},


				// I am the timer used to keep track of when this
				// menu needs to be closed.
				timer: null

			};


			// ---------------------------------------------- //
			// ---------------------------------------------- //
			// ---------------------------------------------- //
			// ---------------------------------------------- //


			// To start with, put the menu bar into the default state.
			gotoState( inDefault );


		})( jQuery, jQuery( "ol.menuBar" ) );

	</script>

</body>
</html>

It took me a little while to figure out how to approach this problem. I was concerned that having menus with interdependent behavior would force me to get sloppy. However, by passing relevant data from state to state, as part of the transition, I feel that I am keeping the clean, cohesive nature of the state machine while affording the complexity of a compound user interface (UI) widget.

The scariest part of this, for me, was transitioning from the Active state to the Pre-Close state, and then back to the Active state. Bouncing between these two states actually hides and then re-opens the menu items a number of times (due to the teardown() and subsequent setup() method calls). This fear, however, is unfounded. It's based on a poor understanding of user interface rendering. In my mind, I saw this as happening slowly - something the user might notice; in reality, however, such changes happen instantly! We have to be careful not to personify our browser behaviors!

Of course, it would get more complex if we had sub-menus. In that case, I would probably remove the Pre-Close state altogether and add the timer to the Active state. But, that's for a whole other exploration.

One of the hardest parts about thinking in terms of a state machine is thinking about a single state at a time. However, with this mental shift, I am finding it much easier to think about what code needs to be written. As you can see, there is a good amount of code here; but, if you look at it closely, the code is actually quite simple. Most of it deals with little more than binding and unbinding event handlers. Simple code, kept in cohesive states, seems to allow for complex interactions quite nicely.

Want to use code from this post? Check out the license.

Reader Comments

198 Comments

@Ben:

It seems like extreme overkill (and potentially a way to add memory leaks) to constantly setup/teardown the event handlers.

There's really no value in unregistered the delegated events and you're potentially introducing memory leaks (if the jQuery undelegate method isn't fully cleaning everything up.)

The other thing that bothers me with these examples is you've put a lot of effort into breaking logic down, but it's still not very re-usable in it's current state. I think you'd be better off devising a StateMachine framework, and then using that framework to handle the guts. That way you have less of that code integrated into your actual maintainable code.

15,811 Comments

@Dan,

Just to get on the same page, I don't necessarily think that an example of this size would necessitate such an approach. After all, we've all been building drop-down menus since the beginning of the web - and, at least me personally, have never used this approach.

I think of it like Object Oriented Programming. With small examples - the ones you can wrap your head around - you probably wouldn't even need it in the first place. I started down this road because I am dealing with a few systems where there is such a tremendous amount of JavaScript that it has just become a burden to change in any way.

So, I need to keep it simple so that I can learn the core principles. I'd like to start applying this to more complex interactions where the approach might make increasingly more sense???

Also, I wouldn't want to talk about memory leaks unless that is a known issue. I had thought that by using jQuery to bind and unbind all events handlers, memory leaks would be properly dealt with. As such, let's consider that a non-issue until it is actually an issue.

You are right about the usefulness of the binding / unbinding certain event handlers. Definitely, in this case, there is no gained value since different UI elements can never be interacted with in multiple ways. Meaning, mousing into a menu NEVER results in a different behavior depending on the state of the widget.

Again, however, I am coming from an area where I have some crazy UIs that actually do change behavior based on state (things like tracking mouse-movements but only when certain things are clicked in certain states).

And, honestly, I'm totally convinced this is the right approach either. I like that I can break my thought processes down into individual contexts without having to worry too much about big picture stuff. I have trouble with big-picture stuff.

As far as reusability goes, I am not sure I completely understand what you mean? Are you saying I should be able to reuse state transitions? Wouldn't they only be relevant to a particular widget?

I suppose the gotoState() method is generic enough to be factored out into something. But I don't know where that would go. I suppose you could create some sort of "Stateful" class that can be sub-classed... and maybe a "State" class that can be sub-classed to make the widget controller and the widget states a bit more Object-Oriented. That could be an interesting idea.

198 Comments

@Ben:

First, I wanted to comment on your statement "Also, I wouldn't want to talk about memory leaks unless that is a known issue." The issue I have with that, is memory leaks in 3rd party code can appear and those are the hardest things to track down. Event unbinding seems to be one area in jQuery I've seen memory leaks occur. While this generally isn't going to be an issue (because you're generally not unbinding enough events,) the way your code is structured you'd theoretically be doing it a lot.

Anyway, it was really more of a note that there was a lot of overhead in the process of unbinding that's not really necessarily. I made the comment more for those people who start to use this as a guide of the "proper" way to do this kind of thing (I'm not saying you said it was the "proper" way, just that there are developers who use stuff they see and take it verbatim w/out understanding the ramifications.)

I think you could certain increase the re-usable of your code if you were doing writing something more like:

var sf = new Stateful();
	 
sf.create(
	"inactive"
	, {
		description: "I am the state in which all of the menu items are hidden. Only the header of each menu is shown in the menu bar."
		, dom: {}
		, setup: function(){}
		, teardown: function(){}
	 
	}
);
 
sf.create(
	"active"
	, {
		description: "I am the state in which the user has moused into a menu header and has caused the menu items for that menu to be shown."
		, dom: {}
		, setup: function(menu){
			this.dom.menu = menu;
			 
			this.dom.menu.mouseleave(function(e){
				sf.set("preClose", [menu]);
			});
		}
		, teardown: function(){}
	 
	}
);
 
sf.create(
	"preClose"
	, {
		description: "I am the state in which the user has moused-out of the open menu and we need to close it. However, rather than closing it right away, we are going to put a small delay on it. Of course, if the user mouses into another menu, we will close the active one immediatley."
		, dom: {}
		, setup: function(){}
		, teardown: function(){}
	 
	}
);
 
// set default state
sf.set("inactive");

The create() method would extend a base object with all your base methods and properties. You could use a set() method to change the current state and a get() method to see the current state. All your state tracking would be in the base class. This certainly would be much more useable and should reduce the complexity of writing the code.

Lastly, you really should look at Backbone.js (http://documentcloud.github.com/backbone/). While it's not a state machine, it allows your to build views for your model, which you can then easily unbind a view and could attach a different view. For me, I think this would be a better model because the view behaviors are probably more re-usable, while they state definitions are still going to be very specific to a particular use case. That said, you could probably combine a Backbone view/model w/a state machine.

198 Comments

@Ben:

FYI - In my example, the set() method would take an array of arguments as the second parameter. I did this because it makes it very easy to then just use the apply() method, instead of having to slice the arguments array.

15,811 Comments

@Dan,

I like where you're going with that. I go back and forth on whether I like the array-as-argument approach. It definitely makes the subsequent apply() method easier; but, I almost feel like that should be encapsulated away from the user. But then again, I could also see it argued that passing an array is easier as it allows the user to build it up programmatically before it is passed-in.

Let me play around with this stuff some more.

Also, I tried to Google for memory leaks in jQuery. All the articles I was finding was for jQuery UI and / or was kind of old (like 2007/2008 era). It might still be a problem; but, I was definitely under the impression that using jQuery to both bind AND unbind handlers was not a problem.

198 Comments

@Ben:

My comment about memory leaks in jQuery, is more that they're sort of out of your control. What I mean by that is what happens if a memory leak is reintroduced down the road?

While that is playing the "what if" game a bit, regression bugs have occurred in the jQuery source from time to time and event binding is usually the area where memory leaks can occur.

JS memory leaks are just so hard to predict and track down, that any time I can write code that helps protect against potential problems, I do. That's really more to my point.

15,811 Comments

@Dan,

I totally respect that. In my refactoring, I tried to perform more "Static" binding for event handlers. However, in my particular demo, I found the particular interactions to be conducive to one-time-only bindings. I am doing "drag"; and, unfortunately, the browser can't keep up with the mouse movements. As such, my mouse would often "enter" the UI element in states where that shouldn't really have been possible.

1 Comments

Hi Ben,

I decided to take the FSM approach for a complex UI based on user inputs a while ago and I just watched your video.

Good to know I'm not the only one thinking FSM's could be a solution for complex UI interactions.

In my case I think I'll be good by changing states on input changes and keeping track of a global current state variable.

www.twitter.com/hisa_py

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel