debuggable

 
Contact Us
 

Getting serious with jQuery - Adding custom CSS selectors

Posted on 17/7/07 by Felix Geisendörfer

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 ^^.

 
&nsbp;

You can skip to the end and add a comment.

Chris Norton said on Jul 17, 2007:

Great post - I've been interested in checking out jQuery for a while now but my JavaScript skills aren't the best. I am wondering what modifications would need to be made with your code to get it to work with a structure like this:

Task
Time

Project
1 hour

Chris Norton said on Jul 17, 2007:

Whoops, sorry I forgot to encode my HTML!

<tabel class="tasks">
<thead>

<th>Task</th>

<th>Time</th>

</thead>

<tbody>

<tr>

<td>Project</td>

<td>1 hour</td>

</tr>

</tbody>

</table>

kabturek said on Jul 17, 2007:

I sow that coming the moment you started evangelized us poor souls @ #cakephp ! :D

Felix Geisendörfer said on Jul 17, 2007:

Chris: No modifications needed. I just use Html 4.01 for most stuff I do (instead of serving XHTML tag soup).

Lucian  said on Jul 17, 2007:

Javascript is my favourite language too...:)
However, I'm in love with ExtJS, but I still use jQuery for animations and other stuff.

Waiting to see your app in action...

Jippi said on Jul 17, 2007:

Thats some dead sexy js you got there.... sooo.. is it compatible with IE ? ;)

malsup said on Jul 17, 2007:

Thanks for the props, Felix! This is a great tutorial on extending jQuery's selection capabilities and is yet another example of how jQuery's extensibility is winning over developers round the globe. I like how you broke out the isTask logic in to its own function. Well done!

Felix Geisendörfer said on Jul 18, 2007:

Lucian: I can see EXT being a good tool for large backend systems that need to somewhat replace/resemble a desktop app. In fact I wish it was around when I joined the big project I'm still working on right now. However, I don't think EXT has good applications for frontend scripting and light-weight backends.

Jippi: Yes, all of the code shown here should be x-browser.

malsup: Hey cool to see you around here, didn't know you where reading my blog : ). I'm glad you like the post and keep up the good work!

brian  said on Jul 25, 2007:

Hey, can you tell us more about your task mgmt. application, is this something you are selling, open-sourcing, what?

Felix Geisendörfer said on Jul 27, 2007:

brian: I'll release more information upon opening the private beta. Meanwhile: There might be a mechanism for obtaining the source but it will not be open source in terms of having a license recognized by the OSF. The app is not planned to be distributed but to be a web service I run.

Giannis said on Feb 04, 2008:

Cool!

Vaggelis said on Feb 04, 2008:

interesting

Elias said on Feb 07, 2008:

Nice

Adamantios said on Feb 08, 2008:

Cool!

Dinshaw said on Sep 27, 2009:

For Rails developers, or anyone not on port 80

obj.hostname.replace(/:3000|:3001/,'')

Lindsay said on Nov 19, 2009:

Haha, Javascript's now my favourite language as well, and that's after having been a Java developer for a decade. Feels...so and light free, like I'm reborn ;)

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.