Unobtrusive & Persistant Script.aculo.us Effects

Nov 01 2005 10:07 PM

Update

Thanks to some pointers from Kyle, I've updated the code to use some of the functions built into Prototype (such as '$" (Dollar), addClassName, & removeClassName) and to use Behaviour's addLoadEvent. I also created a more generic example that isn't quite as specific to this site.

As part of the design of this site, I'm using the script.aculo.us library to make the boxes in the sidebar expand and contract when you click them. The important part to me was to make the effect unobtrusive (no extra markup in the HTML) and keep the position of each box persistant from page to page.

This turned into quite a long post that can really be divided into two sections:

  1. The Box creation and markup.
  2. Makin' the script.aculo.us magic.

The Box

First let's take a look at some example HTML markup.

  1. <div id="recent">
  2.   <h3>Recently</h3>
  3.   <ul id="recent-body">
  4.     <li>Recent Post 1</li>
  5.     <li>Recent Post 2</li>
  6.   </ul>
  7. </div>

A div with an id, a header, and an unordered list with an id - that's it for each box!

Now think of the box in 3 sections: the top (the header of each section), the middle (content), and the bottom.

Top:
The header background actually contains 2 box tops for the different positions, one with a minus (-) and one with a plus (+).
H3 Background

Look at the measurements for CSS purposes. The width of each box is 160px and height is 21px.

H3 Background with measurements

Notice in the CSS that the height + padding-top should equal 21px and the width + padding-left should equal 160px. Also, set the cursor to a pointer so the user knows it is clickable.

  1. #sidebar h3 {   
  2.   background: url(i/sidebar-h3-bg.jpg) no-repeat 0 0;
  3.   margin: 0;
  4.   cursor: pointer;
  5.  
  6.   /* height + padding-top = 21px */
  7.   height: 14px;
  8.   padding-top: 7px;
  9.  
  10.   /* width + padding-left = 160px */
  11.   width: 153px;
  12.   padding-left: 7px;
  13. }

Create a CSS class to use for when the box is contracted and you need to show the other box top (the one with the plus). I use .invisible. Measure how many pixels it is the the beginning of the second box. In my example, it's 165 pixels.

  1. #sidebar h3.invisible {
  2.   background: url(i/sidebar-h3-bg.jpg) no-repeat -165px 0;
  3. }

Middle:
The content needs to have a border on each side with a little padding to seperate the text. I threw in a little gradient background for kicks as well. I use unordered lists for all the content in my sidebar, but with a few changes you can make the middle section whatever you like.

  1. #sidebar ul {
  2.   background: url(i/sidebar-ul-bg.jpg) no-repeat 0 0;
  3.   border-left: 1px solid #A6B6C3;
  4.   border-right: 1px solid #A6B6C3;
  5.   padding: 4px 2px 0 2px;
  6. }

Bottom:
I designed the bottom of the box to "fit in" with the top when it's contracted.
Sidebar div background

Position the bottom of the box at the bottom of the surrounding div. Set the padding-bottom equal to the height of the box bottom image.

  1. #sidebar div {
  2.   background: url(i/sidebar-div-bg.jpg) no-repeat bottom left;
  3.   margin: 2px 0 10px 0;
  4.   padding-bottom: 5px;
  5.   border: none
  6. }

Files you'll need

Download the following and extract all the JavaScript files into a folder on your webserver. I like to keep all of my JavaScript files in a folder called "js."

  1. Scriptaculous Library
  2. Prototype JavaScript Framework
  3. Behaviour

Makin' the magic

Look again at the HTML for the "Recent Posts" box:

  1. <div id="recent">
  2.   <h3>Recently</h3>
  3.   <ul id="recent-body">
  4.     <li>Recent Post 1</li>
  5.     <li>Recent Post 2</li>
  6.   </ul>
  7. </div>

Think about it logically. We need to listen to each h3 element in the sidebar for the onclick event. Then when one is clicked, see if that h3 element has a class name of "invisible." If it does, then we'll run the scriptaculous effect "BlindDown" to bring it back into view and set the class to empty; if the class is already empty, we'll run the "BlindUp" effect to hide it, then set the class to "invisible." Easy enough right?

