the playground

The Playground is our online laboratory for experimenting with Web technologies both old and new. The projects in this section are definitely unfinished and serve only to encourage creative ideas so have fun but expect the unexpected.

now playing

Fast, Scalable, Clean Javascript with jQuery and mod_deflate

By: Brandon Burkett Monday, September 17, 2007

The following post requires subject area experience or a big brain to digest. In short, it covers some cool technology applications that in the example provided allow cool image transitions requiring less bandwidth.

Some of you that know me know I am not a "framework" kind of guy. I don't have a problem with frameworks or rapid application development, it is just not my style. I prefer prepared statements over an Object Relational Mapping (ORM) layer, custom code over generated code, and quality of functionality over quantity of functionality. My goals for projects are: fast/good experience for end users, fast for our server, and utilize as little server resources as possible. Hopefully that paints a picture of the development style I try to use.

When it comes to Javascript development, it can be a painful experience if you are required to do animations and/or DOM/event management. I am sure some of you have had the same experiences I have, including a bunch of for loops to get specifc sets of DOM elements and adding events to them. Animations are usually even worse, consisting of many recursive setTimeout calls littered throughout your object's methods until a specific state is met. Enter the jQuery library.

jQuery

First off, jQuery isn't exactly a framework. It is a library that you can use as little or as much as you want within your existing Javascript classes. This, in addition to its small footprint (will get to later), made jQuery very appealing to me. Using jQuery, I will walk you through a basic class I wrote that handles all the functionality for a simple site that rotates images. It will include image animation and rotation, event handling, and DOM manipulation.

First, lets start by making our javascript object. This will be the container for our site's functionality.


var rotate = 
{

}

Next, lets add our init method. Lets say we have two divs with an id of 'next' and 'previous' that will control how the images rotate. We need to add events to these divs and tell jQuery what we want them to do. If you are not familiar with jQuery syntax, this may look strange to you, but I think it is easy to pick up. We also need to add a couple of attributes to our class to store the images (array) and length of the array.


var rotate = 
{
       	// class attributes/properties
        image_url: new Array(),
        arrayLength: null,
        
        // initalizer method

        init: function()
        {                
                rotate.image_url[0] = 'images/someImage1.jpg';
                rotate.image_url[1] = 'images/someImage2.jpg';
                rotate.image_url[2] = 'images/someImage3.jpg';
                rotate.image_url[3] = 'images/someImage4.jpg';
                rotate.image_url[4] = 'images/someImage5.jpg';                

                // store array length - 1                
                rotate.arrayLength = rotate.image_url.length;
                rotate.arrayLength -= 1;
                
                // add event next / previous divs

                $('#previous').bind('click', function() { rotate.changeImage('prev'); });
                $('#next').bind('click', function() { rotate.changeImage('next'); });
                
                // add background images to divs if js enabled

                $('#previous').html('<img src="images/minus.gif" alt="Previous Image" />');
                $('#next').html('<img src="images/plus.gif" alt="Next Image" />');
        }
}

