Magento and jQuery: Ajax Add to Cart Button

Introduction

This post describes adding an Ajax add-to-cart feature on my Magento’s product view page. This was an interesting exercise, and I learned a lot from a number of different sources. I combined their collected wisdom, and applied it as described here. Some of that wisdom was my own. You’ll need to bring in jQuery and jQuery UI as described in my previous post, Magento and jQuery.

Goal

Provide add to cart button that will give us the familiar ajax modal feel while it’s working, and then let us know it’s added the item. Then, present the user with the option of staying on the current page, or proceeding to the checkout page.

Prep Work I: Reflect the Correct number of Items in “My Cart”

In order for our work to correctly update the “My Cart” link in the header, I needed to add identification information to that section. You can see in the highlighted lines of code from my template/page/html/header.phtml. I applied a style to the whole thing in case the designers wanted to improve its appearance. And then I added div around only the number itself:

template/page/html/header.phtml

<div class="up-top-cart-status">
    <a href="<?php echo $this->getUrl('checkout/cart'); ?>"><img src="<?php echo $this->getSkinUrl(); ?>images/cart-icon.png" alt="Cart" />My Order
        <?php
    $cart = Mage::getModel('checkout/cart')->getQuote()->getData();
    $cart_qty = (!empty($cart['items_qty'])) ? $cart['items_qty'] : 0;
    printf('(<div style="display:inline" id="up-top-number-in-cart">%d</div> Items)', $cart_qty);
          ?>
     </a>
</div>

Prep Work II: Build the php Code for the Ajax Call

This code was based on work found at iFuel Interactive’s Adding to the Cart with a jQuery Ajax Call in Magento. I’ve modified the code to use php’s json_encode() function. I believe that, as it’s php, we should work in php’s milieu instead of handcrafting the JSON ourselves.

Of special note, I am calling getSummaryCount after I’ve saved the cart. This call did not yield the correct number of items in the cart until after I’d saved it. This way, I’m able to put the current number of items into the JSON returned back to the caller.

/mxwest_atc.php

<?php
include_once 'app/Mage.php';
Mage::app();
try{
    $result = array();
    // usage /mxwest_atc.php?product_id=838qty=1

    if(!isset($_GET['sku'])) {
        $sku = '';
    }
    else {
        $sku = $_GET['sku'];
    }

    if(!isset($_GET['product_id'])) {
        $product_id = '';
    }
    else {
        $product_id = $_GET['product_id'];
    }
    if(!isset($_GET['qty'])) {
        $qty = '1';
    }
    else { 
        $qty = $_GET['qty'];
    }

    if ($sku != ""){
        $product_id = Mage::getModel('catalog/product')->getIdBySku("$sku");
        if ($product_id == '') {
            $session->addError("<strong>Product Not Added</strong>The SKU you entered ($sku) was not found.");
        }
    }

    $request = Mage::app()->getRequest();
    $product = Mage::getModel('catalog/product')->load($product_id);
    $session = Mage::getSingleton('core/session', array('name'=>'frontend'));
    $cart = Mage::helper('checkout/cart')->getCart();

    $cart->addProduct($product, $qty);

    $session->setLastAddedProductId($product->getId());
    $session->setCartWasUpdated(true);

    $cart->save();

    $items_in_cart = Mage::helper('checkout/cart')->getSummaryCount();

    $result['result']="success";
    $result['message']="Added!";
    $result['items_in_cart'] = "$items_in_cart";

    echo json_encode($result);
}
catch (Exception $e) {
    $result['result'] = 'error';
    $result['message'] =  $e->getMessage();
    echo json_encode($result);
}
?>

Add to Cart: Pulling it All Together

template/catalog/product/view/addtocart.phtml

Here, we are setting up the input fields and naming the various input elements correctly. In my case, “minimum quantity” also means “quantity multiplier.” That is, I must order a multiple of minimum quantity. If minimum quantity is 4, I must order 4, 8, 12 or so on. My code will round the order up. If you tried to order 3, I’d round you up to 4. Your code might not need this. If that’s the case, just don’t use the qtyM variables, and don’t call upFixQtyMult.

template/catalog/product/view/addtocart.phtml, Part 1 – php