You can read the Behaviour instructions, but basically it allows you to create "rules" that specify elements to add JavaScript events to. This means that we don't have any JavaScript code in our HTML markup - totally unobtrusive baby! So whip out your favorite JavaScript editor and let's get to work!

Here's what the JavaScript "rule" looks like:

  1. '#recent h3' : function(el){
  2.   el.onclick = function(){
  3.     if (Element.hasClassName(this, 'invisible')) {
  4.       new Effect.BlindDown('recent-body');
  5.       Element.removeClassName(this, 'invisible');
  6.       setCookie(this.parentNode.id, '', 365);
  7.     } else {
  8.       new Effect.BlindUp('recent-body');
  9.       Element.addClassName(this, 'invisible');
  10.       setCookie(this.parentNode.id, 'invisible', 365);
  11.     }
  12.   }
  13. }

We're setting the onclick event for #recent h3 unobtusively. If it has the class name invisible, run the BlindDown effect; if it doesn't then run the BlindUp effect. Notice the Element.hasClassName, Element.removeClassName, and Element.addClassName. These are custom Prototype functions that'll work with multiple class names.

Create a new JavaScript file to store the code for your rules. Here's an example with two "boxes":

  1. var theRules = {
  2.   '#box1 h3' : function(el){
  3.     el.onclick = function(){
  4.       if (Element.hasClassName(this, 'invisible')) {
  5.         new Effect.BlindDown('box1-body');
  6.         Element.removeClassName(this, 'invisible');
  7.         setCookie(this.parentNode.id, '', 365);
  8.       } else {
  9.         new Effect.BlindUp('box1-body');
  10.         Element.addClassName(this, 'invisible');
  11.         setCookie(this.parentNode.id, 'invisible', 365);
  12.       }
  13.     }      
  14.   },
  15.   '#box2 h3' : function(el){
  16.     el.onclick = function(){
  17.       if (Element.hasClassName(this, 'invisible')) {
  18.         new Effect.BlindDown('box2-body');
  19.         Element.removeClassName(this, 'invisible');
  20.         setCookie(this.parentNode.id, '', 365);
  21.       } else {
  22.         new Effect.BlindUp('box2-body');
  23.         Element.addClassName(this, 'invisible');
  24.         setCookie(this.parentNode.id, 'invisible', 365);
  25.       }
  26.     }      
  27.   }
  28. };
  29.  
  30. Behaviour.register(theRules);

Simply create all the rules you need based on your HTML markup, then call Behaviour.register(Your Rules Variable) at then end.

Bakin' Cookies

Notice the setCookie function in there? We need to be able to read and set a cookie to store the each box's status (expanded or contracted) from page to page so add the following functions under your rules. Code adopted from here:

  1. function setCookie(name,value,days) {
  2.  if (days) {
  3.    var date = new Date();
  4.    date.setTime(date.getTime()+(days*24*60*60*1000));
  5.    var expires = ";expires="+date.toGMTString();
  6.  } else {
  7.    expires = "";
  8.  }
  9.  document.cookie = name+"="+value+expires+";path=/";
  10. }
  11.  
  12. function readCookie(name) {
  13.  var needle = name + "=";
  14.  var cookieArray = document.cookie.split(';');
  15.  for(var i=0;i <cookieArray.length;i++) {
  16.    var pair = cookieArray[i];
  17.    while (pair.charAt(0)==' ') {
  18.      pair = pair.substring(1, pair.length);
  19.    }
  20.    if (pair.indexOf(needle) == 0) {
  21.      return pair.substring(needle.length, pair.length);
  22.    }
  23.  }
  24.  return null;
  25. }

Persistance Is Key

Now that we're storing the position in a cookie everytime the user clicks a box, we need to read those values when each page is generated in order to maintain each box's status as the user browses the site.

