November 11th, 2005

Graceful Degradation with Prototype, Scriptaculous and Ruby on Rails - Part 2: The Tools Of The Trade

22 comments on 1743 words

NOTE: This is part two of a three part series entitled ”Graceful Degredation with Prototype, Scriptaculous and Ruby on Rails”, part one can be found here.

The problem is also the solution

In the first part of this series I showed a couple of examples of problems that we’ll be facing along the way. Just as we have powerful libraries such as Prototype and Script.aculo.us to help us easily implement Javascript based effects and Ajax, we also need a tool that helps us degrade this functionality. As strange as it may seem, Javascript is both the problem and the solution. One script that was released no to long ago called Behaviour was a great first step in providing an answer.

The promise of Behaviour.js

When Ben Nolan first released Behaviour.js to the community, it looked promising and garnered some well deserved attention. To my knowledge it was one of the first attempts to provide an abstract enough solution to a huge problem.

Behaviour was designed to address the problem with event handlers being placed into html tags, and 95% of the examples show how to handle onclick events. It’s power goes beyond that, but it’s still not designed to handle more complex examples that almost any Ajax based application will face.

Matching against CSS selectors is an extremely useful approach and the learning curve for Behaviour.js is as about a straight line as you can get it if your familiar with CSS. However, we need something with more power, or as a frightened Java developer might put it ”It doesn’t scale”. While I’m certainly not knocking Ben’s efforts with Behaviour and I commend him for the work he’s done on it, but for complex examples I don’t see it being as attractive.

Behaviour passes the element that triggered the event to the properties you define. This works fine when that’s the node you want to deal with, but it starts getting tedious when that node can be any place in the DOM at any given time. Every element in a document is unique and can contain a unique identifier by means of it’s id attribute. The id attribute of a DOM node should be thought of as a primary key if we were talking about a database. In this case, the document is our database and the elements within that document are our records with their unique id.

Using ID’s as instructions for our behavior layer

In a valid XHTML document, there is only one thing we can (or hope we can) rely on to be unique and that is an elements id attribute. We can think of an id in the dom like a primary key in the database, it should always be unique. In our case, it also needs to describe what our behavior layer should do. Here is an example of what this might look like:


<ul id="blurbs">
<li id="blurb229">
  <div>
    <h3>A lovely title</h3>
    <p>You should probably delete me from the database.</p>
    <div class="actions"><a href="/blurbs/destroy/229" id="remove-blurb229">delete</a></div>
  </div>
</li>
</ul>

This could be considered a pretty standard example of what you could be facing. Pay close attention to the descriptive naming conventions in the id’s. The ul has an id of blurbs, it’s children which are what we are really working with here have a singular name mixed with the object id that would be stored in a database for this object–blurbs229. 229 is the magic number here, the blurb prefix is merely a namespace for the object we are working with, and also, valid ids can not start with a number. Finally, the a element which fired the event has our instruction which tells our behavior layer what to do, in this case remove-blurb229. Perhaps a chart will paint a clearer picture.

Html-Dbase-Relation

Now that we’ve laid this out in html, we need a viable tool to help us tie it all together and make it work.

The Degrader tool: Degrader.js

Download Degrader.js

I’ve tried to comment the code in Degrader.js as much as possible, and hopefully that will get you up to speed on what it’s doing and how to use is. It’s still very much in the first stages of development so if you have any suggestions on improvements or questions then please fire away with a comment and I’ll try to answer any questions. If this leads to simpler and more flexible tools then I couldn’t be happier. Now with that said, lets get a brief run down of how it works.

At the heart of Degrader.js are Regular Expressions, although not the easiest things to learn they are by far the most powerful and flexible to work with and since we need the ability to describe almost anything, Regular Expressions were a prime candidate.

Degrader.js works by matching rules you define against a dom elements id. If a match is made, Degrader.js will trigger the callback onMatch and pass the complete rule and the element belonging to the id that was matched. Ok, that was a mouth full, lets look at some code.


var rules = $A([
 {
   element: 'a',
   pattern: /^toggle-([\-a-z0-9]+)/i,
   onMatch: function(element, rule) {
     var target = Degrade.X(element, rule.pattern, 1);
     Event.observe(element, 'click', function(){Element.toggle(target)});
   }
 }]);

new Degrade.Degrader(rules);

In the above code snippets, I have 1 rule which is used to toggle an elements visibility. Degreader.js will parse the DOM and would detect all a elements whose pattern matched what we specified in our code.


<!-- Matches -->
<a href="#" id="toggle-item23">Show Item 23</a>
<a href="#" id="toggle-magicitem43">I'm Magic</a>

<!-- Not matched -->
<a href="#" id="toggle3232">I won't be matched</a>
<span id="toggle-item42">I also will not be matched</span>

