Ajax Based Product Add to Cart from Category Page

In this blog post we will see how to add products to shopping cart directly from category page using ajax operations.

We would be able to add all product types simple,configurable,bundled,grouped, virtual and downloadable from the category page itself. For the products which require options to be selected before adding to cart e.g configurable,bundled etc , a lightbox iframe window will show up showing the options to be selected by user with a ‘Add to Cart’ button.
This blog is in continuation of the previous blog written, the same code base has been used and new coded is added. So it would be better to once go through the previous blog as well. Here are screen shots of the module in action, just so you know what we are going to develop.

Downloadable Product Add to Cart

Downloadable Product Add to Cart

Virtual Product Category Page Add to Cart

Virtual Product Category Page Add to Cart

Magento Bundled Product Add to Cart

Magento Bundled Product Add to Cart

Grouped Product Add to Cart

Grouped Product Add to Cart

Category Page Simple Product

Category Page Simple Product

Configurable Product with Custom Options

Configurable Product with Custom Options

Attached is source code for this module
Magneto version 1.6(+)
[dm]17[/dm]
Magneto version 1.6(-)
[dm]18[/dm]
Now let see what all we need to do, to achieve the above.

Step1: Override list.phtml

First we will have to override the list.phtml file and create our own list.phtml file in our module. In default theme, the list.phtml has this code written on the ‘Add to Cart’ button

onclick="setLocation('<?php echo $this->getAddToCartUrl($_product) ?>')"

The setLocation function simply redirects to another page. We cannot use this, since we need to have ajax. Now we will have to call another function instead of setLocation which will make an ajax call instead of page redirect. For the ajax calls we are going to use the jQuery library just like the previous blog. Let name our new function setLocationAjax(), so in no our code will be

onclick="setLocationAjax('<?php echo $this->getAddToCartUrl($_product) ?>')"

We also need to do is put a ajax loading icon next to the add to cart button, and when the ajax button is clicked the loading should show up. So the final code for our ‘Add to Cart’ button would be

<p><button type="button" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart" onclick="setLocationAjax('<?php echo $this->getAddToCartUrl($_product) ?>','<?php echo $_product->getId()?>')"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button></p>
                        
<span id='ajax_loader<?php echo $_product->getId()?>' style='display:none'><img src='<?php echo $this->getSkinUrl('images/opc-ajax-loader.gif')?>'/></span>

Here the setLocationAjax has two parameters, first is the add to cart url and second is the product id. The second parameter is actually required to identify the loading image placed next to the add to cart button. If you see the id of the ajax loader image it is id=’ajax_loadergetId()?>’. So based on the product id, we can hide/show the correct ajax loading image.
But still this code is not complete, we still have to check if the product has some options (configurable, grouped, bundled, custom options). If the product has required options, the add to cart will not work until the options have been selected by the user. So what we will do is check if the product has required options then show a lightbox with the options only, so that user can select the options and add the product to cart. Here is the complete code for the ‘Add to Cart’ button.

<?php if ( !($_product->getTypeInstance(true)->hasRequiredOptions($_product) || $_product->isGrouped()) ) { ?>
                        <p><button type="button" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart" onclick="setLocationAjax('<?php echo $this->getAddToCartUrl($_product) ?>','<?php echo $_product->getId()?>')"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button></p>
                        <span id='ajax_loader<?php echo $_product->getId()?>' style='display:none'><img src='<?php echo $this->getSkinUrl('images/opc-ajax-loader.gif')?>'/></span>
                        <?php } else { ?>
                        	<button type="button" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart" onclick="showOptions('<?php echo $_product->getId()?>')"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button>
                        	<a href='<?php echo $this->getUrl('ajax/index/options',array('product_id'=>$_product->getId()));?>' class='fancybox' id='fancybox<?php echo $_product->getId()?>' style='display:none'>Test</a>
                        <?php }  ?>

If you look in default what we have done here is, put an if condition to check if product has required options or is a grouped product. If this is true then we then the ‘Add to Cart’ button has showOptions function and an anchor tag for the lightbox. If the condition is false then the default ‘Add to Cart’ button is shown with setLocationAjax() option and the ajax image loader.
The complete list.phtml file is found the in the attached module.