Create a variable boxIds that uses the $ (dollar) function with the id name of each div (box) that needs to be checked. The hideBoxes() function will check each cookie and "position" the boxes accordingly.

  1. function hideBoxes() {
  2.                
  3.   // Id names of all the "boxes"
  4.   boxIds = $("box1","box2");   
  5.  
  6.   for (i = 0; i <boxIds.length; i++) {
  7.     if (boxIds[i]) {
  8.       cookieValue = readCookie(boxIds[i].id);
  9.       if (cookieValue == 'invisible') {
  10.         var h3 = boxIds[i].getElementsByTagName('h3');
  11.         Element.addClassName(h3[0], 'invisible');
  12.         var kids = boxIds[i].childNodes;
  13.         for (j = 1; j <kids.length; j++) {
  14.           if (kids[j].id) {
  15.             Element.hide(kids[j]);
  16.           }
  17.         }
  18.       }
  19.     }      
  20.   }     
  21. }


Last but not least, make sure the hideBoxes() function runs when the browser window loads. Behaviour includes a load event already, so you can just use that.

  1. Behaviour.addLoadEvent(hideBoxes);

Final Thoughts

A lot of the code in this example is specific to this site, but the idea and framework is universal and easily adaptable to any site. There's a more generic example which you can view the source of to see how it works.

I really just wanted to show that it's pretty easy to add cool JavaScript effects from the script.aculo.us library to your site without mucking up all your HTML code.

43 ResponsesAdd yours

wow

you’ve made excellent use of those unobtrusive scripts. I really, really like that!

Great post Tony! Lots of helpful info for anyone wanting to add JavaScript effects to their site.

Thanks guys — There wasn’t too much documentation on this stuff so I figured someone may find what I learned useful…

[…]  Shrinking sidebar boxes […]

Actually, you’ve got a bit of a problem in some of your scripts.

You should never check against className in particular because it doesn’t tell whether an element has that clas. For example, this would not get indexed by your rules:

<div id=”recent><h3 class=”invisible alt”>This isn’t right</h3></div>

You should always use a regex to check for class names and add class names, I usually add helper functions HasClass AddClass and RemoveClass to do this.

Also, note that Behaviour comes with a Behaviour.addLoadEvent that lets you circumvent the need for custom functions.

One last thing… using prototype’s $ you can reduce a lot of unnececary code. Instead of creating an array of id’s and cycling through them, use the dollar sign function: $(’id1′, ‘id2′, ‘id3′). This will return an array of objects.

I’ve been using these methods for months now - and I can tell you they’re amazing. I love Behaviour especially. Good work, looks like you’re starting off on the right foot.

This is very nice use of the script. Might implement this some where sometime, eventually, but the problem is that some people might go over board with all the effects on the webpage.

Nice tutorial, but take heed what Kyle said, the addClass, hasClass and removeClass functions are great.

Very nice. Prototype has a built in Element.toggle() effect, but that only affects the display property of an object - none or block. It would be great if there were Effect.toggle() functions - like moo.fx - built into script.aculo.us.

Thanks for the info Kyle.

You’re right about checking against the className, but at the time .invisible was the only class I was using. I’m working on updating it so it’ll work with multiple class names though.

I didn’t even think to use the Behavior addLoadEvent - I’ll add that.

I just recently learned more about the “Dollar” ($) function and will look into adopting it as you suggested as well.

Really interesting reading. Have to test this…

Tony- I’m coding a site with a sidebar similar to yours using scriptaculous as well. My sidebar boxes use a toggle widget, so I don’t need the “unobtrusive” portion, but am interested in creating a persistence cookie. How might I strip down your code to achieve this?

I’m doing a pretty generic “onclick=”Effect.toggle”, so I imagine I need to set the cookie based on that click somehow. I imagine it wouldn’t require Behavior either.

If we ahve like 10 sidebar items that needs to be toggled. we need ten # box h3 functions. Is there a way to use a for loop and cycle through each one.

