Accessing data saved in the class property of DOM-elements with Prototype

Every so often, when in JavasScript-code-mode, there's the need to make some decisions based on data that's not available in the document itself. Say your templating engine comes up with the following HTML, which is basically a list of friends linking to their respective profiles;

HTML:
  1. <ul class="myfriends">
  2.     <li><a href="/wolksken">Laura Bogaert</a></li>
  3.     <li><a href="/izewizai">Isaï Persyn</a></li>
  4.     <li><a href="/boogie">Maarten Bogaert</a></li>
  5. </ul>

Now you'd want to mix in some extra-js-fu, by displaying a mini profile when you click any of the links. For that you'll be using your AJAX-API that needs eg. a user-identification integer or string as parameter. How is your JavaScript gonna know what this userID is for each of the listed friends? Right, you need some extra information for this.

Different approaches

There's a few ways of including data you need on a js-application level to your DOM-tree, some of which I list here;

  • Hidden form elements:
    You could nest some forms with hidden elements containing the data you need into your DOM. But I guess for the use case here, that would make your html quite cluttered and you'll have quite some overhead traversing the DOM to find the elements related to the click-event of your link. On the upside though, you can store about any value in those form elements as long as they're properly escaped. Example HTML:
    HTML:
    1. <a href="/wolksken">Laura Bogaert</a><form><input type="hidden" name="userid" value="1043" /></form>

  • Custom attributes:
    Why not just add some custom userID-attributes to the elements you need? It's very readable and flexible, it works in all browsers that support XHTML and - as with form elements - allows for about any type of text string, thus is an excellent solution. And in fact, the X in XHTML is for extensible right? But of course, there's the Standards Police™, who might not like that you're adding new custom attributes that aren't in the original specification, to put you off from using this technique. Example HTML:
    HTML:
    1. <a href="/wolksken" userid="1043">Laura Bogaert</a>

  • Abuse an existing attribute:
    You could hide the fact you actually want to use custom attributes, by just abusing some of the existing ones. Let's for instance take the rel="" attribute; an existing attribute, available on anchors, left mostly unused; if we use it to store a userid in, validators won't complain, so we're all set? Well, quite an ugly hack, isn't it? Not semantic, not always available and you still have no easy solution for key-value pairs. Example HTML:
    HTML:
    1. <a href="/wolksken" rel="1043">Laura Bogaert</a>

  • (Ab)use your class-attribute:
    There is one attribute that does fit for using to save data though, since it's available on every element, flexible enough, and it (can) even make sense on a semantic level. While class names will in 99% of the cases play a purely visual role, you could use them for application logic too. Isn't that what microformats are doing? Since you're using class names, you have to deal with some of it's limitations though; they can't have spaces, and some browsers might not like all characters. Example HTML:
    HTML:
    1. <a href="/wolksken" class="userid_1043">Laura Bogaert</a>

Each of these 4 techniques have their own reasons to exist, and I'm not here to tell you which one is best, or which one you should use. I'm here to share two helper methods I've written for the class names approach.

Extending Prototype with classname data helpers

So, we're adding some extra data to our class-attributes. The templating engine does this in the format of "_".

HTML:
  1. <ul class="myfriends">
  2.     <li><a href="/wolksken" class="gender_male userid_1043">Laura Bogaert</a></li>
  3.     <li><a href="/izewizai" class="gender_male userid_3409">Isaï Persyn</a></li>
  4.     <li><a href="/boogie" class="gender_female userid_8590">Maarten Bogaert</a></li>
  5. </ul>

If we want to access these key-value pairs in JavaScript we go to the DOM-element, loop over it's class names, split the keys and values, and return the value for the queried key. Likewise you might want to have a class data setter method too.

For this I wrote two methods, getClassData(key) and setClassData(key, value), for Prototype, my weapon of choice for JavaScript development. These methods are getters and setters for those key-value pairs, and thanks to Element.addMethods, available on every DOM-element. Here's an example of it's use:

