Perfection kills

Exploring Javascript by example

Archives Posts

Detect idle state with custom events

October 17th, 2007 by kangax

Some time ago, Andrew Sellick posted his idle state snippet based on prototype. The idea is to capture the moment when user has been idle for certain period of time. Idle means that there was no interaction with document or viewport. You might be wondering why in the world would we need to capture user idle state. Well, I can definitely think of 2 good reasons:

  • We could pause periodical ajax requests happening on a background to prevent server overload
  • We could stop some resource intensive operations happening on a client side (i.e. animation or data processing)

Ok, so detecting idle state could be useful, but how do we go about implementing it?

Before diving into all the dirty details, I’d like to mention that Andrew’s code encaplsulates “onIdle” logic into a one single function – this is where we would have to stop our requests/calculations. Such approach is absolutely fine but to decouple things a little we could abstract them into custom events. This will allow us to subscribe anyone to independently observe idle/active state of the user and act accordingly. There are quite few very, good, explanations of custom events, just in case you’re not sure what they are, as well as prototype, based, extensions. Prototype natively supports custom events since version 1.6 (which is not yet officially released). The implementation still varies (i.e. there were some drastic changes in one of the last revisions) but overall works flawlessly. In the following example I am going to use a new way of defining events – prefixing them with pseudo namespace (which I think is more descriptive).

Ok, so let’s see what these weird things are all about.

1

First, we store events (and corresponding elements) that represent any kind of interaction in a two dimensional array. I think these should pretty much cover any user action.

var events = [
	[window, 'scroll'],
	[window, 'resize'],
	[document, 'mousemove'],
	[document, 'keydown']
];

2

Next, we are interating over this array, invoking Event.observe for each element/event pair. The observe handler is simply an anonymous function. This function will fire our custom ’state:active’ event passing idleTime parameter. We basically encapsulate few native DOM events into one custom. To make things clear, the event is essentially just an object. It has methods such as .stop() and .findElement() as well as properties such as .target and .pageX. When firing custom event we can pass an optional object along with it – storing any additional data that might be useful later on. This data is then accessible through event.memo and in our case quite conveniently stores idle time.

events.each(function(e){
	Event.observe(e[0], e[1], function(){
		document.fire('state:active', {idleTime: new Time() - document._idleTime});
		clearTimeout(document._idleTime);
	})
})

3

Now that we have taken care of ’state:active’ all that’s left to do is set a timer to fire the opposite event – ’state:idle’. You can obviously change the time to anything you like. The trick here is that ’state:idle’ event will only get fired if timer doesn’t get cleared within 5 seconds (by any of the above specified user actions)

document._timer = setTimeout(function(){
	document.fire('state:idle')
}, 5000);

4

What’s really cool is that we can now observe these custom events just like we would observe the regular ones! Note the use of memo.idleTime to retrieve data passed along with the object.

document.observe('state:idle', onStateIdle).observe('state:active', onStateActive);
 
onIdle = function() {
	console.log('Oh no... where is everybody?');
}
onIdleEnd = function(e) {
	console.log('Looks like user is still alive but have been idle for ', e.memo.idleTime, ' ms');
}

We can now wrap this code into a self containing class to keep things clean and unobtrusive:

var Notifier = Class.create({
 
	_events: [[window, 'scroll'], [window, 'resize'], [document, 'mousemove'], [document, 'keydown']],
	_timer: null,
	_idleTime: null,
 
	initialize: function(time) {
		this.time = time;
 
		this.initObservers();
		this.setTimer();
	},
 
	initObservers: function() {
		this._events.each(function(e) {
			Event.observe(e[0], e[1], this.onInterrupt.bind(this))
		}.bind(this))
	},
 
	onInterrupt: function() {
		document.fire('state:active', { idleTime: new Date() - this._idleTime });
		this.setTimer();
	},
 
	setTimer: function() {
		clearTimeout(this._timer);
		this._idleTime = new Date();
		this._timer = setTimeout(function() {
			document.fire('state:idle');
		}, this.time)
	}
})

Here’s a little demo page to see this in action.

Enjoy your ride.

Archives Posts

Refactoring with Prototype

October 5th, 2007 by kangax

