Hey folks,
in my upcoming task management application (which will btw. *fix* task management : ) I'm doing a *lot* of Javascript. In fact I want to make a statement before beginning this post: Javascript is now officially my favourite language. But don't worry, I won't give up on the PHP/etc. for the server side just yet (seen what John Resig has been up to as of lately?) - JS doesn't compete in that category so far. I will however probably start to spend equal amounts of time working in JS and PHP which means you could see a lot more JS on this blog in future (you know, the future where I'll have time for some hardcore blogging again : ).
Anyway, back to the topic - I'm writing a task management application and this is a greatly simplified version of the markup I'm using:
<table class="tasks">
<tr>
<th>Task
</th>
<th>Time
</th>
</td>
<tr class="task">
<td>Finish project X
</td>
<td>1 hour
</td>
</tr>
<tr class="task">
<td>Write a blog entry
</td>
<td>1 hour 30 minutes
</td>
</tr>
</table>
Since I'm getting fancy with JS these days I've written some jQuery plugins that allow me to interact with my DOM in a very nice way:
$('tr.task:nth(1)')
.task('field', 'name', 'Write a good blog entry')
.task('field', 'time', 2*60)
.task('save');
The sample above will select the second row (:nth is 0-indexed) in my table, change the 'Task' name to 'Write a good blog entry' and the 'Time' to '2 hours'. Pretty neat. But - I was lying earlier. At least partially. My actual markup is more complicated then the example above, but there is no 'task' class on the tr columns. Why? It's because I'm always looking for excuses to use jQuery's awesome selector engine and practice my CSS3 skills : ). So my code from above needs to be rewritten like this:
$('tr[:not(th)]:nth(1)')
.task('field', 'name', 'Write a good blog entry')
.task('field', 'time', 2*60)
.task('save');
So what does that do? Well instead of selecting all rows with a class 'task' it selects all rows that do not contain a 'th' element which comes down to the same elements. But you all know I'm a friend of simplicity, so even if jQuery offers us some nice selectors it might be possible to get even fancier. I've been thinking about it for a while and came to the conclusion that what I really want this selector to look like is this:
$(':task(2)')
.task('field', 'name', 'Write a good blog entry')
.task('field', 'time', 2*60)
.task('save');
Now that's crazy (!) I can hear some of your say, but I really wanted it so I've tried to figure out how. Today I came across one of Mike Alsup's pages (who btw. makes some of the best jQuery plugins out there) and finally saw how to do it. My initial attempt was very much oriented at how most selectors work and got pretty complex because this seems to be meant for more simple selectors. Anyway here is the code for it:
jQuery.expr[':'].task = '(i==0 && (arguments.callee.c = 0) || 1)
&& /tr/i.test(a.nodeName)
&& !jQuery.find("th",a).length
&& (arguments.callee.c++ || 1)
&& (m[3] === undefined || m[3] == arguments.callee.c)';
Here is the short version of what it does: This expression is called up for every element the jQuery CSS engine loops through. If it evaluates to true then the element "matches", otherwise it's not included in the resulting jQuery array. My expression here only matches if the element is a 'tr', does does not contain a 'th' and if the :task(n) parameter matches the nth-task that was found (kept track of in arguments.callee.c). This of course is a very unreadable as it heavily depends on JS's short circuit logic (something PHP has as well) so I came up with a more readable version of it:
// Selector for :task and :task(n)
jQuery.isTask = function(a, i, m) {
// Reset our element counter if this function is called for the first time on the current set of elements
if (i == 0) {
arguments.callee.count = 0;
}
// If this is not a <tr> or contains <th> then we are not interested in it
if (!/tr/i.test(a.nodeName) || jQuery.find('th', a).length) {
return false;
}
// Increment our element counter
arguments.callee.count++;
// If no task# was given or it matches our current counter then return true
return (!m || m[3] === undefined)
? true
: arguments.callee.count == m[3];
};
jQuery.expr[':'].task = 'jQuery.isTask(a, i, m);';
As you can see, this should pretty much do the same thing but is easier to maintain. For those of you still totally confused some hints about the code: 'a' is always the element we currently test. 'i' is the index of the element we are currently testing, but since we only count the ones we are interested in, we do our own counting in arguments.calllee.c / arguments.calllee.counter. 'm' is the array matched by the regular expression in jQuery internally. The regex used there are rather complex but in our case m looks like this: m = [':task(2)', ':', 'task', '2', undefined];. So when we check against m[3] we're accessing the :task(n) parameter.
Soo, what value does this have if we're back at where our initial 'tr.task:nth(1)' has brought us? Good question ; ). How about this: Has anybody ever thought about providing a JS API for their application? Not one where you can remotely access the service - that's oldschool. No, I mean one where interested developers can directly hack the way your app works without using greasemonkey? I have, and I think there is some real potential to be unleashed here given that you can provide the right tools and security for the job. I mean I'm using Thunderbird for my emailing right now, but if Google would give me tools that would easily allow me to modify the way Gmail works to my liking - I'd be switching to it as my main interface right away! The possibilities this opens are really endless and I only see limited appliance in my little task management app. However some stuff like this surely would be worth implementing:
$(':task(@done)')
.task('remove');
$(':task(@name*=work)')
.task('field', 'date', 'tomorrow')
.task('save');
Anyway, now you know what has been getting me really excited as of lately. It's yet another proof of what a beautiful piece of art jQuery is. I checked EXT + prototype today and it doesn't seem as easy to extend them like this (if I'm wrong, please let me know!). So it's bedtime for me now and I can finally do this for tonight:
$('*:task').task('remove');
Thanks for reading and hopefully you got some useful information out of this,
-- Felix Geisendörfer aka the_undefined
PS: If that technique I use to manipulate the tasks in my app interests you - I'll be releasing the JS script making it possible soon.
PS2: I did not proof read this post and hitting bed now - it probably contains more typos and grammar issues then usual ^^.