An Exploration In Separating Hash-Change Events From Programmatic Hash Updates
A couple of weeks ago, I presented the idea of learning about "best practices" in event-driven programming by looking at the way the browser handles user-generated vs. programmatic updates. As an example, I put forth the Form-submission. If the user submits a form, the browser triggers a "submit" event; if, however, the page programmatically submits the form, no such "submit" event is triggered. This kind of separation between what does and does not trigger an event can be seen in several such user-interface (UI) interactions. Putting my faith in the years of development behind the Web Browser, I wanted to see if I could apply this same kind of "best practice" mentality to the window's hash value.
NOTE: I know there are a number of really awesome plugins that abstract hash-functionality. This is, in no way the point of this blog post. I say this only to keep you focused on what's important. If you recommend a plugin, just keep in mind that you are definitely going to be missing the point of the following exploration.
When I think about the web browser, I think about the user interface as being a sort of barrier. On one side is the user - on the other side is the Javascript and native programming that responds to user interactions. User interactions seem to trigger events (ex. form-submission, select-change, input-blur) while programmatic changes, on the other hand (ex. form.submit(), select.selectedIndex, input.blur()), do not. If we take, for a moment, that this is done as a "best practice," we can generalize a rule that when it comes to user interface (UI) elements, users can trigger events, programmers cannot.
Well, what about the browser's location? This is definitely a UI element; after all, the user can manually change it. Going on what we've established previously, this means that a manual change of the location should trigger an event while a programmatic change of the location should not trigger anything.
Before we can do this, I think we need to create a sense that a UI element consists of two components: an internal model and an external manifestation. An event, therefore, gets triggered when the external manifestation is altered before the internal model is updated. If the internal model is updated first (such as through a programmatic change), the external manifestation is merely changed in order to align its state with that of the internal model and no event need be triggered.
When it comes to the window's location, the external manifestation (the Location bar) has already been created for us. As such, we need to create our own version of the internal model. This internal model, however, necessarily needs to be something more than a process that just monitors the state of the location - that's just an observer, not a model. What we need is something that actually contains an internal state that describes the location.
Once we have this internal model, we can then use it to both respond to UI (Location bar) changes and to update the UI based on the changes to this model. After some playing around, here's what I came up with:
<!DOCTYPE html>
<html>
<head>
<title>Separating Hash-Change Events From Programmatic Updates</title>
<script type="text/javascript" src="./jquery-1.4.3.js"></script>
</head>
<body style="margin-left: 270px ;">
<h1>
Separating Hash-Change Events From Programmatic Updates
</h1>
<p>
Event: <span id="eventOutput"> ... </span>
</p>
<p>
<a href="#A">A-Link</a> -
<a href="#B">B-Link</a> -
<a href="#C">C-Link</a>
</p>
<script type="text/javascript">
// Set up a timer that will monitor the location and wait
// for hash-chnage events. The sandbox is the object on
// which we will trigger hash-change events.
hashController = (function( sandbox ){
// Set the raw hash as the empty string. Since all
// hash values begin with hash, any change will trigger
// a change.
var rawHash = "";
// Set the current hash to be empty. If the raw hash is
// empty, then the current hash is also empty.
//
// NOTE: The current hash will NOT start with the hash
// sign.
var currentHash = "";
// I am the timer that monitors the hash.
var timer = null;
// I turn on the hash change monitoring.
var start = function(){
timer = setInterval(
function(){
// Get the hash out of the live location.
var liveHash = window.location.hash;
// Check to see if it is different than
// the locally-stored raw hash.
if (rawHash != liveHash){
// Save the old hash for when we trigger
// the hash-change event (we'll want to
// announce the old hash as well).
oldHash = currentHash;
// Overwrite the loacl hash.
rawHash = liveHash;
// Clean the current hash (remove the
// hash sign).
currentHash = rawHash.substr( 1 );
// Trigger a hash-change event on the
// sandbox. We are going to assume that
// the sandbox has the same bind/trigger
// API as the jQuery object.
sandbox.trigger({
type: "hashchange",
hash: currentHash,
prevHash: oldHash
});
}
},
50
);
};
// I turn off the hash change monitoring.
var stop = function(){
clearInterval( timer );
};
// Start the monitor.
start();
// Return the public interface for the hash controller.
// This will grant external access to parts of the
// underlying hash-monitoring mechanism.
return({
// I return the current hash value.
getHash: function(){
return( currentHash );
},
// I set the current hash value.
setHash: function( newHash ){
// When setting the hash, the first thing we
// want to do is stop monitoring the location -
// the external manifestation of our hash. We
// do this because we don't want to announce
// our hash-change event when programmatically
// setting the hash. This will give us time to
// adjust our internal model before we change
// our external model.
stop();
// Store the old hash.
oldHash = currentHash;
// Change the internal hash.
currentHash = newHash;
// Create the raw hash.
rawHash = ("#" + currentHash);
// Set the live hash.
window.location.hash = currentHash;
// Now that we've updated our internal model
// and the public manifestation (the location),
// we can now start monitoring our hash again.
start();
// Return this object reference for method
// chaining.
return( this );
}
});
})( $( document ) );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Bind to the hash-change event on the document in order
// to monitor events.
$( document ).bind(
"hashchange",
function( event ){
// Output event to the page.
$( "#eventOutput" ).text(
"hashChange: " + event.hash
);
}
);
</script>
</body>
</html>
As you can see, I create a Singleton - hashController - that has two public methods: getHash() and setHash(). Internally, the hashController does have a process that monitors the window's location. This is to see when its "external manifestation" has been updated by the user - user-driven updates, do trigger a "hashchange" event. However, if we programmatically call the setHash() method, you'll notice something critical - the very first thing that the hashController does is stop monitoring its external manifestation.
I stop the monitoring process for two reason:
I am honestly not sure what kind of race-conditions a function running within a setInterval() process might create. I don't understand the parallel nature of timers enough to know how to most carefully treat shared variables.
To make a point. When we programmatically alter the internal state of the hashController, I am trying to be absolutely clear that we are going to update the internal state and then, only as a subsequent action, change the external manifestation (Location bar) for no reason other than to mirror the new internal state of the model.
In performing the updates in this order, once the monitoring process is turned back on, there's no difference between the internal model and the external manifestation. As such, the monitoring process has no reason to trigger any event.
I don't know if this sounds completely crazy or what, but there's something about this that feels very right to me. But, then again, I'm not exactly versed in the art of event-driven programming. Of course, this type of approach doesn't necessarily apply to all aspects of event-driven programming; this exploration deals very specifically with models that can be changed by direct user interactions; specifically, models that have user-interface components.
Want to use code from this post? Check out the license.
Reader Comments
In this post, I talked about my ignorance to how race conditions work in Javascript with intervals. This morning, I did some testing for race conditions using setTimeout(), setInterval(), and AJAX requests:
www.bennadel.com/blog/2120-Exploring-Race-Conditions-In-Javascript-With-SetInterval-SetTimeout-And-AJAX.htm
It appears that no race conditions really exist. While the timers allow things to begin executing in parallel, it appears that the actual execution is done in serial.