I’m not surprised to see prototype code that sucks. Been there, done that (and I’m sure still doing it) – nobody is perfect. What surprises me though is how ridiculous some explanations (of writing crappy code) sound. “I don’t really know javascript. I do PHP professionally” is my favorite. Don’t get me wrong, I understand that experience and deep understanding of language is essential for writing smarter code. It could be a real challenge to wrap your mind around some of the dynamic concepts of such languages as javascript. That I can understand. What I don’t understand, though, is how people don’t follow some of the fundamental principles of programming. Do we really need to know javascript to prevent repetition, unnecessary constructions or poor optimization? I seriously doubt it…

I want to take a look at few examples of real life applications and see what we can do to make them… better. Aiming for a smaller footprint, faster execution and prettier code is what makes it a real challenge, so let’s dive in…

Repetition

function hideall() {
    hidediv('dropdown1');
    hidediv('dropdown2');
    hidediv('dropdown3');
    hidediv('dropdown4');
    hidediv('dropdown5');
    hidediv('dropdown6');
    hidediv('dropdown7');
}

Very basic, ugly (and unfortunately real) example. The only part that varies here are numbers so all we need to do is loop through them. Prototype provides us with a quite convenient utility function for generating ranges – $R takes 2 parameters – starting and ending values and creates a range object by calling succ method on each value. What’s really cool about this object is that it extends prototype’s Enumerable methods – iteration has never been easier:

$R(1,7).each(function(n) {
    hidediv('dropdown' + n);
})

Not only we decreased the overall size of this snippet, but notice how more generic and flexible it has become.

Repetition 2

function resetnotes() {
    $('left1').removeClassName('learn');
    $('left1').removeClassName('play');
    $('left1').removeClassName('grow');
    $('left1').removeClassName('join');
}

This one is similar but is even worse, as we’re calling $ function multiple times, decreasing overall performance. What varies here are plain strings and that’s when $w helper could come in handy. Inspired by Ruby, $w takes a string of space-separated values and creates an array out of it. Remember that arrays are automatically extended with Enumerable methods:

// storing element's reference for faster access
var left1 = $('left1');
// creating array of strings and iterating over them
$w('learn play grow join').each(function(s){
    left1.removeClassName(s);
})

Another solution would be to wrap (augment) removeClassName to accept multiple arguments (classNames).
I don’t suggest to use the following snippet in a production environment as I haven’t tested it thoroughly. The idea is to show you basic way of augmenting prototype’s existent methods.

Element.addMethods({
    removeClassName: Element.Methods.removeClassName.wrap(function() {
        var args = $A(arguments), proceed = args.shift(), element = $(args.shift());
        if (args.length > 1) {
            // this is our modified behavior (only if more than 1 argument was passed)
            element.className = element.className.replace(new RegExp("\s*(" + args.join('|') + ")\s*"), '');
            return element;
        }
        // and this is a default one
        return element.proceed();
    })
})

Attention: Try not to fall into a trap of “generic-madness”. Yes, it’s beautiful. Yes, it encapsulates certain logic into its own layer and creates reusable chunks of code, but no matter how great it is, think twice before taking this approach. Does it pay to spend time creating another abstraction layer? Will it actually be used in the future? If it doesn’t simplify, just drop it. Keep it simple and move on.

Using Element.addMethods we redefine prototype’s removeClassName to suit our needs. Notice how unobtrusive yet powerful this solution is. By wrapping already existent method with or own behaviour, we keep same logic in same place, and making sure augmented method does not break anything (Take a look at a previous post about Function#wrap if you’re not familiar with it)

Ternary operators

if (typeof this.prevbutton == 'object') {
	$(prevmonth).appendChild(this.prevbutton);
}
else {
	$(prevmonth).update(this.prevbutton);
}

Using “if … else … ” for only two conditions is usually a bad thing to do. Selecting one of two actions is even worse.
Ternary operator, on the other hand, is a perfect replacement for “if … else … ” clause. Remember, that we can use bracket notation to call one of two object’s methods:

$(prevmonth)[typeof(this.prevbutton) == 'object' ? 'appendChild' : 'update'](this.prevbutton);

Chaining

$(nextmonth).addClassName('calcontrol');
$(nextmonth).addClassName('calnextmonth');
$(nextmonth).observe('click',this.nextmonth);

Taking advantage of chaining is easy – most of the methods in prototype return reference to the element they were invoked on. Another thing to note is that addClassName is simply appending it’s argument to the className of an element. There’s really no need invoking it more than once:

$(nextmonth).addClassName('calcontrol calnextmonth').observe('click', this.nextmonth);

Those are very simple yet essential tips for writing more concise, elegant code and I would really want to see more people using them.
Enjoy your ride and happy prototyping…