Skip to main content
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Phillip Senn and Michael Senn
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Phillip Senn Michael Senn

Treating User Interface (UI) Widgets Like Finite State Machines

By
Published in Comments (5)

Lately, I've been thinking a lot about JavaScript-heavy, thick-client applications and Finite State Machines. I've seen the way that Finite State Machines can take a large, complex tasks and break it down into smaller, much more manageable states. I've even tried to apply this kind of mentality to binding and unbinding event handlers in JavaScript. But, now that I am getting more comfortable with state machines and, in particular, state transitions, I wanted to try and apply this mindset to an cohesive user interface (UI) widget.

The user interface widget that I wanted to experiment with was a drop-down menu. It seemed to me to be small enough to maintain sanity while being complex enough to meaningfully explore the topic. And, as I am getting more comfortable with state machines, I am realizing that it is absolutely critical to think about the valid states that the state machine (our UI widget) can be in before actually coding. For our menu, here are the states I can come up with:

  • Default - Just the root / header of the menu is showing.

  • Hover - The user has hovered over the menu header, but has not clicked to reveal the items.

  • Active - The user has clicked the header and has revealed the items.

When it comes to transitioning from one of these states to the other, we are going to rely on user interactions. Specifically, we are going to use mouse gestures and clicks to move from one menu state to the next. I don't have a state diagram, but I'll break the interaction / transition model into a finite list:

Default State

  • Header::MouseEnter - Go to the Hover state.

Hover State

  • Header::MouseLeave - Go to the Default state.
  • Header::Click - Go to the Active state.

Active State

  • Header::Click - Go to the Active state.
  • Stage::MouseDown - Go to the default state.

When it comes to state management, there has to be a balance between CSS (Cascading Style Sheets) and JavaScript. As I am learning this stuff, I want offload as much as the show/hide functionality into the CSS and keep all of the event binding/unbinding in JavaScript. That is, I wanted to put as few show() and hide() method calls in my JavaScript as possible.

This means that as the state of the user interface (UI) widget / Finite State Machine changes, so does the CSS. Using a quasi-OO-CSS (Object Oriented CSS) approach, I denoted each state with an additional CSS class on the menu's top DOM element:

  • div.menu {} /** default state **/
  • div.menuInHover {}
  • div.menuInActive {}

Each of these root CSS classes determines the look of the menu, header, and items elements according to the given state. To leverage the "cascading" portion of the technology, the "menu" class is always attached to the top menu DOM element. Then, the "menuInHover" and "menuInActive" classes are added to it, in an OO-CSS fashion, overriding only the portions of the Default state that are necessary.

When it comes to the JavaScript part of the user interface (UI) widget, each state is defined as an object that has a few core methods:

  • gotoState() - This is a convenience method that can be passed off to an event-handler to be used to transition to the given state.

  • setup() - This configures the UI widget for the given state.

  • teardown() - This tears down and removes any configuration applied in the setup() method.

I explored this setup/teardown approach a few weeks ago when looking at binding and unbinding event handlers. However, in this exploration, I'm building in a smoother transitional model which keeps state transitions more implicit.

Now that we have a sense of what we're dealing with, let's take a look at some code. The CSS is at the top and the JavaScript is at the bottom. Notice that I use a self-executing function call in order to create a "sandbox" for our widget configuration code.

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

	<style type="text/css">

		/* DEFAULT state for MENU. */

		div.menu {
			height: 30px ;
			position: relative ;
			width: 200px ;
			}

		div.menu > a.header {
			background-color: #F0F0F0 ;
			border: 1px solid #999999 ;
			border-radius: 5px 5px 5px 5px ;
			color: #333333 ;
			display: block ;
			height: 30px ;
			line-height: 30px ;
			padding: 0px 10px 0px 10px ;
			width: 180px ;
			}

		div.menu > ol.items {
			border: 1px solid #999999 ;
			border-width: 0px 1px 1px 1px ;
			border-radius: 0px 0px 5px 5px ;
			display: none ;
			left: 0px ;
			list-style-type: none ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 0px 0px 0px ;
			position: absolute ;
			top: 30px ;
			width: 200px ;
			}

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


		/* HOVER state for MENU. */

		div.menuInHover {}

		div.menuInHover > a.header {
			background-color: #EAEAEA ;
			border-color: #333333 ;
			}


		/* ACTIVE state for MENU. */

		div.menuInActive {}

		div.menuInActive > a.header {
			border-bottom-width: 0px ;
			border-radius: 5px 5px 0px 0px ;
			}

		div.menuInActive > ol.items {
			display: block ;
			}

		div.menuInActive li.item:hover {
			background-color: #F0F0F0 ;
			}

	</style>