Step2: Javascript

Next in our list.phtml file we need to put in lot of javascript to the following

  • Initialize Fancybox
  • For Products which have options, fancybox with iframe should show up when click on ‘Add to Cart’ button
  • For Product which don’t have options, product should be added to cart through Ajax
  • The top links and cart sidebar should be updated after successful ‘Add to Cart’

First we will download the fancybox library and then unpack the files in our skin folder i.e skin\frontend\default\default\js\fancybox
Next to add fancybox to category pages add this code to the theme layout file, in my case ajax.xml

<catalog_category_default>
    	<reference name="head">
            <action method="addItem"><type>js</type><name>jquery/jquery-1.6.4.min.js</name></action>
            <action method="addItem"><type>skin_js</type><name>js/fancybox/jquery.fancybox-1.3.4.js</name></action>
            <action method="addItem"><type>skin_js</type><name>js/fancybox/jquery.easing-1.3.pack.js</name></action>
            <action method="addItem"><type>skin_js</type><name>js/fancybox/jquery.mousewheel-3.0.4.pack.js</name></action>
            <action method="addItem"><type>js</type><name>jquery/noconflict.js</name></action>
            <action method="addCss"><stylesheet>js/fancybox/jquery.fancybox-1.3.4.css</stylesheet></action>
        </reference>
    	<reference name='product_list'>
    		<action method='setTemplate'><template>ajax/catalog/product/view/list.phtml</template></action>
    	</reference>
    </catalog_category_default>
    <catalog_category_layered>
    	<reference name="head">
            <action method="addItem"><type>js</type><name>jquery/jquery-1.6.4.min.js</name></action>
            <action method="addItem"><type>skin_js</type><name>js/fancybox/jquery.fancybox-1.3.4.js</name></action>
            <action method="addItem"><type>skin_js</type><name>js/fancybox/jquery.easing-1.3.pack.js</name></action>
            <action method="addItem"><type>skin_js</type><name>js/fancybox/jquery.mousewheel-3.0.4.pack.js</name></action>
            <action method="addItem"><type>js</type><name>jquery/noconflict.js</name></action>
            <action method="addCss"><stylesheet>js/fancybox/jquery.fancybox-1.3.4.css</stylesheet></action>
        </reference>
    	<reference name='product_list'>
    		<action method='setTemplate'><template>ajax/catalog/product/view/list.phtml</template></action>
    	</reference>
    </catalog_category_layered>

Now when you open the category page, using firebug check if the fancybox library is loaded or not.
Here is code to initialize fancybox for all anchor tags with ‘fancybox’ class.

jQuery(document).ready(function(){
		jQuery('.fancybox').fancybox(
			{
			   hideOnContentClick : true,
			   width: 382,
			   autoDimensions: true,
               type : 'iframe',
			   showTitle: false,
			   scrolling: 'no',
			   onComplete: function(){ //Resize the iframe to correct size
				jQuery('#fancybox-frame').load(function() { // wait for frame to load and then gets it's height
					jQuery('#fancybox-content').height(jQuery(this).contents().find('body').height()+30);
					jQuery.fancybox.resize();
				 });

			   }
			}
		);
	});

Here is code of the showOptions() function. This will show fancybox with the iframe URL when click on ‘Add to Cart’.

function showOptions(id){
		jQuery('#fancybox'+id).trigger('click');
	}

Here is code to to add to product to cart using ajax which don’t have any options like simple and virtual products.

function setAjaxData(data,iframe){
		if(data.status == 'ERROR'){
			alert(data.message);
		}else{
			if(jQuery('.block-cart')){
	            jQuery('.block-cart').replaceWith(data.sidebar);
	        }
	        if(jQuery('.header .links')){
	            jQuery('.header .links').replaceWith(data.toplink);
	        }
	        jQuery.fancybox.close();
		}
	}
	function setLocationAjax(url,id){
		url += 'isAjax/1';
		url = url.replace("checkout/cart","ajax/index");
		jQuery('#ajax_loader'+id).show();
		try {
			jQuery.ajax( {
				url : url,
				dataType : 'json',
				success : function(data) {
					jQuery('#ajax_loader'+id).hide();
         			setAjaxData(data,false);           
				}
			});
		} catch (e) {
		}
	}
