Sneaky Abstractions

Subscribe to my Feed, follow me on , recommend me on Working With Rails or see my code on GitHub

Unobtrusive Ajax patterns: revealers

Posted on July 15, 2008 08:18 Tagged with unobtrusive, javascript, ajax patterns.

The revealer pattern has two components: The revealer and the target. This can be a one-to-many or even many-to-many relationship, but the core functionality consists of the revealer element showing a target element when an event happens. It’s a very simple but yet versatile pattern.

The goal in this little tutorial is to be able to mark elements as either revealers or targets by using class names. We want to be able to write this HTML,

<h3 class="revealer reveals:more-info">More information</h3>
<p id="more-info">
  This is the extra information.
</p>

and it will automatically hide the “more-info” element until the revealer (the h3) is clicked.

First, we find all elements that have the class name "revealer":

$$('.revealer').each(function(revealer){
  //...
});

Then, we extract the target element IDs from the class attribute and find the corresponding elements in the DOM:

$$('.revealer').each(function(revealer){
  var className = revealer.readAttribute('class'),
      re = "reveals:([^ ]+)";//We need both a "normal" and a "global" version

  //Find the target elements for this revealer
  var targets = (className.match(new RegExp(re, 'g')) || []).map(function(str){
    return $(str.match(new RegExp(re))[1]);
  });

});

We search through the class attribute with a regular expression. Note that we use both a “normal” and a “global” version of it. The global version is used to find all occurrences, which are fed into a map where the ID part (after “revealer:”) is extracted. The ID is used to find the element in the DOM.

Now that we have the revealer and the targets, we just need to add some initial class names to them and attach an event handler to the revealer element which toggles some class names on the targets:

$$('.revealer').each(function(revealer){
  // [...]

  //Add the initial class names
  revealer.addClassName('activated');
  targets.invoke('addClassName', 'revealable');
  targets.invoke('addClassName', 'concealed');

  revealer.observe('click', function(e){
    //Not all elements have a default action,
    //but we stop it to be on the safe side
    e.stop();
    targets.invoke('toggleClassName', 'revealed');
    targets.invoke('toggleClassName', 'concealed');
  });
});

We add the initial class names to be able to target them in the style sheet. When done this way, the CSS rules will only be applied if the user has JavaScript running. The targets have an initial class name “concealed” which is used to target them in the style sheet:

/* The cursor will be shown only when JS is available */
.revealer.activated {
  cursor: pointer;
}

.revealable {

}

  .revealable.concealed {
    display: none;
  }

  .revealable.revealed {
    
  }

In this example only the “concealed” class is used to hide the element. We could have set the target’s initial display value to “hidden” and then changed it when the element receives the “revealed” class, but this isn’t optimal. We have no idea of knowing what the initial value of the property was; it could be “block”, “inline” or something else entirely. By using the reverse logic, that is we hide the element when it has the class name “concealed”, it will automatically go back to its initial value when that class name is removed.

Now, we can see if our code works. I’ve added a blue border to revealer elements and a green border to all targets. As a meaningless gimmick, the second part of the tutorial is contained inside a revealable target with the heading as the revealer :)

Many to many

So far, the revealer element has been the one containing the link to its related targets, but we can also make it the target’s responsibility to tell us which element(s) it can be revealed by. To make this happen, we reverse the logic:

$$('.revealable-target').each(function(target){
  var className = target.readAttribute('class'),
      re = "revealed-by:([^ ]+)";

  var revealers = (className.match(new RegExp(re, 'g')) || []).map(function(str){
    return $(str.match(new RegExp(re))[1]);
  });

  target.addClassName('revealable');
  target.addClassName('concealed');
  revealers.invoke('addClassName', 'revealer');
  revealers.invoke('addClassName', 'activated');

  revealers.each(function(revealer){
    revealer.observe('click', function(e){
      e.stop();
      target.toggleClassName('revealed');
      target.toggleClassName('concealed');
    });
  });
});

First we find all elements with the class name “revealable-target”, then search its class attribute for “revealed-by:foo” to get the revealer elements by ID. Then we add an observer to each revealer element in the same way we did in the reverse example.

Below is an example where the revealable target contains the information about its revealer as opposed to the revealer containing the target information.

This element isn’t marked as a revealer

I’m also a revealer for the above target