JavaScript:
  1. $$('ul.myfriends').first().observe('click', function(event) {
  2.     var userID = event.element().getClassData('userid');
  3.     alert(userID);
  4. });

Here's the source code:

JavaScript:
  1. /**
  2. * Extend all Prototype DOM-element with getClassData() and setClassData() methods
  3. *
  4. * @author Jurriaan Persyn - http://www.jurriaanpersyn.com
  5. * @version 0.1
  6. */
  7. Element.addMethods({
  8.  
  9.     /**
  10.      * Returns a string stored in the classnames of a DOM-element in the form of "$key$glue$data"
  11.      *
  12.      * @param DOM-element   element id or reference to a DOM-element
  13.      * @param string key the key of the classname
  14.      * @param    string glue optional, default "_"
  15.      * @return mixed_var null or string
  16.      */
  17.     getClassData: function(element, key, glue)
  18.     {
  19.         element = $(element);
  20.         if (!glue)
  21.         {
  22.             glue = "_";
  23.         }
  24.         key = key + glue;
  25.         var data = null;
  26.         element.classNames().each(function(className) {
  27.             if (className.substr(0, key.length) === key)
  28.             {
  29.                 data = className.replace(key, "");
  30.             }
  31.         });
  32.         if (data)
  33.         {
  34.             data = decodeURIComponent(data);
  35.         }
  36.         return data;
  37.     },
  38.    
  39.     /**
  40.      * Stores a string in the classnames of a DOM-element in the form of "$key$glue$data"
  41.      *
  42.      * @param DOM-element element id or reference to a DOM-element
  43.      * @param string key the key of the classname
  44.      * @param string data a string with some data
  45.      * @param    string glue optional, default "_"
  46.      * @param element Returns the element itself to allow chaining
  47.      */
  48.     setClassData: function(element, key, data, glue)
  49.     {
  50.         if (!glue)
  51.         {
  52.             glue = "_";
  53.         }
  54.         element = $(element);
  55.  
  56.         var previousData = element.getClassData(key);
  57.         if (previousData)
  58.         {
  59.             previousData = encodeURIComponent(previousData);
  60.             element.removeClassName(key + glue + previousData);
  61.         }
  62.  
  63.         data = encodeURIComponent(data);
  64.         element.addClassName(key + glue + data);
  65.        
  66.         return element;
  67.     },
  68.    
  69.     _eoo: true
  70.    
  71. });

Here's a seperate file with the script: classdata-extensions-01.js.

The getClassData will always return values as strings. You could build in some type-hinting methods, but you'd have to implement that same protocol in your templating engine too then, so I decided to leave it out here. The data value is now being url-escaped to add it to the classes, but of course there's some limitations there too.

Here's to hoping these 2 methods might provide you some use ...

(delayed) BarCamp Gent 2008 Afterthoughts

Barcamp Gent has been more more than 2 weeks now, but I still felt the need to give some credit to those who did great, interesting, good and/or remarkable presentations.

One of the first talks I attended was the one by Werner Raemakers demoing Channel.TV and talking about the efforts VRT and BBC are making to get their video recordings digitally distributed. It was cool to see how they're experimenting with Apple TV and own linux based set top boxes.

Elise Houard later on did an introduction on OAuth (waf? o-af? open-waf? ... What a nightmare to pronounce this correctly), the open protocol for secure API authentication between web and desktop applications. (You've seen a protocol similar to OAuth, if you've used any application using the Flickr API before.) I fully support this standard, 'cause I'm sure it's more secure and correct than eg. most screen-scraping-based contact-importers used mostly, and it's good to have a standard for API providers, because they're all dealing with the issue of authorization.
I'm still waiting to see some more sites support this standard, though, and I'm also keen to know in which way implementing the OAuth protocol affects the user experience, because I assume navigating away from a site could potentially scare a user maybe even more then asking for a password. I do see the advantages in allowing for partial access (in features - read and/or write - and in time - eg. limited for a week), but I think for most users it comes down to: "do I trust this site or not?", and if they do, will make OAuth or asking for a password of another site really make any difference?
Must add I've been impressed by the recent implementations of 'check who of your whatever-online-addresbook-service are on here too' by Flickr and Dopplr recently.