Of note here is that I am uniquely identifying the qty field; this isn’t necessary when I’m using this code for only 1 item. But if this code is used multiple times on a page, I’ll need unique identifiers. “A-ha!” I here you say “- the SKU isn’t unique!” Calm down. It’s not finished yet.

The UI experience will be encapsulated in the div id dialog. The subsequent jQuery UI code will use that div to keep the user up to date on what’s happening. See line 17, highlighted.

<?php
    $_product = $this->getProduct();
    $_myProductSku = $_product->getSku();
    $_mxwMinQty = $this->getMinimalQty($_product);
?>
<?php $buttonTitle = $this->__('Add to Cart'); ?>
<?php if($_product->isSaleable()): ?>
    <div class="add-to-cart">
        <?php if(!$_product->isGrouped()): ?>
        <label for="qty"><?php echo $this->__('Qty:') ?></label>
        <input type="text" size="4" name="qty" id="qty<?=$_myProductSku?>" maxlength="4" value="<?=$_mxwMinQty?>" title="<?php echo $this->__('Qty') ?>" class="input-text qty" />
        <input type="hidden" id="qtyM<?=$_myProductSku?>" value="<?=$_mxwMinQty?>" />
        <?php endif; ?>
        <br /><a href="#"><img src="<?php echo $this->getSkinUrl(); ?>images/buy-now-btn.png" alt="Buy Now" onclick="addItemToCart('<?=$_myProductSku?>');" alt='<?=$buttonTitle ?>' /></a>
        <?php echo $this->getChildHtml('', true, true) ?>
    </div>
<div class="ui-widget" id="dialog" title="Adding to Cart"></div>

template/catalog/product/view/addtocart.phtml, Part 2 – the Javascript

jQuery here is represented by $jQ. For more details on how we arrived at that nomenclature, please see my earlier post Magento and jQuery, which explains how I brought jQuery in in its noConflict() mode. That post also describes the reasoning behind the “ui-xxx” styling – that’s what jQuery UI’s ThemeRoller produces. Again, please consult the previous post for those details.

This code is pretty straightforward. Call the mxwest_atc.php script. It will return a JSON encoded response in (duh) “response.” The fancy bit of updating the header for number of items in cart is highlighted at lines 20 and 21. (You did remember to do the work described in “Prep Work I”, above, right?). I used the variable “o” as shorthand for object. Although I generally prefer more descriptive variable names, I figured the 2 lines here were comprehensible enough that I could shorthand it.

<script type="text/javascript">
function
addItemToCart(sku) {
    $jQ("#dialog").dialog('destroy');
    $jQ("#dialog").html("<center>Adding ...<p><img src='<?=$this->getSkinUrl()?>/images/up-ajax-loader.gif'></p></center>");
    $jQ("#dialog").dialog({show: 'scale', hide: 'scale', modal: 'true'});

    var qty_id = "#qty"+sku.toString();
    var qty = $jQ(qty_id).val();

    var qtyM_id = "#qtyM"+sku.toString();
    var qtyM = $jQ(qtyM_id).val();

    qty = upFixQtyMult(parseInt(qty),parseInt(qtyM));

    var cart_parms = "sku=" + sku + "&qty=" + qty;
    var myUrl = "<?=Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_WEB);?>/mxwest_atc.php?"+cart_parms;
    var jqhxr = $jQ.ajax({ url: myUrl })
        .success( function(response){
            o = $jQ.parseJSON(response);    
            $jQ("#up-top-number-in-cart").html(o.items_in_cart);
            $jQ("#dialog").html("<span class=\"ui-icon ui-icon-info\"></span>Thanks! " + sku + " was added to your cart." );
            $jQ("#dialog").addClass("ui-state-highlight ui-corner-all");
            $jQ("#dialog").dialog({hide: 'scale',
                buttons: {
                    "Keep Shopping": function() { $jQ(this).dialog("close"); },
                    "Checkout": function() { $jQ(this).dialog("close"); window.location = "<?=Mage::getUrl('checkout/cart', array('_secure' => true));?>"; }
                }
            });
        })
        .error( function() {
            $jQ("#dialog").html("<span class=\"ui-icon ui-icon-alert\"></span>Sorry, something bad happened. " + sku + " not added.");
            $jQ("#dialog").addClass("ui-state-error ui-corner-all");
            $jQ("#dialog").dialog({ buttons: { "Ok": function() { $jQ(this).dialog("close"); } } });
        });
}
function
upFixQtyMult(qty,qtyM) {
    var m = +qtyM;
    var q = m;
    if(isNaN(qty)) return +m;
    q = +qty;
    if( q <= m ) return m;
    return +((m<=1)?q:(q+(m-(q%m))));
}
</script>

