Amazon Books Widget with jQuery and XML Image Scroller

It makes sense to forgo database tables and server-side code when you need to store a limited amount of non-sensitive data. Accessing this data can be a snap with jQuery because the library was built to traverse XML documents with ease. With some custom JavaScript and jQuery magic you can create some interesting widgets. A good way to demonstrate this functionality is by building a browsable Amazon.com books widget.

 

Step 1: The HTML

 

Before I get to the HTML, it is worthwhile to note that I am not using PNG files. You could very well substitute PNGs for the GIFs, and it would not affect the functionality. It would mean, however, you would need to implement a fix for the lack of PNG transparency support in Internet Explorer. There are several jQuery plugins that are available.

Amazon.com Books Widget

alt
alt

alt
alt

Step 2: The CSS

 

The CSS is fairly straightforward and self-explanatory, so I will not be spending a great deal of time explaining all the facets of each selector. Almost all selectors are child elements of the parent container with the identifier “books”. You can see that the width applied is optional. Removing it will allow the widget to expand and contract freely.

/* foundation */

body {
font: 100% normal "Arial", "Helvetica", sans-serif;
}
#books {
width: 515px; /* optional */
}
#books img {
border: 0;
}
#books .clear_both {
clear: both;
}
#books .float_left,
#books  ul li {
float: left;
display: inline;
}
#books .float_right {
float: right;
}
#books .overclear {
width: 100%;
overflow: hidden;
}

/* styles */

#books .buttons {
position: relative;
height: 30px;
margin: 0 0 5px 0;
}
#books .prev {
position: absolute;
top: 0;
left: 0;
visibility: hidden;
}
#books .next {
position: absolute;
top: 0;
right: 0;
}
#books .showing {
margin: 5px 60px 0 60px;
text-align: center;
font-size: .8em;
}
#books .top {
background: url(../images/books_top.gif) repeat-x;
}
#books .inner {
padding: 0 0 0 20px;
margin: 0 0 -20px 0;
background: url(../images/books_left_mid.gif) repeat-y;
}
#books  ul {
margin: 0;
padding: 0;
list-style-type: none;
background: url(../images/books_right_mid.gif) repeat-y top right;
}
#books  ul li {
display: none;
position: relative;
margin: 0;
padding: 0 20px 20px 0;
font-size: .8em;
z-index: 1;
}
#books  ul li.loader {
display: block;
float: none;
height: 115px;
margin: 0 0 20px -20px;
background: url(../images/books_loader.gif) no-repeat center center;
}
#books  ul li a.info {
position: absolute;
bottom: 20px;
right: 20px;
}
#books  ul li a.thumb {
display: block;
border: 1px solid #ddd;
}
#books  ul li a.thumb img {
display: block;
margin: 0;
padding: 3px;
}
#books .btm {
background: url(../images/books_btm.gif) repeat-x;
}
.books_tool_tip {
display: none;
position: absolute;
top: 0;
left: 0;
width: 350px;
z-index: 9999;
}
.books_tool_tip .books_pointer_left {
position: absolute;
top: 0;
left: 0;
width: 10px;
height: 10px;
background: url(../images/books_pointer_left.gif);
}
.books_tool_tip .books_pointer_right {
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 10px;
background: url(../images/books_pointer_right.gif);
}
.books_tool_tip .inner {
border: 1px solid #ddd;
padding: 15px 15px 3px 15px;
margin: 0 0 0 9px;
background: #fff;
}
.books_tool_tip .inner_right {
margin: 0 9px 0 0;
}
.books_tool_tip .inner p {
font-size: .8em;
margin: 0;
padding: 0 0 12px 0;
}

Step 3: The XML

 

There is nothing revolutionary about this XML. As you will see, each book contains a title, author(s), an image, an Amazon URL, a reviews total, and a reviews average. The XML could be normalized in one area, and that is the “author” node. Strictly speaking, there can be several authors, and an author can be one of two types, an author or an editor. However, I kept it simple in order to focus on the core functionality. A good bit of homework would be to see how you could better optimize that node, and then successfully parse it with jQuery.



Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides




250
4.5


Andrew Hunt, David Thomas




131
4.5


Martin Fowler, Kent Beck, John Brant, William Opdyke




139
4.5


Martin Fowler




56
4.5


Elisabeth Freeman, Eric Freeman, Bert Bates, Kathy Sierra




252
4.5


Thomas Cormen, Charles Leiserson, Ronald Rivest, Clifford Stein




167
4.0


Frederick P. Brooks




128
4.5


Joshua Bloch




120
5.0


Jeffrey Friedl




125
4.5


Michael Sipser




52
4.5


Steve Krug




453
4.5


Edward R. Tufte




96
4.5


David Flanagan




278
4.5


Jenifer Tidwell




47
4.5


William Lidwell, Kritina Holden, Jill Butler




54
4.5


Peter Morville




46
4.0


John Battelle




99
4.5


W. Jason Gilmore




100
4.0



Step 4: The JavaScript

 

The JavaScript is certainly the most complicated portion of the tutorial. As best I can, I usually begin scripts like this by stubbing out the state and behavior of an object to get a feel for functionality. This particular object is simply called “BOOKS”. I also use what is known as the Module Pattern, which is detailed by Eric Miraglia on the Yahoo! User Interface Blog. This design pattern gives you the ability to create private methods and properties. Whenever releasing a script into the wild (like now), this pattern helps to eliminate the possibility of conflicts with other functions and objects other developers may already be using.

var BOOKS = function(){
var _P = {
init : function( params ) {},
params : null,
data : null,
loadXml : function() {},
first : 0,
max : 0,
count : 0,
preloadBooks : function() {},
browseBooks : function( browse ) {},
tooltip : {
show : function( e, $o ) {},
hide : function( e, $o ) {},
getMouseCoord : function( v, e ) {},
getViewport : function() {}
}
};
return {
init : function( params ) {
_P.init( params );
}
};
}();

All of my private members I placed inside of an object called “_P”. This has more to do with organizational efforts than anything. So long as a member is not in the BOOKS return statement, I could very well have created it as a standalone variable or function. Since I need a way to associate public parameters (settings) with private members, I have one public method. This public initialization method will pass the settings along to a private initialization method, acting as a go-between. I will revisit those settings in the final step.

Here is a look now at the final [removed]