Later that day Kris Buytaert ranted about the fact (!) the web 2.0 services of today still don't work together the way they could, and should. Why can't all the services aggregate all the information one publishes on all the different sites, and eg. show me the restaurant tips from NotSoSo created by my best Netlog friends for the trip next week I've been planning using Dopplr? Should be possible ... Anyways, we've all definitely got some work to do to make this possible. The discussion took an interesting direction when Pascal Van Hecke spoke about initiatives in further decentralising your online presence, kind of returning to the subjects related to OpenID and OAuth.

Frank Louwers shared his enthousiasm for the Baobab Health project, running in Malawi. In his Africa On Rails talk he spoke about how a small team of developers is using Ruby On Rails to create kiosks that help suggesting the amount of drugs for HIV positive patients. This way not only the 160 doctors of Malawi can help out the 1 000 000 people suffering of AIDS. Their main concerns are power & budget, but in an interview with one of the developers it was interesting to see how they appreciated and disliked the same things in Ruby as local Ruby developers, albeit in totally different circumstances. Definitely check out baobabhealth.org.

My last session for the day was another demo, given by Bart Claeys. He and a friend did a quick tour through the most important features of Adobe Lightroom. I hope a demo of Apple's Aperture is up next, so I can decide where to start managing the photos of the newly bought DSLR (Wooha!).

I left with a whole bunch of ideas, new people to follow on Twitter and lots of inspiration. Thumbs up for Thomas Bouve, he organised a great BarCamp edition (for the very first time in my hometown, yeey!).

More info and linkage at: http://barcampgent.wikispaces.com/.

Get Your Frontend Sorted

After a hectic weekend (and an even more hectic start of the week, thus the delay), here's the slides, video and some thoughts for the presentation I did with Lennart Schoors at last saturday's BarCamp Gent. It's a bunch of thoughts, experiences and stuff we learnt while working on the frontend for Netlog. While that frontend's far from being a finished and fully optimized product, we think there's somehow a few tips we could share;

Lots of thoughts were crammed in this 20 minute session, so I hope we didn't overwhelm too much ... Credit where credit is due, so a shout out to Lennart, to the Yahoo Performance team and Steve Souders for stressing the importance of frontend performance, to Firebug & YSlow! for being indispensible tools, and to our colleagues for making Netlog an inspiring to work for.

There's a video recording of the presentation too, thanks to Luc Van Braekel. I wouldn't watch it, though.

@media ajax 2007, london (day two)

Phew! Back home from the 5 day London trip now ... What can I say? Damn what a city! Gent feels like no-man's-land now.

Day two of the @media ajax conference was definitely as interesting and inspiring as day one. It was much more on a technical level with some really neat talks by Dan Webb, Alex Russell (Dojo), John Resig (jQuery Demo) and Douglas Crockford (JSLint), a hardcore session by Brendan Eich and an entertaining panel to finish the two days of pure javascript shizzle. The conference being so specialised (in topics as well as in audience), definitely made for an interesting two days without too much useless introductary mumbo jumbo.

Meanwhile I've found these resources: the first slides popping up at slideshare and some thoughts by James on the individual sessions. Apart from that not much popping up at the Technorati-pages yet. Here's some of my own pictures from London, with a photo or two from the conference. As said in my previous post on day one, I hope to be sharing my notes and thoughts with you soon ...

A big thank you to Netlog for getting me at this conference. Up to me to share the knowledge now, :).

Unserializing stored $_SESSION data in PHP

While improving the way we handle and store sessions for the *.facebox.com websites we needed a way to access, read, change and store saved php-sessions (we keep them in a memory cache). Apparently stored session are not simple serialized strings of the $_SESSION-object *. Well, almost, but not exactly. So we wrote our own ... Or at least: Had to write our own.