Step3: Controller

Now in our controller file we need to put in code first for our ajax/index/addAction . The same code as the previous blog is used here without any changes, so i am just pasting the code here

public function addAction()
	{
		$cart   = $this->_getCart();
		$params = $this->getRequest()->getParams();
		if($params['isAjax'] == 1){
			$response = array();
			try {
				if (isset($params['qty'])) {
					$filter = new Zend_Filter_LocalizedToNormalized(
					array('locale' => Mage::app()->getLocale()->getLocaleCode())
					);
					$params['qty'] = $filter->filter($params['qty']);
				}

				$product = $this->_initProduct();
				$related = $this->getRequest()->getParam('related_product');

				/**
				 * Check product availability
				 */
				if (!$product) {
					$response['status'] = 'ERROR';
					$response['message'] = $this->__('Unable to find Product ID');
				}

				$cart->addProduct($product, $params);
				if (!empty($related)) {
					$cart->addProductsByIds(explode(',', $related));
				}

				$cart->save();

				$this->_getSession()->setCartWasUpdated(true);

				/**
				 * @todo remove wishlist observer processAddToCart
				 */
				Mage::dispatchEvent('checkout_cart_add_product_complete',
				array('product' => $product, 'request' => $this->getRequest(), 'response' => $this->getResponse())
				);

				if (!$cart->getQuote()->getHasError()){
					$message = $this->__('%s was added to your shopping cart.', Mage::helper('core')->htmlEscape($product->getName()));
					$response['status'] = 'SUCCESS';
					$response['message'] = $message;
					//New Code Here
					$this->loadLayout();
					$toplink = $this->getLayout()->getBlock('top.links')->toHtml();
					$sidebar_block = $this->getLayout()->getBlock('cart_sidebar');
					Mage::register('referrer_url', $this->_getRefererUrl());
					$sidebar = $sidebar_block->toHtml();
					$response['toplink'] = $toplink;
					$response['sidebar'] = $sidebar;
				}
			} catch (Mage_Core_Exception $e) {
				$msg = "";
				if ($this->_getSession()->getUseNotice(true)) {
					$msg = $e->getMessage();
				} else {
					$messages = array_unique(explode("\n", $e->getMessage()));
					foreach ($messages as $message) {
						$msg .= $message.'<br/>';
					}
				}

				$response['status'] = 'ERROR';
				$response['message'] = $msg;
			} catch (Exception $e) {
				$response['status'] = 'ERROR';
				$response['message'] = $this->__('Cannot add the item to shopping cart.');
				Mage::logException($e);
			}
			$this->getResponse()->setBody(Mage::helper('core')->jsonEncode($response));
			return;
		}else{
			return parent::addAction();
		}
	}
Options Iframe inside Fancybox

We will now see the code of the iframe which shows up inside the fancybox. The URL of the iframe is ajax/index/options. So so we will edit the controller file and add this code

public function optionsAction(){
		$productId = $this->getRequest()->getParam('product_id');
		// Prepare helper and params
		$viewHelper = Mage::helper('catalog/product_view');

		$params = new Varien_Object();
		$params->setCategoryId(false);
		$params->setSpecifyOptions(false);

		// Render page
		try {
			$viewHelper->prepareAndRender($productId, $this, $params);
		} catch (Exception $e) {
			if ($e->getCode() == $viewHelper->ERR_NO_PRODUCT_LOADED) {
				if (isset($_GET['store'])  && !$this->getResponse()->isRedirect()) {
					$this->_redirect('');
				} elseif (!$this->getResponse()->isRedirect()) {
					$this->_forward('noRoute');
				}
			} else {
				Mage::logException($e);
				$this->_forward('noRoute');
			}
		}
	}

This code is taking from the catalog/product/view action. In short, what this code does is loadLayout() and renderLayout(). But it loads proper handles depending on product type, i.e for configurable product it will load the handle PRODUCT_TYPE_configurable from the catalog.xml
Next we need to add code in the layout file for the action ajax/index/options