For completeness sake … let’s close that php if!

<?php endif; ?>

End Note

Hope this helps! Magento is incredibly feature-rich, but I also find it incredibly dense to penetrate. I think that much of the User Experience still needs to abstracted out of php and back into jQuery UI (or similar) to improve the User Experience. There are many parts of the code that make me scratch my head. A head full of headaches at times. Hopefully, though, I helped shed a little light on using this cantankerous rascal in a more effective way. Let me know what you think!

Magento and jQuery

Warnings, Disclaimers, Fear, Loathing, etc.

The javascript described herein isn’t production hardened. The methods described herein for getting the javascript into magento proved effective for me on 1.5.0.1. The code example at the end was meant to prove the concept of an Ajax Add To Cart Solution that didn’t require eleventy million hours of work. You have been warned. Also, much of the code here was adapted from the very good entry at the TheUnical Technologies’ Blog, so a big Thank You to them!

The Magento jQuery Problem

Magento (up through v1.5.0.1) uses the ProtoType and Scriptaculous javascript libraries. These libraries help make the nice JavaScript effects/ elements that are built-in to Magento, especially on the administrative side.

For development on a Magento Site, though, we have a few problems. Problem one is that Magento is behind the curve on current versions of these libraries. I am not going to spend days on end figuring out exactly where the default magento installation uses these libraries. A cursory examination of the frontend default theme indicates about 185 “javascript” uses (as indicated simply by grep’ing for them). I doubt that upgrading these javascript libraries is high on Magento’s to-do list. And I don’t blame them: what’s there now works, in both their frontend default, and in the admin panel.

Problem two is that all the designers/ developers we run into only know jQuery. I’m not going to get into a debate about it. jQuery works, we understand it, so fine, go ahead and use it. Whether it’s jQuery or not is beside the point, really. I don’t want to rely on Magento’s plans for User Experience anyway, so I don’t want to be “piggy-backing” on their choice of javascript libraries.

As of 1.5.0.1, Magento reads in the layout/page.xml file of your theme, and brings in the various javascript libraries and – for lack of a better term – “munges” (not minimizes!) them into a single JavaScript file. This makes a degree of a sense as it cuts down the number of http requests required to bring all the necessary JavaScript files down to the user’s browser.

Magento cannot (yet?) bring in JavaScript (or anything else, for that matter) from a Content Delivery Network (CDN). At present, Magento does not support CDNs in its Community Edition platform (This isn’t to say Enterprise Edition can: it’s just to say I only know the Community Edition, and it can’t).

So bottom line: I want to use jQuery/ jQuery UI to enhance the user experience on my Magento site. I want to use jQuery’s ThemeRoller to define and standardize the CSS I’m using. And I want my jQuery to load in in its minimized form from a CDN.

Magento Configuration

1. Load in the jQuery libraries.

Option 1: Load in the jQuery libraries from a CDN

I’m using Google here. jQuery’s site lists other options you may wish to consider. In the admin panel, go to System->Configuration->Design->HTML Head, and scroll down to the Miscellaneous Scripts box. Add these lines:

<script
  src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"
    type="text/javascript">
</script>
<script 
  src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/jquery-ui.min.js"
    type="text/javascript">
</script>
<script type="text/javascript">
//<![CDATA[
    var $jQ = jQuery.noConflict();
//]]>
</script>

This accomplishes the magic of having a global $jQ as the jQuery library – in no conflict mode so it won’t interfere with anything the “default” javascript libraries might want to do.  (Remember also that jQuery UI is stateful. That could prove handy!). Now you’re ready to use jQuery wherever you want to. You will of course need the CSS, though, so, on to step 2.