The following function unserializes encrypted session data and returns an object representing the $_SESSION object. A serialized $_SESSION object differs from a normal serialize () in that each key of the $_SESSION-object/array is seperated by | from it's (usual php-)serialized value.

PHP:
  1. /**
  2. * This function unserializes a stored session. This serialisation is slightly different then the
  3. * php-serializer (each key of $_SESSION is seperated by a |).
  4. *
  5. * @param  string    $serialized_string  a serialized representation of a session object
  6. * @return mixed_var                     an unserialized object
  7. */
  8. function unserialize ($serialized_string)
  9. {
  10.     $object = array ();
  11.     $buffer  = $serialized_string[0];
  12.     $open    = 0;
  13.     $openAcc = 0;
  14.     $key     = '';
  15.  
  16.     for ($i = 1, $count = strlen ($serialized_string); $i <$count; $i++)
  17.     {
  18.         $curChar = $serialized_string[$i];
  19.        
  20.         if ($curChar == '|' && $open == 0)
  21.         {
  22.             $key = $buffer;
  23.             $buffer = '';
  24.             continue;
  25.         }
  26.         elseif ($curChar == '{' && $serialized_string[$i-1] == ':' && $open == 0)
  27.         {
  28.             $openAcc ++;
  29.         }
  30.         elseif ($curChar == '}' && $open == 0 && $openAcc> 0)
  31.         {
  32.             $openAcc --;
  33.             if ($openAcc == 0 && $serialized_string[$i-1] == '{')
  34.             {
  35.                 $object[$key] = array ();
  36.                 $buffer = '';
  37.                 continue;
  38.             }
  39.             elseif ($openAcc == 0 && $serialized_string[$i-1] == ';')
  40.             {
  41.                 $object[$key] = unserialize ($buffer . $curChar);
  42.                 $buffer = '';
  43.                 continue;
  44.             }
  45.             elseif ($openAcc == 0 && $serialized_string[$i-1] == '}')
  46.             {
  47.                 $object[$key] = unserialize ($buffer . $curChar);
  48.                 $buffer = '';
  49.                 continue;
  50.             }
  51.         }
  52.         elseif ($curChar == '"' && $serialized_string[$i-1] == ':' && $serialized_string[$i+1] != ';')
  53.         {
  54.             $open++;
  55.         }
  56.         elseif ($curChar == '"' && $serialized_string[$i+1] == ';' )
  57.         {
  58.             $open--;
  59.         }
  60.         elseif ($curChar == ';' && $open == 0 && $openAcc == 0)
  61.         {
  62.             $object[$key] = unserialize ($buffer . $curChar);
  63.             $buffer = '';
  64.             continue;
  65.         }
  66.         $buffer .= $curChar;
  67.     }
  68.    
  69.     return $object;
  70. }

The function we wrote should do the same as other attempts at writing such a function; as you can find here. But which one is the most efficient?
It is different from these or these other attempts in that it can handle ';' or '|' in values (php serializing doesn't escape those characters).

We opted not to use session_decode () and session_encode () for temporary overwriting of the $_SESSION object (like this solution), because of security reasons.

Still this feels like it's more of a work-around because we're mimicking existing functionality. Who guarantees our function does exactly the same as the way php serializes the sessions internally? All seems to be going fine though, we haven't seen any problems with the sessions related to this function on our sites yet, and if you look at the amount of members we serve on the Facebox sites it feels like a pretty stable and robust solution.

Should add that credit for this algorithm goes out to Toon Coppens.

* Does anyone know why? I will have to remember to ask the mySQL consultant Kristian Köhntopp, that'll be helping us out with mySQL soon. I'm told he's one of the peeps who introduced the sessions-concept in php3. Look forward to learning from him ...

Update (2007-01-07): Fixed a bug in handling empty arrays.
Update 2 (2007-01-09): Fixed a bug in handling nested arrays.