I notice, that when i collapse all divs on your menu, and refresh the page, they do get back to the state i left them (cookies working there) but is there a way to not draw them fully displayed in the loading stage, before you toggle?

I am trying to do a verticle bar, that sweeps from side to side, and challenged by the same problem. using display:none seems to disable toggle() for future use, even though i renable display:inline after onload().

just curious if you have tried any workarounds?

dfense - I see what you’re saying, but I haven’t tried to find a “workaround” because it would limit accessibility.

If you hide sections of a page while it’s being rendered and the user has javascript turned off, the user won’t be able to make those sections visible. By using javascript to “hide” the boxes after the page loads, you can be sure that the user will be able to re-enable them, or they won’t be hidden at all if javascript is turned off.

I’m not sure why the toggle() function wouldn’t work in your case though… Have you tried using display: block; instead of display: inline;?

haven’t tried block level display. another suggestion was to remove the initialization of effects (of divs) from onload() and put inline right after divs are declared in html.

I’ll poke at both of those approaches.

i am releasing a wordpress theme that incorporates your techniques. thanks for the awesome tutorial.

(theme at http://area51.wdanielryan.com/themes/platinum)

A re-write using http://encytemedia.com/event-selectors/ would perhaps be a worthwile excercise :)

hint: DOMContentLoaded event
See http://dean.edwards.name/weblog/2005/09/busted/
I’ve used it in several places to alter content / display before the page gets rendered.

Noticing that the cookie save function you use in this example doesn’t appear to work for IE6 — it’s able to save the name, but no value.

However, they appear to work on your site. Have you since updated the function from this tutorial to save the cookies in a way that works with IE6?

I’m checking my code against this example and the code in your site and I’m still not entirely sure why I can’t get my code to work in IE.

If I figure it out, I’ll be sure to email you the results but in the meantime, any help is definitely appreciated.

Cheers, and thanks for sharing this. It has been quite useful.

Thank you so much for this article! You just saved me a lot of time at work. Very well done tutorial with clean, clear examples. I just bookmarked your blog.

I have also noticed my tests fail to respect the cookie in IE 6 and yet this site works fine (FF is fine). There must be a subtle difference in the code base somewhere.

Anybody else?

looking for information and found it at this great site.

I love this site. Good work…

Check out http://www.planjam.com/date.php

The site does an excellent job with many aspects of the script.aculo.us library.

looking for information and found it at this great site.

perfect site good information, very nice news and etc… tnx

I love this site. Good work…

i try to find something at google.com and take it on your site…thanks

thank you for your work

Very needed information found here, thank you for your work

I love this site. Good work…

Perfect pages… tnx

Perfect pages… tnx

I love this site. Good work…

Perfect pages… tnx

thank you for your work

Thanks Tony. Just what I was looking for :-)

Any news on the updated script mate?

[…] 不過在 HTML 裡面寫一堆 inline script ,不僅看起來不爽,維護起來也麻煩,於是我發現了這個:Behaviour ,它可以讓你用 CSS selector 指定 JavaScript 行為,例如指定 id 為 AAAAA 的物件在 onclick 時做什麼事,實在是非常實用。這裡有篇關於結合兩者的淺顯教學:Unobtrusive & Persistant Script.aculo.us Effects。 […]

Great work.
How Do I start with everything closed?

Hi,

The effect works but my issues is the cookie is not setting itself? When I go to a new page the navigation goes back to it’s previous state.

Thanks,
Seth

oiupoipoipoipo poi poi po poi poi poi po poi po

Hi Tony,

Newbie here…

Thanks for this tutorial. I’ve got things working on my own site, but the speed at which the BlindUp and BlindDown happen is different than yours. Mine happens much slower. Is there a way to control this?

Thanks.

Stephen,

You can change the duration time by simply adding the duration tag to your code.

Effect.SlideDown(’d1′,{duration:1.0}); return false;

Change the duration in seconds:

1.0 = 1 Second

5.0 = 5 Seconds

Hope that helps!

Thanks,

Seth

http://www.re-volvemedia.com

You must be logged in to post a comment.