var BOOKS = function(){
var _P = {
init : function( params ) {
_P.params = params;
_P.loadXml();
},
params : null,
data : null,
loadXml : function() {
$.ajax({
type : "GET",
url : _P.params.xmlPath,
dataType : "xml",
success : function( data ) {
_P.data = data;
_P.max = _P.params.perView;
_P.count = $( "book", data ).length;
_P.preloadBooks();
_P.browseBooks();
}
});
},
first : 0,
max : 0,
count : 0,
preloadBooks : function() {
$( "ul", "#books" ).empty();
$( "book", _P.data ).each(function( i ) {
var title = $.trim( $( "title", this ).text() );
var href = $.trim( $( "href", this ).text() );
$( "ul", "#books" ).append([
"

  • More Info",<br /> title,<br /> "
  • " ].join( "" ));
    $( "body" ).append([
    "

    ",
    title,
    " (by ",
    $.trim( $( "author", this ).text() ),
    "
    )",
    "

    (",
    $.trim( $( "reviews > total", this ).text() ),
    ")",
    "

    " ].join( "" ));
    });
    $( ".info", "#books" ).hover(function( e ) {
    _P.tooltip.show( e, $( "#books_tool_tip_" + $( "a.info", "#books" ).index( this ) ) );
    }, function( e ) {
    _P.tooltip.hide( e, $( "#books_tool_tip_" + $( "a.info", "#books" ).index( this ) ) );
    });
    $( "#books .prev" ).click(function() {
    _P.browseBooks( "prev" );
    return false;
    });
    $( "#books .next" ).click(function() {
    _P.browseBooks( "next" );
    return false;
    });
    },
    browseBooks : function( browse ) {
    if ( browse == "prev" ) {
    if ( _P.first == _P.count && ( _P.count % _P.max > 0 ) ) {
    _P.first = _P.first - ( ( _P.count % _P.max ) + _P.max );
    } else {
    _P.first = _P.first - ( _P.max * 2 );
    }
    }
    var range = _P.first + _P.max;
    var start = 1;
    if ( range > _P.max ) {
    start = ( ( range - _P.max ) + 1 );
    }
    if ( _P.first == 0 ) {
    $( "#books .prev" ).css( "visibility", "hidden" );
    } else {
    $( "#books .prev" ).css( "visibility", "visible" );
    }
    if ( range < _P.count ) {
    $( "#books .next" ).css( "visibility", "visible" );
    } else if ( range >= _P.count ) {
    range = _P.count;
    $( "#books .next" ).css( "visibility", "hidden" );
    }
    $( "book", _P.data ).each(function( i ) {
    if ( i >= _P.first && i < range ) {
    $( "#books li:eq(" + i + ")" ).fadeIn( "slow" );
    } else {
    $( "#books li:eq(" + i + ")" ).css( "display", "none" );
    }
    });
    _P.first = range;
    $( "#books .showing" ).html([
    "Viewing ",
    start,
    " - ",
    range,
    "
    of ",
    _P.count,
    "
    " ].join( "" ));
    },
    tooltip : {
    show : function( e, $o ) {
    var v = _P.tooltip.getViewport();
    var pageX = _P.tooltip.getMouseCoord( v, e )[0] + 15;
    var pageY = _P.tooltip.getMouseCoord( v, e )[1];
    $o.find( ".books_pointer_right" ).addClass( "books_pointer_left" ).removeClass( "books_pointer_right" );
    if ( pageX + $o.width() > v.innerWidth + v.pageXOffset ) {
    pageX = pageX - $o.width() - 30;
    $o.find( ".inner" ).addClass( "inner_right" );
    $o.find( ".books_pointer_left" ).addClass( "books_pointer_right" ).removeClass( "books_pointer_left" );
    }
    $o.css( "left", pageX ).css( "top", pageY ).css( "display", "block" );
    },
    hide : function( e, $o ) {
    $o.css( "display", "none" );
    },
    getMouseCoord : function( v, e ) {
    ( !e ) ? e = window.event : e = e;
    ( e.pageX ) ? v.pageX = e.pageX : v.pageX = e.clientX + v.scrollLeft;
    ( e.pageY ) ? v.pageY = e.pageY : v.pageY = e.clientY + v.scrollTop;
    return [ e.pageX, e.pageY ];
    },
    getViewport : function() {
    var viewport = {}
    if ( self.innerHeight ) {
    viewport.pageYOffset = self.pageYOffset;
    viewport.pageXOffset = self.pageXOffset;
    viewport.innerHeight = self.innerHeight;
    viewport.innerWidth = self.innerWidth;
    } else if ( document.documentElement && document.documentElement.clientHeight ) {
    viewport.pageYOffset = document.documentElement.scrollTop;
    viewport.pageXOffset = document.documentElement.scrollLeft;
    viewport.innerHeight = document.documentElement.clientHeight;
    viewport.innerWidth = document.documentElement.clientWidth;
    }
    return viewport;
    }
    }
    };
    return {
    init : function( params ) {
    _P.init( params );
    }
    };
    }();

    Step 5: The Final Widget

    The last thing to do is to pass the settings through to the JavaScript initialization method from the HTML. There are three arguments: the path to the XML file, the path to the images used in the JavaScript, and the number of books you would like displayed per view. For this tutorial, it is assumed there is only one books widget per page (called “books”), which is why there is no parameter for ID or class name. Here is the XHTML in final form:

    Amazon.com Books Widget

    $(function(){
    BOOKS.init({
    xmlPath : "data/books.xml",
    imgPath : "images",
    perView : 4
    });
    });

    alt
    alt

    alt
    alt

    Be Sociable, Share!

    Leave a comment