Now lets explain a little more about what is going on above. We created two attributes called image_url and arrayLength. Inside our init method we are storing images into the image_url array and its length - 1 into arrayLength (so we get the highest number in the array, in this case 4). Next, we use jQuery to select the div with id 'previous' and bind a click event to it. We do the same for for the div with id 'next' (Note: we have not defined the class method changeImage yet, but we will in our next step.). Notice how easy jQuery syntax is to read? You can access elements directly by there id or class name using the pound (#) or dot (.) operator.

The final thing we need to do is add next/previous images to the divs. We do this with jQuery by using #previous or #next to select the DOM element and fill its inner html with a previous/next image. This will allow users with Javascript enabled to use image rotation, while users without will not even know the functionality is there. This way non-Javascript users will not have unexpected results when they see a next/previous image and try to click them. Ideally, you should offer the same functionality for Javascript and non-Javascript users, but that is a topic for another playground piece.

Now lets define the changeImage method.


var rotate = 
{
       	// class attributes/properties
        image_url: new Array(),
        arrayLength: null,
        
        changeImage: function(type)
        {
                // fade current image out

                $('#currentImage').animate({ width: 1, opacity: 0.1}, 'slow', function()
                {
                        var thisNum = parseInt($('#currentImage').attr('alt').replace(/Image Number /g,''));
                        var newImageId = 0;
                        
                        if(type == 'prev')
                        {
                                // determine next image

                                newImageId = (thisNum > 0) ? (thisNum-1) : rotate.arrayLength;                                
                        }
                        else
                        {
                                // determine next image
                                newImageId = (thisNum < rotate.arrayLength) ? (thisNum+1) : 0;                                
                        }
                        
                        // force image to load

                        var nextImage = new Image(); 
                        nextImage.src = rotate.image_url[newImageId];
                        
                        if(nextImage.complete)
                        {
                                // fade new image in
                                $('#currentImage').attr('src', rotate.image_url[newImageId]).attr('alt', 'Image Number '+newImageId);
                                $('#currentImage').animate({width: 500, opacity: 1}, 'slow');                                
                        }
                        else

                        {
                                nextImage.onload = function()
                                {
                                        // fade new image in
                                        $('#currentImage').attr('src',rotate.image_url[newImageId]).attr('alt', 'Image Number '+newImageId);
                                        $('#currentImage').animate({width: 500, opacity: 1}, 'slow');
                                }                                                                
                        }                                                
                });                
        },

        // initalizer method

        init: function()
        {                
                rotate.image_url[0] = 'images/someImage1.jpg';
                rotate.image_url[1] = 'images/someImage2.jpg';
                rotate.image_url[2] = 'images/someImage3.jpg';
                rotate.image_url[3] = 'images/someImage4.jpg';
                rotate.image_url[4] = 'images/someImage5.jpg';                

                // store array length - 1                
                rotate.arrayLength = rotate.image_url.length;
                rotate.arrayLength -= 1;
                
                // add event next / previous divs

                $('#previous').bind('click', function() { rotate.changeImage('prev'); });
                $('#next').bind('click', function() { rotate.changeImage('next'); });
                
                // add background images to divs if js enabled

                $('#previous').html('<img src="images/minus.gif" alt="Previous Image" />');
                $('#next').html('<img src="images/plus.gif" alt="Next Image" />');
        }
}

Breaking down the changeImage method, we see the first thing we are doing is using jQuery to access the element with id 'currentImage'. By using the animate method, we can modify many properties. In this case I am a setting the width to 1 (end point) and an opacity to 0.1 (end point). The slow attribute means the animations will go from the current width/opacity to the new width/opacity at a slow speed. You can specify slow, normal, fast, or even your own duration. The function at the end is a call back function that will execute when the animation is complete. Inside of the call back function, we are using jQuery to get the current image number from the alt tag in element id currentImage (which could be random on the first page load via PHP or other server side script).

We then use the ternary operator to increment or decrement the number we received from currentImage. This way we know which image in the array to show next. Next we create a new image and set the src. This will force the image to start loading. We need to place two conditions before we finish our animation since we are not sure when the image will complete loading. First, check to see if the image is cached or already finished loading by using the .complete attribute on the new image. It will return true if the image is already loaded. If the image is not loaded, we need to attach an onload event to it. With these two checks in place, we can complete the animation after the image loads. Notice how I chain two .attr calls on currentImage before I begin the closing animation. With jQuery, you can chain together multiple calls to reduce the amount of code you have to write. The closing animation will bring the width back to 500 and set the opacity to 1.

We have one last thing to do. The script needs to run when the document loads. One feature jQuery has is to load scripts after the DOM finishes loading. To do this we need to add the following line as the very last line in our script:


$(document).ready( function(){ rotate.init() });

Just include the script in your html page and you are done. If you want to see the script in action, here is an example. You can disable and enable Javascript to see the changes.

Now What?

The last thing I want to cover is gzip compression. This compliments jQuery very nicely. If you are running a *nix server and apache 2.x, mod_deflate is probably already installed. If not, check out the apache documentation for mod_deflate. If you are using apache 1.x, mod_gzip will do a very similar thing.

To use mod_deflate, include the following lines in your .htaccess file:


# used to gzip html/js files
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/x-javascript
BrowserMatch \bMSIE\s6 no-gzip

What this will do is serve your html, xml, text, and javascript files compressed with gzip. A minified version of jQuery is around 45 K, but served with mod_deflate it is 14 K. You can view the results yourself if you need more proof. This will save a lot of bandwidth on your server and give your clients faster pages.

This is only the tip of the iceburg of functionality jQuery offers. Head over to the documentation page to see everything it can do.

Except where noted in the site legal statements, the content of this site is licensed under a
Creative Commons Attribution-Share Alike 3.0 United States License.