Option 2: Load in the jQuery libraries locally.

First, we’ll need to download the jquery.min.js and jquery-ui.min.js libraries into our js folder. In this example, I have a “mxwest” folder under js where my JavaScript resides.

Next, I specify that these files should be loaded in my layout/page.xml file:

<action method="addJs"><script>mxwest/jquery.min.js</script></action>
<action method="addJs"><script>mxwest/jquery-ui.min.js</script></action>

Finally, I instantiate $jQ in my theme’s template/page/head.phtml file. In my case, I placed the code (in blue) after getTranslatorScript but  before getIncludes. I’ve no idea if this matters or not, but it does work on my installation.

<?php echo $this->getCssJsHtml() ?>
<?php echo $this->helper('core/js')->getTranslatorScript() ?>
<script type="text/javascript">
//<![CDATA[
    var $jQ = jQuery.noConflict();
//]]>
</script>
<?php echo $this->getIncludes() ?>

2. Load in your jQuery CSS

I placed the jQuery UI theme (as opposed to the Magento theme) into my skin, in the css folder. The theme generated by jQuery’s ThemeRoller comes contained in its own subfolder. I simply ftp’d that into place in my skin.

Next, I updated my layout/page.xml file to bring the jQuery UI CSS in:

<action method="addCss">
    <stylesheet>css/custom-theme/jquery-ui-1.8.10.custom.css</stylesheet>
</action>

jQuery UI’s ThemeRoller named everything for me. So I just went with it.

Sample Code

Here’s an example using $jQ to create a little “working” dialog and perform an Ajax add to cart. This is way faster than waiting for the whole checkout page to be returned to the user. This code appears in my theme’s view/addtocart.phtml.

[Note: this code really belongs folded into a minimized JavaScript library of our own creation, but calm down I’m not solving everything in the whole world at once. Also, it should be refactored. Later.]

<div id="dialog" title="Adding to Cart"></div>  <!-- “ui-widget” is jQuery UI standard! -->
<script type="text/javascript">
// This function is called when the Add to Cart button is pressed:
function
addItemToCart() {
    $jQ("#dialog").dialog('destroy'); // Clobber anything that might be left around
    $jQ("#dialog").html("Working..."); // Set my message.
    $jQ("#dialog").dialog(); // Show it...
 
    // Notice a little php here. Prolly should be an argument to this function.
    var sku = "<?=$_myProductSku?>";
 
    /* Same here. The reason for concatening sku to qty is in anticipation of making this function
        usable on a list page - I need unique identifiers for valid XHTML. */
    var qty = $jQ("#qty<?=$_myProductSku?>").val();
 
    // This is simply the GET query to my add to cart program.
    var cart_parms = "sku=" + sku + "&qty=" + qty;
 
    /* mxwest_atc.php is a mini-Mage app that adds to the cart. The Mage:: object ensures this is portable
        to other domain names. */
    var myUrl = "<?=Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_WEB);?>/mxwest_atc.php?"+cart_parms;
 
    /* This is a jQuery 1.5 implementation! Notice how my callbacks are defined!
        Successful or not, notice that I inject jQuery UI styling into the dialog
        using the addClass() method. */
    var jqhxr = $jQ.ajax({ url: myUrl })
        .success( function(){
        $jQ("#dialog").dialog('destroy');
        $jQ("#dialog").html("<span class=\"ui-icon ui-icon-info\"></span>Successfully added "
            + sku + " to cart.");
 
        $jQ("#dialog").addClass("ui-state-highlight ui-corner-all");
        $jQ("#dialog").dialog({
                buttons: {
                    "Ok": function() {
                    $jQ(this).dialog("close");
                    }
                }
            });
        })
        .error( function() {
        $jQ("#dialog").dialog('destroy');
        $jQ("#dialog").html("<span class=\"ui-icon ui-icon-alert\"></span>Sorry, something bad happened. "
            + sku + " not added.");
 
        $jQ("#dialog").addClass("ui-state-error ui-corner-all");
        $jQ("#dialog").dialog({
                buttons: {
                    "Ok": function() {
                    $jQ(this).dialog("close");
                }
        });
    })
    .complete( function() {});
}