</head>
<body>

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


	<!-- BEGIN: Menu Widget. -->
	<div class="menu">

		<a href="#" class="header">
			My Awesome Friends
		</a>

		<ol class="items">
			<li class="item">
				Anna Banana
			</li>
			<li class="item">
				Joanna
			</li>
			<li class="item">
				Tricia
			</li>
		</ol>

	</div>
	<!-- END: Menu 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 widget controller.
		(function( $, menu ){


			// Cache DOM references for later use.
			var dom = {};
			dom.stage = $( document );
			dom.menu = menu;
			dom.header = dom.menu.find( "> a.header" );
			dom.items = dom.menu.find( "> ol.items" );


			// This is the current state of the widget. Once the
			// states are defined, this will be further set.
			var currentState = null;


			// I fascilitate the transition from the current to
			// the target state.
			var gotoState = function( newState ){

				// 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){

					// Setup the new state.
					newState.setup()

				}

				// 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 (mostly for
				// debugging and documentation).
				description: "I am the state in which only the menu header appears.",


				// I am a short-hand GOTO function. This method can
				// be passed off to event handlers without scoping
				// problems.
				gotoState: function(){

					// Put widget into this state.
					gotoState( inDefault );

				},


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

					// Add a mouse-enter event for the menu. When the
					// user mouses over the header, we need to put
					// the menu into the Hover state.
					dom.menu.mouseenter( inHover.gotoState );

				},


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

					// Remove the mouse-enter event.
					dom.menu.unbind( "mouseenter" );

				}

			};


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


			var inHover = {

				// I am the description of the state (mostly for
				// debugging and documentation).
				description: "I am the state in which the user has moused-over the header of the menu, but the menu has now shown yet.",


				// I am a short-hand GOTO function. This method can
				// be passed off to event handlers without scoping
				// problems.
				gotoState: function(){

					// Put widget into this state.
					gotoState( inHover );

				},


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

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

					// Add a mouse-leave event for the menu. When
					// the user mouses out of the menu, we need to
					// put it back into the Default state.
					dom.menu.mouseleave( inDefault.gotoState );

					// Add a click handler to show the menu items.
					dom.header.click(
						function( event ){

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

							// Goto to the active tate.
							gotoState( inActive );

						}
					);

				},


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

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

					// Remove the mouse-leave event.
					dom.menu.unbind( "mouseleave" );

					// Remove the click event.
					dom.header.unbind( "click" );

				}

			};


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


			var inActive = {

				// I am the description of the state (mostly for
				// debugging and documentation).
				description: "I am the state in which the user has clicked on the menu and the menu items have been shown. At this point, menu items can be clicked for fun and profit.",


				// I am a short-hand GOTO function. This method can
				// be passed off to event handlers without scoping
				// problems.
				gotoState: function(){

					// Put widget into this state.
					gotoState( inActive );

				},


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

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

					// Now that the menu is shown, let's use a click
					// event on the state to trigger the exiting of
					// this state (rather than a mouseleave).
					dom.stage.mousedown(
						function( event ){

							// Get a reference to the target item.
							var target = $( event.target );

							// Check to make sure that the click did
							// not occur INSIDE the menu. If it did,
							// then we don't want to do anything.
							if (!target.closest( "div.menu" ).length){

								// Click was OUTSIDE the menu. Go
								// back to the default state.
								gotoState( inDefault );

							}

						}
					);

					// Add a click handler to hide the menu items.
					dom.header.click(
						function( event ){

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

							// Go back to the hover tate.
							gotoState( inHover );

						}
					);

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

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

						}
					);

				},


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

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

					// Remove the mouse listener from the stage.
					dom.stage.unbind( "mousedown", inDefault.gotoState );

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

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

				}

			};


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


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


		})( jQuery, jQuery( "div.menu" ) );

	</script>

</body>
</html>

No doubt, you might look at this and think that it's a lot of code for such a simple example. And, it's true, there is a lot of code here. In fact, you could probably accomplish the same outcome with a few lines of code and no state machine. But, as we get into thicker, more JavaScript-intense applications, our widgets aren't so simple; they can be quite complex and may include many states of configuration. What I find so appealing about a "state machine" approach is not that it's less code but, rather, because it's much easier to break apart and understand.

A perfect example of this might be the binding and unbind of the "click" handler on the menu items. In reality, there's no need to bind and unbind the click handler - the menu items will never be visible and, at the same time, not clickable. Or, at least we don't think so. So, while it might seem excessive to bind and unbind the click handler based on the state of the menu, doing so allows us to more easily augment the state machine going forward. And remember, so much of writing good code isn't about making it easy now, it's about making it easy later.

Finite State Machines are pretty cool. I'm trying not to apply them arbitrarily; but, I have to say that I really like the way they make you think. Rather than simply thinking about user interactions, you have to think about the valid states of an entity. This forces you to think more holistically, which will, I believe, produce better, more maintainable code in the long run.

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

Reader Comments

1 Comments

Nice!
Another approach I've been using a lot is the idea that you keep track of a widget's state in purely abstract "data" form and then just re-render the entire widget every time the state changes, even minutely. jQuery templates make this super easy. I think this is the way most UI interfaces not created in HTML work. Yes, you are making the processor do a lot more work, often to change something very small, but you are massively simplifying the maintenance.

15,841 Comments

@Aaron,

I've played around very briefly with that kind of approach. I was trying to bring the "server-side" model to the client. On the server, every request causes a total page re-rendering. So, I thought, what if the client-side code does the same thing.

www.bennadel.com/blog/1999-Reflections-On-My-Client-Side-MVC-View-Rendering-Hackathon.htm

It's been a long time since I looked that, but I think it actually cleared out the root container for every "major" interaction and then re-rendered.

There's so many way to approach this stuff! I feel like I'm never gonna be satisfied. Though, I think I am liking this Finite State Machine approach - feels like it will make it easier for my monkey-brain to pick it apart and think about it a piece at a time.

1 Comments

With jQuery, I've been using

$.fn.data

to keep track of my states and a widget "controller" to handle next state. I think a drawback to my current approach is difficulty in adding future states. By making every state aware of where it could go, you allow for greater flexibility because every state only needs to be concerned with itself and the next state when a particular action occurs. In my current approach, I have to modify my "controller", which is managing all states. I think the biggest takeaway point here is, no matter how you are managing your states, you should be thinking about widgets in terms of state and figuring out what those states are, as much as possible, before coding.

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