<ajax_index_options>
    	<reference name="root">
            <action method="setTemplate"><template>page/empty.phtml</template></action>
        </reference>
        <reference name="head">
        	<action method="addItem"><type>js</type><name>jquery/jquery-1.6.4.min.js</name></action>
        	<action method="addItem"><type>js</type><name>jquery/noconflict.js</name></action>
        </reference>
        <reference name="head">
            <action method="addJs"><script>varien/product.js</script></action>
            <action method="addJs"><script>varien/configurable.js</script></action>

            <action method="addItem"><type>js_css</type><name>calendar/calendar-win2k-1.css</name><params/><!--<if/><condition>can_load_calendar_js</condition>--></action>
            <action method="addItem"><type>js</type><name>calendar/calendar.js</name><!--<params/><if/><condition>can_load_calendar_js</condition>--></action>
            <action method="addItem"><type>js</type><name>calendar/calendar-setup.js</name><!--<params/><if/><condition>can_load_calendar_js</condition>--></action>
        </reference>
        <reference name="content">
            <block type="catalog/product_view" name="product.info" template="ajax/catalog/product/options.phtml">
                <!--
                <action method="addReviewSummaryTemplate"><type>default</type><template>review/helper/summary.phtml</template></action>
                <action method="addReviewSummaryTemplate"><type>short</type><template>review/helper/summary_short.phtml</template></action>
                <action method="addReviewSummaryTemplate"><type>...</type><template>...</template></action>
                -->
                
                <block type="catalog/product_view" name="product.info.addtocart" as="addtocart" template="ajax/catalog/product/view/addtocart.phtml"/>

                <block type="catalog/product_view" name="product.info.options.wrapper" as="product_options_wrapper" template="catalog/product/view/options/wrapper.phtml" translate="label">
                    <label>Info Column Options Wrapper</label>
                    <block type="core/template" name="options_js" template="catalog/product/view/options/js.phtml"/>
                    <block type="catalog/product_view_options" name="product.info.options" as="product_options" template="catalog/product/view/options.phtml">
                        <action method="addOptionRenderer"><type>text</type><block>catalog/product_view_options_type_text</block><template>catalog/product/view/options/type/text.phtml</template></action>
                        <action method="addOptionRenderer"><type>file</type><block>catalog/product_view_options_type_file</block><template>catalog/product/view/options/type/file.phtml</template></action>
                        <action method="addOptionRenderer"><type>select</type><block>catalog/product_view_options_type_select</block><template>catalog/product/view/options/type/select.phtml</template></action>
                        <action method="addOptionRenderer"><type>date</type><block>catalog/product_view_options_type_date</block><template>catalog/product/view/options/type/date.phtml</template></action>
                </block>
                        <block type="core/html_calendar" name="html_calendar" as="html_calendar" template="page/js/calendar.phtml"/>
                    </block>
                <block type="catalog/product_view" name="product.info.options.wrapper.bottom" as="product_options_wrapper_bottom" template="catalog/product/view/options/wrapper/bottom.phtml" translate="label">
                    <label>Bottom Block Options Wrapper</label>
                    <action method="insert"><block>product.tierprices</block></action>
                    <block type="catalog/product_view" name="product.clone_prices" as="prices" template="catalog/product/view/price_clone.phtml"/>
                    <action method="append"><block>product.info.addtocart</block></action>
                    <action method="append"><block>product.info.addto</block></action>
                </block>

                <block type="core/template_facade" name="product.info.container1" as="container1">
                    <action method="setDataByKey"><key>alias_in_layout</key><value>container1</value></action>
                    <action method="setDataByKeyFromRegistry"><key>options_container</key><key_in_registry>product</key_in_registry></action>
                    <action method="append"><block>product.info.options.wrapper</block></action>
                    <action method="append"><block>product.info.options.wrapper.bottom</block></action>
                </block>
                <block type="core/template_facade" name="product.info.container2" as="container2">
                    <action method="setDataByKey"><key>alias_in_layout</key><value>container2</value></action>
                    <action method="setDataByKeyFromRegistry"><key>options_container</key><key_in_registry>product</key_in_registry></action>
                    <action method="append"><block>product.info.options.wrapper</block></action>
                    <action method="append"><block>product.info.options.wrapper.bottom</block></action>
                </block>
                <action method="unsetCallChild"><child>container1</child><call>ifEquals</call><if>0</if><key>alias_in_layout</key><key>options_container</key></action>
                <action method="unsetCallChild"><child>container2</child><call>ifEquals</call><if>0</if><key>alias_in_layout</key><key>options_container</key></action>
            </block>
        </reference>
    </ajax_index_options>

