Learning Event-Driven Programming Best Practices From Web Browsers
Event-driven programming is an area of computer-science that I have really been trying to wrap my head around. So far, it has been an interesting but slow journey. One of the things that I have come to realize, however, is that event handlers should never be able to trigger themselves. That is, an event handler should never be able to raise an event that is bound to the currently-executing event handler. Doing so would likely result in an infinitely recursing chain of events.
As it turns out, the web browser seems to already understand this concept. If you attempt to focus a form field within an onFocus() event handler or submit a form from within an onSubmit() event handler, the browser will carry out the given action without triggering any subsequent events. If you only ever work within the confines of jQuery, you probably wouldn't know this because jQuery has no safe-guards against this - calling focus() within a focus event-handler will happily launch an infinitely recursing loop.
As you can see in this code, our "focus" event handler turns around and, again, tries to blur and then focus itself. You'd think that this would create an infinite loop; however, when we focus the form field, we get the following console output:
User focused me!
The focus-handler only executed once. There were no errors, no recursion; the event handler simply executed without raising any unfortunate events.
If you look at the code, though, you'll notice that my original title for the blog post was, "When To Trigger A User-Driven Event?" I started out with this title because the event architecture of the browser seems to go beyond simply preventing recursive event calls; in fact, that might be a side-effect of the real the fact that the browser also seems to draw a hard line between user-triggered events and system-triggered events. To see what I'm talking about, take a look at this video:
In the video, you can see that my onSubmit event handler was able to intercept user-driven form submissions. However, the event handler was not invoked during a programmatic form submission. We're starting to see that things are a little bit more complex than just preventing recursive events - some events are only raised by user-interactions.
I don't really know what conclusions to draw at this point as I'm still sorting out all the event-driven concepts in my head. What I can say, however, is that event-driven programing involves way more than simply triggering events - there seems to be a whole multi-layer approach to triggering events and carrying out actions. If anyone has a good grip on the trends that I'm seeing, I'd really appreciate some more insight.
Want to use code from this post? Check out the license.
Very interesting. Just I hope that we don't face a different browser behavior issue, then we need also to take care of which browser client is using.
Honestly ... I hate getting deeply into these complicated details and trying my best to avoid it ... it totally confuses me.
As I saw from your nice video, JQuery is solving one issue and leaving another .. so can I say "we can't rely on JQuery always?"
But in case if I have to develop rich internet application with Ajax and user interaction ... something high level close to the huge Google Docs. Then of course I have to take care of all these things .. finally, this is my point of view :)
I wouldn't be concerned about depending on jQuery. jQuery is awesome and the point of the post certainly wasn't to point out shortcomings with the library. Really, what I wanted to explore was the architecture of event-driven applications. I think web browsers are a wonderful example of a complex system that uses a lot of events... and does so very well.
Mostly, I just want to start thinking more deeply about what parts of my application respond to events vs. what parts of my applications announce events.
An addendum to my original comment. Try the following Ben:
I've wrapped the last call to focus in a setTimeout call and I'm certain that now you will see recursive behavior. I remember sometime ago I was having some weird interactions between the different dom event levels and the solution was to wrap things inside setTimeout to give everything enough time to propagate upwards.
For example, suppose that you have an (input type="button") button that you want to go to a different action page from the rest of the form. You can always say this:
You don't want to replace this.form.action unless the form's onsubmit returns true (passed validation). Otherwise, you've messed up all of the (input type="submit") buttons by overlaying the default action.
I might add that JS isn't terribly consistent with regard to doing just one thing. If you set this.form.MyRadio.checked, you will automatically uncheck a different radio button in the MyRadio group. A truly fine-grained JS interface would require you to uncheck the other radio buttons in the group yourself. But in that case, the DOM-meisters decreed that consistent user interface trumps fine granularity.
In other words, doing just one thing per event or event handler, or doing more than one thing, is a tradeoff. It was resolved one way in some situations and another way in other situations.
I just thank goodness for the consistency of returning false to interdict default behavior and stop event propagation. Thank you for that, DOM-meisters.
The more I think about this post, the less I think about the browser's implementation and the more I think about application architecture. I pointed all this out because I was seeing a trend in separating "something". Even after I pointed the two occurences, I was still not sure what the trend was; and, as Steve points out,
It was resolved one way in some situations and another way in other situations.
The scenario that keeps coming back to mind is that of a system that monitors the location hash for thick-client applications. Imagine my hash can be changes arbitrarily:
... and this change is monitored and used to update the UI of the application.
When the hash is changed by the user (ie. the user manually updates the hash in the browser), the system detects that and announces a "hashchange" event (or something to that effect). The UI controller / mediator then listens for that event and updates the UI.
But, now, let's come at it from the other side - image the user clicks on the "Contacts" link within the app and the app itself changes the hash:
location.hash = "#/contacts";
This is where we can quickly run into poorly designed event architectures (I know because I've been there :)). If the application changes the hash, this can, in turn, trigger a "hashchange" event, which can, in turn, trigger an unintended, and likely to be redundant update to the UI.
NOTE: The "hashchange" event that I'm talking about is not the native event - I know some browsers actually support a native hashchange event. I just mean a generic event used when monitoring the URL.
So again, we come to a point where we need a strict separation between what triggers events and what does not. In our case, when the *USER* performs an action (ie. modifies the URL), we want to trigger the hashchange event; however, when the *SYSTEM* performs an action (ie. modifies the URL), we do not want to trigger an event.
I don't yet know how to best go about this. I'm just starting to see that there a real hurdles that I don't know how to get over yet.
I notice that click() behaves differently than submit().
These both yield the same result for me; the event delegate fires as expected:
But as you show, these do not. The second line does not fire the event delegate.
Here's a postulate: Behind the scenes, at the browser-level, submit() is a method. Calling submit() invokes the method. UI-events, on the other hand, are pure events, not methods.
I say this because jQuery has a special event code within its event.js module that triggers and raises a "submit" event for us. When we bypass jQuery in the console, are we also bypassing the event wiring?
Looking deeper into this, there are evidently browser differences in how submit "event" is handled. This applies to all events, really.
As usual, jQuery protects us from all that.
THEREFORE it would appear that, arising from this, we could say as a rule avoid calling DOM node events directly if you're using jQuery.
This observation might help: If the onevent() handler can prevent the default behavior by returning false or calling preventDefault(), the event() method generally doesn't call the onevent() handler. That way, you can call onevent() to decide whether to trigger event().
But you said that focus() calls onfocus(), didn't you? That would seem to be an exception that I was unaware of.
In the early days of Netscape it was easy to get into a UI loop if you called alert() from within onblur(), because the alert itself was a blur event. So you clear the alert, and it would pop up again immediately, infinitely, until you killed the browser entirely. So I got into the habit of avoiding onfocus() and onblur() for data validation.
Does focus() call onfocus() in all browsers you tested? Or just the awful one?
So-So written article, if only all bloggers offered the same content as you, the internet would be a somewhat better place. Keep it up! Happy Days. j/k -- please block uggbootssale for the love of BN.COM!
Very cool post, @Ben. Always love "Best Practices" posts, esp when they may keep me from avoiding a pitfall (did you hear the Atari tones?) in the future.
@Anirudha - I think you are wanting to know more about JQuery -- http://jquery.com/
i am know about jQuery but i don't know which software they use to write jQuery in this video.
@ randall tell me the name of software they use in this video.
It looks like Ben is using CF Builder here, an Eclipse-based ColdFusion editor. My guess is that he's got a jQuery plugin added that's helping with the code hints.
I am sure there are variations across the different browsers. If I remember correctly, I only tested this is my *awesome* browser ;) But, again, I think the main take-away I had from this was much less about the way we should work with native browser events and more about how we should think about building event-driven architectures that don't get caught in infinite loops (ala the blur/alert cycle you were talking about).
Very specifically, I want to think about the whole hash-change event; I think if I can wrap my head around that (changing it without then subsequently reacting to it) then I will have a much much better understanding of the type of architecture I am talking about.
So I took this concept and used it to explore the Location Bar and subsequent hashChange events:
Not sure if I am totally crazy or totally onto something :)