jQuery in Magento made easy


First of all I’d like to credit my colleague Eoghan. He’s the Javascript guru in the company, and really this blog post is based on a library he developed for use on our projects, adapted for Magento. I’m still feeling my way around Javascript, in terms of taking advantage of the existing frameworks (jQuery, Prototype, MooTools) to write modular code that is loosely coupled with the underlying HTML/CSS.

For those of you who have used Magento, you will know that it comes packaged with Prototype and Scriptaculous. The entire functionality both frontend and backend is dependent on the 10 or so Javascript classes written on top of Prototype. While technically it is possible to strip back this Javascript to allow orders to be placed for users without Javascript enabled, it really depends on your setup and is a reasonable amount of work for a questionable pay-off.

But of course, Prototype is somewhat limited as a framework. Not only does it not have the same amount of 3rd party modules written for it, in comparison with jQuery, but it also tends to be a bit more verbose in terms of getting it to do what you want. So it’s not surprising that Magento Connect is littered with modules that provide functionality (e.g., slideshows) that are written in jQuery.

Of course, you can’t just throw the jQuery library and some custom code into a Magento template and expect it to work. There will be conflicts between Prototype and jQuery as both use the ‘$’ sign notation. Fortunately, jQuery has the noConflict() method that you can use to allow Prototype and jQuery to function happily together.

But as online shops become more sophisticated in terms of the user experience, there can be an increase in the number of jQuery modules being used on the store. For example, on a store I am building at the moment you have a sliding panel and a lightbox on every page, as well as an image zoom, a colour picker and another sliding panel on the product page. If we start putting <script> tags into the Magento template files where each particular functionality is needed, we soon end up with code that is difficult to maintain. Not only that, but including Javascript inline like that, slows down page loads.

So rather than having each piece of jQuery in the appropriate template, remembering to wrap it with noConflict() each time, let’s put it in one file for the entire site. Here’s the example I’ll be using:

(function($){

     $.studioforty9= {
          version: '0.1',
          website: 'http://dev.studioforty9.com/',
          sections: {
               'cms-index-index' : 'this.initHome()',
               'cms-page-view' : 'this.initCMS()',
               'catalog-category-view' : 'this.initCategory()',
               'catalog-product-view' : 'this.initProduct()',
               'checkout-cart-index' : 'this.initCart()',
               'checkout-onepage-index' : 'this.initCheckout()'
          },
          init: function()
          {
               this.TopPanel();
               this.LightBox();
          },
          initSiteSection: function(section){
               var section = $('body').attr('class').split(' ')[1];
               eval(this.sections[section]);
          },
          initHome: function(){},
          initCMS: function(){},
          initCategory: function(){},
          initProduct: function(){
               this.ProductOptions();
               this.ProductZoom();
               this.ColourPicker()
          },
          initCart: function(){},
          initCheckout: function(){},
          Utilities:
          {
               external: function() {
                    $('A[rel*="external"]').click(function(e) {
                         var url = $(this).attr('href');
                         window.open(url);
                         e.preventDefault();
                    });
               }
          },
          Forms:
          {
               focus: function() {
                    var prevLabel = '';
                    $('.focus').focus(function() {
                         if ( $.trim($(this).val()) == $(this).attr('title') ) {
                              prevLabel = $(this).val();
                              $(this).val('');
                         }
                    });
                    $('.focus').blur(function() {
                         if ( $(this).val() == '' ) {
                              $(this).val(prevLabel);
                         }
                    });
               }
          },
          TopPanel: function() {
               $('#lightbox-link').bind('click', function(event){
                    event.preventDefault();
                    $('.lightbox').slideDown('slow');
                    $('#close-link').show();
               });
              
               $('#login-link').bind('click', function(event){
                    event.preventDefault();
                    $('.login').slideDown('slow');
                    $('#close-link').show();
               });
              
               $('#close-link').bind('click', function(event){
                    event.preventDefault();
                    $('.login').slideUp('slow');
                    $('.lightbox').slideUp('slow');
                    $('#close-link').fadeOut('slow');
               });
          },
          LightBox: function(){
               $('.lightbox-img').lightBox({
                    containerResizeSpeed: 800,
                    imageLoading: 'http://dev.studioforty9.com/skin/frontend/default/modern/img/lightbox-ico-loading.gif',
                    imageBtnClose: 'http://dev.studioforty9.com/skin/frontend/default/modern/img/lightbox-btn-close.gif',
                    imageBtnPrev: 'http://dev.studioforty9.com/skin/frontend/default/modern/img/lightbox-btn-prev.gif',
                    imageBtnNext: 'http://dev.studioforty9.com/skin/frontend/default/modern/img/lightbox-btn-next.gif',
               });
          },    
          ProductOptions: function(){
               if($('#choose-options').length > 0){
                    $('#choose-options').bind('click', function(event){
                         event.preventDefault();
                         $('.options').slideToggle('slow');
                    });
               }
          },
          ProductZoom: function(){    
               var options = {
                             zoomWidth: 300,
                             zoomHeight: 260,
                           xOffset: 122,
                           yOffset: -82,
                           position: "right",
                           title: false
               };

               $('.product-image-zoom a').jqzoom(options);
          },
          ColourPicker: function(){    
               $('#picker-link a').bind('click', function(event){
                    event.preventDefault();
                    $('#colour-picker').fadeToggle();
               });                    
              
               $('#colour-picker').farbtastic({callback : '.product-essential', width: 150});
          }
     }

     $(function() {
          $.studioforty9.init();
          $.studioforty9.initSiteSection();
     });

})(jQuery.noConflict());

There’s a few things I’d like to point out about this code.

First of all, there’s only one call to noConflict(). The entire code is wrapped as an anonymous function, and noConflict() applied to this. Easy!

Second, also at the bottom of the code is the equivalent of the onDomReady function. This is the code that is run when the page is finished loading. 2 lines. Easy to see what’s going on. There’s the init() function that calls other functions that we want to run on every page on the site. So in our case this is the TopPanel that slides down when particular links are clicked and the LightBox for particular images.

The other function is initSiteSection(). As we’ve said already, we want certain code to only be run on certain pages. Rather than introduce a raft of if…else statements, we can leverage Magento to figure this out in a more seamless manner. Each Magento page has particular classes on the

tag. The main ones are listed in the sections object. We associate these classes with a particular function to run (e.g., when the

tag has a class of ‘catalog-product-view’ we want to run the initProduct() function). This way it’s very easy to find out exactly what code from the class is running on any given page.*

* I should point out that it’s also necessary to include the relevant libraries in the appropriate layout XML file. So for example, we would want to add the following lines of code to catalog.xml file of our theme to add the jqZoom library to

of the page:

<catalog_product_view translate="label">
          ...
        <reference name="head">
          ...  
          <action method="addItem"><type>skin_js</type><name>js/jquery.jqzoom1.0.1.js</name></action>
          <action method="addItem"><type>skin_css</type><name>css/jqzoom.css</name></action>
          ...
        </reference>

        ...
</catalog_product_view>

 

Ted Robinson
Ted Robinson |