This code is similar to the <catalog_product_view> layout, except i have removed some blocks like description, attributes which are not required.
Next we need to add code to the options.phtml file used in the above block.

<style>
<!--
body.ajax-index-options{
 width:380px;
 padding:0px;
 margin:0px;
}
body.ajax-index-options .product-shop .product-options-bottom .price-box{
 float:left;
}
-->
</style>
<?php $_helper = $this->helper('catalog/output'); ?>
<?php $_product = $this->getProduct(); ?>
<script type="text/javascript">
    var optionsPrice = new Product.OptionsPrice(<?php echo $this->getJsonConfig() ?>);
</script>
<div class="product-view" style="width:380px">
    <div class="product-essential" style="width:330px">
    <form action="<?php echo $this->getSubmitUrl($_product) ?>" method="post" id="product_addtocart_form"<?php if($_product->getOptions()): ?> enctype="multipart/form-data"<?php endif; ?>>
        <div class="no-display">
            <input type="hidden" name="product" value="<?php echo $_product->getId() ?>" />
            <input type="hidden" name="related_product" id="related-products-field" value="" />
        </div>

	
        <div class="product-shop">
            <div class="product-name">
                <h1><?php echo $_helper->productAttribute($_product, $_product->getName(), 'name') ?></h1>
            </div>
    
    		<?php echo $this->getChildHtml('product_type_data') ?>
    		
    		<?php if (!$this->hasOptions()):?>
                <div class="add-to-box">
                    <?php if($_product->isSaleable()): ?>
                        <?php echo $this->getChildHtml('addtocart') ?>
                    <?php endif; ?>
                </div>
            <?php endif; ?>
    		        
            <?php if ($_product->isSaleable() && $this->hasOptions()):?>
                <?php echo $this->getChildChildHtml('container1', '', true, true) ?>
            <?php endif;?>

        </div>
        <div class="clearer"></div>
        <?php if ($_product->isSaleable() && $this->hasOptions()):?>
            <?php echo $this->getChildChildHtml('container2', '', true, true) ?>
        <?php endif;?>
    </form>
    <script type="text/javascript">
    //<![CDATA[
        var productAddToCartForm = new VarienForm('product_addtocart_form');
    	productAddToCartForm.submit = function(button, url) {
		if (this.validator.validate()) {
			var form = this.form;
			var oldUrl = form.action;
			if (url) {
				form.action = url;
			}
			var e = null;
			// Start of our new ajax code
			if (!url) {
				url = jQuery('#product_addtocart_form').attr('action');
			}
			url = url.replace("checkout/cart","ajax/index"); // New Code
			var data = jQuery('#product_addtocart_form').serialize();
			data += '&isAjax=1';
			jQuery('#ajax_loader').show();
			try {
				jQuery.ajax( {
					url : url,
					dataType : 'json',
					type : 'post',
					data : data,
					success : function(data) {
						jQuery('#ajax_loader').hide();
                        parent.setAjaxData(data,true);
					}
				});
			} catch (e) {
			}
			// End of our new ajax code
			this.form.action = oldUrl;
			if (e) {
				throw e;
			}
		}
	}.bind(productAddToCartForm);
    productAddToCartForm.submitLight = function(button, url){
            if(this.validator) {
                var nv = Validation.methods;
                delete Validation.methods['required-entry'];
                delete Validation.methods['validate-one-required'];
                delete Validation.methods['validate-one-required-by-name'];
                if (this.validator.validate()) {
                    if (url) {
                        this.form.action = url;
                    }
                    this.form.submit();
                }
                Object.extend(Validation.methods, nv);
            }
        }.bind(productAddToCartForm);
    //]]>
    </script>
    </div>
</div>

This is all that is required. This module has been tested in magento 1.6 version only, but should work on all magento version. Please provide comments if something is missing or bugs show up, so that we can improve this free module.