Subscribe to my Feed, follow me on Twitter, recommend me on Working With Rails or see my code on GitHub
Unobtrusive Ajax patterns: star rating
The “star rating” is now becoming a familiar pattern on the web, where users can rate an item using by giving it a number of “stars”, and their choice gets sent back to the server asynchronously.
It’s quite easy to add behaviour on top of this pattern unobtrusively. Let’s say we have this HTML:
<ul class="rater">
<li><a href="/rate/1">1</a></li>
<li><a href="/rate/2">2</a></li>
<li><a href="/rate/3">3</a></li>
<li><a href="/rate/4">4</a></li>
<li><a href="/rate/5">5</a></li>
</ul>
This works without any JavaScript and can be styled quite easily. We’re not going to mess around much with the DOM structure, all we really need to do is hijack the links and make them work asynchronously. We can hook into each “rater” by using its class name:
$$('ul.rater').each(function(rater){
//...
});
So, to hijack the links, we use the event delegation technique, which means we attach a single handler to the rater element:
$$('.rater').each(function(rater){
rater.observe('click', function(e){
var target = e.element();//The element which was clicked
if (!rater.hasClassName('rated') && target.tagName.toLowerCase() == 'a') {
//We're in a link element, stop the default action
e.stop();
// [...]
}
});
});
In the handler, we check if the element that was clicked is a link. If not, we don’t do anything, but if it is a link, we can proceed by sending an asynchronous request:
// [...]
new Ajax.Request(target.readAttribute('href'), {
onLoading: function(){
rater.addClassName('loading');
},
onSuccess: function(res){
//Add "selected" class to all previous list items
target.up().previousSiblings().invoke('addClassName', 'selected');
//Add to clicked list item
target.up().addClassName('selected');
rater.addClassName('rated');
},
onComplete: function(){
rater.removeClassName('loading');
}
});
// [...]
The request takes its URL from the link’s href attribute. When it gets sent (onLoading), the rater receives the class name “loading”, which is removed again when the request completes (onComplete), no matter if the request was successful or not. If it was successful, we loop through the rater’s li elements up to and including the selected and add the class name “selected”.
We use CSS to make it look like a rating widget, using the “selected” class to show visually which rating the user selected:
.rater {
list-style-type: none;
overflow: hidden;
font-size: 150%;
font-weight: bold;
}
.rater li {
float: left;
}
.rater li a {
display: block;
text-decoration: none;
color: #ff6 !important;
background: #666;
padding: 0 0.25em
}
.rater li.selected a {
color: #ff3 !important;
background-color: #333;
}
In this example, I’ve replaced the numbers with stars – ★ -you might not have the right font to see it, but you might as well use
background-image in the CSS. You’ll also notice that on mouseover only the current element gets highlighted; there’s not really any good way to do this with CSS, so you probably have to attach onmouseover and onmouseout handlers to the rater to add the “selected” class name.
The example doesn’t actually submit anything, but uses a timeout to simulate the request.