So the first two elements would trigger onMatch and execute the function you define for it, in this case it would be the code below.


function(element, rule) {
     var target = Degrade.X(element, rule.pattern, 1);
     Event.observe(element, 'click', function(){Element.toggle(target)});
   }

As you can see, the function you provide should accept the element which triggered the match and the complete rule equipped with the pattern or any other properties you wish to define. Simple enough right?

I’m going to leave it here for now, but in part 3 we’ll build a sample Ruby on Rails application that will tie everything together and apply what I’ve been talking about in a sudo-real-world application.

Discussion

  1. ceejayoz ceejayoz said on November 13th

    Oooooooh…

    Very cool.

  2. Argo Argo said on November 15th

    Very smart!

  3. Waylan Waylan said on November 15th

    Although slightly off topic, I can see some complain that they then need to define both a class and id for every list item. After all, they want every li to be styled the same – which requires a class.

    Obviously doing ”#blurbs li” in your css would do the trick, but sometimes the HTML is not so clear cut. The fact is you can just use the id as I dicussed way back in April of 2004 here: http://achinghead.com/archives/14/css-21-tricks/

    Only thing is, I’m not sure how well it works cross-browsers. Still worth exploring.

  4. Zenstyle Zenstyle said on November 19th

    I’m sorry but you’re not actually deleting records from your db with: a href=”/blurbs/destroy/229” are you?

    That’s not just a ruby on rails no-no, it’s a no-no period.

  5. And I left my email on accident And I left my email on accident said on November 19th

    That’s a web no-no hehe.. remove the previous posts email please

  6. Fruitbatinshades Fruitbatinshades said on November 30th

    What if you are using IDs already like in asp.net?

  7. Justin Palmer Justin Palmer said on November 30th

    You can modify it to use class names. .NET does some horrendous things with code, so it’s hard to accommodate for that. Also, I think you can change id’s in .NET, no?

  8. Justin Palmer Justin Palmer said on November 30th

    ZenStyle, your right, it’s not safe to use GET request for things such as this but I was mainly concerned with demonstrating how to use the tool.

  9. Cully Larson Cully Larson said on December 7th

    Why is it unsafe to use GET requests to remove stuff? It isn’t any safer to use POST. If someone was determined to hack your site, they could send any of the same data via POST as they could via GET. Though, it is somewhat of a standard to use POST to modify data and GET to retrieve, it doesn’t make it unsafe to use either for either purose.

  10. Justin Palmer Justin Palmer said on December 8th

    Cully, link prefetchers like Googles web accelerator can wipe out a database if your using GET request for destructive operations.

    It’s not about hacking, a user with good intentions using a prefetcher could unknowingly delete a lot of important information.

  11. ZenStyle ZenStyle said on December 17th

    Stopped by your page again justin…. sorry for hasslin ya about the destructive get, and not immediately complimenting your innate ability to write good meat and potatoes web development articles…

    Honestly I think gracefully degrading in some parts isn’t really necessary, if the user turned off javascript he’s most likely not using some retarded browser, he very much “disabled” his javascript… telling him to turn it back on suffices for the most part. GMail requires not only javascript but cookies.. we’re all modeling after them but no ones attacking them for that.

  12. Justin Palmer Justin Palmer said on December 18th

    Hey, no problem! I need you guys to keep me on my toes anyway.. ;-)

  13. Percy Percy said on December 20th

    I know I’ve often wanted to log into a system and delete something or create something, and the only browser I have available to me at the time is lynx or some other text-based, non JS browser. I know this doesn’t happen too often, but honestly, it’d be nice to have a fallback for most of everything. Great article, I like where this is going. I’m going to try to come up with some crazy ideas on how to improve this method.

  14. Zenstyle Zenstyle said on December 21st

    I don’t see any problem with creating a limited interface for limited browsers, I’m just saying expect it to be limited, I’m not trying to impress my users using lynx… and hell if I’m gonna support them either.. I’m a unix programmer and even “I” don’t do that.. you’ll actually waste more time trying to fill out a form in lynx where your left button will throw you back out of the page or the contrast barely tells you where one cell starts and the other stops to for instance, downloading a couple megs from mozilla.. or better yet… put the portable version on your usb stick drive

    What we need to do is ATTACK idiotic user behavior which makes our jobs much more difficult then it has to be. then to constantly pile on more layers of idiot guard… Not only do I have to deal with the users I have to deal with the browser, the os, the tools, the spiders, the oh god.. I’ve got expert programmer friends who have left this scene for this reason.

  15. Brendan Baldwin Brendan Baldwin said on January 12th

    A couple of issues with using the “id” attribute is that to be valid esp XHTML Strict (if you’re interested), id should only ever contain alpha-numeric characters and underscores: not hyphens.

    Additionally, you should consider that the “id” field is the only field indexed in the DOM for use by getElementByID, which makes it a useful handle not to be spent on this function if you plan on doing other ajaxy things with the link tag.

    I actually use the “rel” attribute to achieve this functionality. It also has NO limitations on allowable content which means I can put in much a more explicit domain-specific-language to the task.

    For example, you could have links like this in table column headers for sorting:

    <a href=”?sort=zipcode” rel=”sort(zipcode)”>Zip</zip>

    This allows a non-ajaxy querystring request or an ajaxy “expression” indicating I want this link to sort the table on the zipcode column based on the click.

    Etcetera etcetera.

    :-)

  16. Brendan Baldwin Brendan Baldwin said on January 12th

    Oh I forgot to mention the best part. I don’t need to register any behavior with that specific anchor tag element. Instead I just say this:

    function something_clicked()
      {
      if(typeof(event)!='undefined')
        {var $event=event?event:arguments[0];}
      else
        {var $event=arguments[0];}
      var $element=$event.target?
        $event.target:$event.srcElement;
    
      var $tag=$element.tagName;
      var $rel=$element.attributes.rel?
        $element.attributes.rel.value:"";
      alert('tag='+$tag+' rel='+$rel);
      }
    
    document.onclick=something_clicked;
    

    Note the use of $event in the function above is done because you can not use ‘var event’ due to the case where it will exist already (it does in IE and in Opera, but not in Mozilla/Firefox.) So this is a workaround. Generally i use $ to prefix local variables in js functions as a convention, which nicely sidesteps the issue for many similar problems.

  17. hellekin hellekin said on January 24th

    I bumped into the problem of selecting-a-selector. I found that CSSQuery does a good job at matching complex DOM element combinations. Ron Lancaster made an alternative Behaviour.js using Dean Edward’s cssQuery that proved to fit my needs better. Prototype + Behavior to keep HTML clean as all javascript can do in the HEAD tag.

  18. hellekin hellekin said on January 24th

    I bumped into the problem of selecting-a-selector. I found that CSSQuery does a good job at matching complex DOM element combinations. Ron Lancaster made an alternative Behaviour.js using Dean Edward’s cssQuery that proved to fit my needs better. Prototype + Behavior to keep HTML clean as all javascript can do in the HEAD tag.

  19. Brad Brad said on April 3rd

    If the purpose of removing onclick attributes is to clean up the HTML so that it only deals with structure, then I really do see how this:

    <a href=”?sort=zipcode” rel=”sort(zipcode)”>Zip</zip>

    is any better than this:

    <a href=”?sort=zipcode” onclick=”sort(zipcode)”>Zip</zip>

    It’s just less semantic, is all. For that matter, I don’t really this this:

    <a href="/blurbs/destroy/229" id="remove-blurb229">

    as being such a huge improvement on this:

    <a href="/blurbs/destroy/229" onClick="remove-blurb('229')">

    or, if it needed an id anyway, this:

    <a href="/blurbs/destroy/229" id="A229" onClick="remove-blurb(this)">

    I mean, the anchors with the onClick handlers as I have them are about the same length (O.K., slightly longer), they are more semantic, and they require much less JavaScript code.

    I know it is all the rage to remove all onClick handlers, but if we just replace them with other attributes that do the same thing but are less clear and require a larger JS download, then what is the point?

  20. Jeremy Jeremy said on April 28th

    Brad:

    A more annoying example that’s no-bueno for spiders & W3 Validation; is example onClick code required in a prototype.js effect (script.aculo.us, Rico, etc…) :

    <a href="#" onclick="$('stepMain').style.display == 'none' ? new Effect.BlindDown('stepMain', {duration:0.4}) : new Effect.BlindUp('stepMain', {duration:0.4}); return false;; return false;">Open / Close</a>

    There are a million methods to make the code cleaner if I had all the freedom of starting from scratch. However, when I’m developing an entirely new site using a framework like ruby, most of this AJAX-related javascript code is generated modularly with a function. So before running off and incorporating it with ruby, you just need to make sure you’re investing your time in the right option.

    I like this RegExp idea, I just think there are better ways of utilizing it to take care of this problem… maybe it can consist of a single ‘JS’ file that includes some kind of configuration file that links ids, to events, to functions. Then you have a regular expression engine that reads the config file and defines onClick events and functions.

    Using something like that will keep all un-needed code out of our HTML pages, and stay valid at the same time.

  21. balupton balupton said on July 10th

    Jeremy:

    I don’t see how that response to brad is any better, you could just make that onclick code a function, and do thefunction();”[/code]

  22. balupton balupton said on July 10th

    Jeremy:

    I don’t see how that response to brad is any better, you could just make that onclick code a function, and do
    <a href="#" onclick="return thefunction();">sadasd</a>

    (Sorry about the double post, I can’t read ;))

Sorry, comments are closed for this article.