At this moment we’re working on 3 projects, all of them require a category list with products that are only configurable and the customer has to be able to choose the options from the configurable and add it directly to the cart without going to the product page.
Step by Step
How can we add all this to the product category list? Sadly it’s not that easy.
- The configurable.js only works with one product at a time, so it won’t work
- The configurable options block can’t be called either: we need a specific block for each product on the category list
Using the previous work from the Inchoo blog I want to detail the procedure.
To solve our problem we need to do the following steps. Note that Inchoo people shared with the world their specific configurable js file for this case.
Step 1
Create a template called configurable_category.phtml and put it on the template/catalog/product/view/type folder of your theme (create the folder if it doesn’t exist). Put the following code in that file:
<?php
$_product = $this->getProduct();
$_attributes = Mage::helper('core')->decorateArray($this->getAllowAttributes());
?>
<?php if ($_product->isSaleable() && count($_attributes)):?>
<dl>
<?php foreach($_attributes as $_attribute): ?>
<dt><label><em>*</em><?php echo $_attribute->getLabel() ?></label></dt>
<dd<?php if ($_attribute->decoratedIsLast){?><?php }?>>
<div>
<select name="super_attribute[<?php echo $_attribute->getAttributeId() ?>]" id="attribute<?php echo $_attribute->getAttributeId() ?>" class="required-entry super-attribute-select_<?php echo $_product->getId()?>">
<option><?php echo $this->__('Choose an Option...') ?></option>
</select>
</div>
</dd>
<?php endforeach; ?>
</dl>
<script type="text/javascript">
var spConfig_<?php echo $_product->getId()?> = new Inchoo_Product.Config(<?php echo $this->getJsonConfig() ?>);
</script>
<?php endif;?>
The differences with the original configurable.phtml file and this one are that now we create a different spConfig object for each product. This object contains all the product configurable options. We also add the class super-attribute-select with the product id at the end.
Step 2
Now create the configurable_list.js file that is going to handle and print the options on the select. Create this file in the js directory (we’ll place it directly in /js):
if(typeof Inchoo_Product =='undefined') {
var Inchoo_Product = {};
}
/**************************** CONFIGURABLE PRODUCT **************************/
Inchoo_Product.Config = Class.create();
Inchoo_Product.Config.prototype = {
initialize: function(config){
this.config = config;
this.taxConfig = this.config.taxConfig;
var settingsClassToSelect = '.super-attribute-select_'+this.config.productId;
this.settings = $$(settingsClassToSelect);
this.state = new Hash();
this.priceTemplate = new Template(this.config.template);
this.prices = config.prices;
this.settings.each(function(element){
Event.observe(element, 'change', this.configure.bind(this))
}.bind(this));
// fill state
this.settings.each(function(element){
var attributeId = element.id.replace(/[a-z]*/, '');
attributeId = attributeId.replace(/_.*/, '');
if(attributeId && this.config.attributes[attributeId]) {
element.config = this.config.attributes[attributeId];
element.attributeId = attributeId;
this.state[attributeId] = false;
}
}.bind(this))
// Init settings dropdown
var childSettings = [];
for(var i=this.settings.length-1;i>=0;i--){
var prevSetting = this.settings[i-1] ? this.settings[i-1] : false;
var nextSetting = this.settings[i+1] ? this.settings[i+1] : false;
if(i==0){
this.fillSelect(this.settings[i])
}
else {
this.settings[i].disabled=true;
}
$(this.settings[i]).childSettings = childSettings.clone();
$(this.settings[i]).prevSetting = prevSetting;
$(this.settings[i]).nextSetting = nextSetting;
childSettings.push(this.settings[i]);
}
// Set default values - from config and overwrite them by url values
if (config.defaultValues) {
this.values = config.defaultValues;
}
var separatorIndex = window.location.href.indexOf('#');
if (separatorIndex != -1) {
var paramsStr = window.location.href.substr(separatorIndex+1);
var urlValues = paramsStr.toQueryParams();
if (!this.values) {
this.values = {};
}
for (var i in urlValues) {
this.values[i] = urlValues[i];
}
}
this.configureForValues();
document.observe("dom:loaded", this.configureForValues.bind(this));
},
configureForValues: function () {
if (this.values) {
this.settings.each(function(element){
var attributeId = element.attributeId;
element.value = (typeof(this.values[attributeId]) == 'undefined')? '' : this.values[attributeId];
this.configureElement(element);
}.bind(this));
}
},
configure: function(event){
var element = Event.element(event);
this.configureElement(element);
},
configureElement : function(element) {
this.reloadOptionLabels(element);
if(element.value){
this.state[element.config.id] = element.value;
if(element.nextSetting){
element.nextSetting.disabled = false;
this.fillSelect(element.nextSetting);
this.resetChildren(element.nextSetting);
}
}
else {
this.resetChildren(element);
}
//this.reloadPrice();
// Calculator.updatePrice();
},
reloadOptionLabels: function(element){
var selectedPrice;
if(element.options[element.selectedIndex].config){
selectedPrice = parseFloat(element.options[element.selectedIndex].config.price)
}
else{
selectedPrice = 0;
}
for(var i=0;i<element.options.length;i++){
if(element.options[i].config){
element.options[i].text = this.getOptionLabel(element.options[i].config, element.options[i].config.price-selectedPrice);
}
}
},
resetChildren : function(element){
if(element.childSettings) {
for(var i=0;i<element.childSettings.length;i++){
element.childSettings[i].selectedIndex = 0;
element.childSettings[i].disabled = true;
if(element.config){
this.state[element.config.id] = false;
}
}
}
},
fillSelect: function(element){
var attributeId = element.id.replace(/[a-z]*/, '');
attributeId = attributeId.replace(/_.*/, '');
var options = this.getAttributeOptions(attributeId);
this.clearSelect(element);
element.options[0] = new Option(this.config.chooseText, '');
var prevConfig = false;
if(element.prevSetting){
prevConfig = element.prevSetting.options[element.prevSetting.selectedIndex];
}
if(options) {
var index = 1;
for(var i=0;i<options.length;i++){
var allowedProducts = [];
if(prevConfig) {
for(var j=0;j<options[i].products.length;j++){
if(prevConfig.config.allowedProducts
&& prevConfig.config.allowedProducts.indexOf(options[i].products[j])>-1){
allowedProducts.push(options[i].products[j]);
}
}
} else {
allowedProducts = options[i].products.clone();
}
if(allowedProducts.size()>0){
options[i].allowedProducts = allowedProducts;
element.options[index] = new Option(this.getOptionLabel(options[i], options[i].price), options[i].id);
element.options[index].config = options[i];
index++;
}
}
}
},
getOptionLabel: function(option, price){
var price = parseFloat(price);
if (this.taxConfig.includeTax) {
var tax = price / (100 + this.taxConfig.defaultTax) * this.taxConfig.defaultTax;
var excl = price - tax;
var incl = excl*(1+(this.taxConfig.currentTax/100));
} else {
var tax = price * (this.taxConfig.currentTax / 100);
var excl = price;
var incl = excl + tax;
}
if (this.taxConfig.showIncludeTax || this.taxConfig.showBothPrices) {
price = incl;
} else {
price = excl;
}
var str = option.label;
if(price){
if (this.taxConfig.showBothPrices) {
str+= ' ' + this.formatPrice(excl, true) + ' (' + this.formatPrice(price, true) + ' ' + this.taxConfig.inclTaxTitle + ')';
} else {
str+= ' ' + this.formatPrice(price, true);
}
}
return str;
},
formatPrice: function(price, showSign){
var str = '';
price = parseFloat(price);
if(showSign){
if(price<0){
str+= '-';
price = -price;
}
else{
str+= '+';
}
}
var roundedPrice = (Math.round(price*100)/100).toString();
if (this.prices && this.prices[roundedPrice]) {
str+= this.prices[roundedPrice];
}
else {
str+= this.priceTemplate.evaluate({price:price.toFixed(2)});
}
return str;
},
clearSelect: function(element){
for(var i=element.options.length-1;i>=0;i--){
element.remove(i);
}
},
getAttributeOptions: function(attributeId){
if(this.config.attributes[attributeId]){
return this.config.attributes[attributeId].options;
}
},
reloadPrice: function(){
var price = 0;
var oldPrice = 0;
for(var i=this.settings.length-1;i>=0;i--){
var selected = this.settings[i].options[this.settings[i].selectedIndex];
if(selected.config){
price += parseFloat(selected.config.price);
oldPrice += parseFloat(selected.config.oldPrice);
}
}
optionsPrice.changePrice('config', {'price': price, 'oldPrice': oldPrice});
optionsPrice.reload();
return price;
if($('product-price-'+this.config.productId)){
$('product-price-'+this.config.productId).innerHTML = price;
}
this.reloadOldPrice();
},
reloadOldPrice: function(){
if ($('old-price-'+this.config.productId)) {
var price = parseFloat(this.config.oldPrice);
for(var i=this.settings.length-1;i>=0;i--){
var selected = this.settings[i].options[this.settings[i].selectedIndex];
if(selected.config){
price+= parseFloat(selected.config.price);
}
}
if (price < 0)
price = 0;
price = this.formatPrice(price);
if($('old-price-'+this.config.productId)){
$('old-price-'+this.config.productId).innerHTML = price;
}
}
}
}
Step 3
In order to load the configurable_list.js file when browsing a category we need to add it into the head block in the catalog.xml file. So now open the catalog.xml of your theme and look for the tag. There we now add the js file. Simply add the following lines inside it:
<reference name="head">
<action method="addJs"><script>configurable_list.js</script></action>
</reference>
Step 4
At this point we have prepared our layout to handle the configurable options block. The only thing that lasts is… that configurable block. We cannot add it to the catalog.xml file, because each configurable block will be different than the previous one (and adding it through the xml layout will show always the same block with the same options). To solve this, we need to dynamically create a block in the phtml file. Simply add these lines before the add to cart button in the catalog/product/list of your theme:
<?php Mage::unregister('product') ?>
<?php Mage::register('product', $_product); ?>
<?php if ( $_product->getTypeId() == 'configurable'): ?>
<?php echo $this->getLayout()->createBlock('catalog/product_view_type_configurable', '', array('template'=> 'catalog/product/view/type/configurable_category.phtml'))->toHtml(); ?>
<?php endif; ?>
It’s very important to «register» and «unregister» the product global object, because the configurable template gets the product from there. If we don’t do this, then the configurable block won’t be able to load the product.
Step 5
At this point, if you load your category product list, you should be the configurable product options and also select them. But the add to cart button won’t use them when adding the product to the cart. By default there is no form tag, but we need to add it to the list.phtml file, inside the foreach loop, concretly inside the div class=”actions”:
<div class="actions">
<?php if($_product->isSaleable()): ?>
<form action="<?php echo $this->helper('checkout/cart')->getAddUrl($_product) ?>" method="post" id="product_addtocart_form" <?php if($_product->getOptions()): ?> enctype="multipart/form-data"<?php endif; ?>>
<?php Mage::unregister('product') ?>
<?php Mage::register('product', $_product); ?>
<?php if ( $_product->getTypeId() == 'configurable'): ?>
<?php echo $this->getLayout()->createBlock('catalog/product_view_type_configurable', '', array('template'=> 'catalog/category/configurable_category.phtml'))->toHtml(); ?>
<?php endif; ?>
<button type="submit" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button>
<?php else: ?>
<?php if ($_product->getIsSalable()): ?>
<p class="availability in-stock"><span><?php echo $this->__('In stock') ?></span></p>
<?php else: ?>
<p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
<?php endif; ?>
<?php endif; ?>
<ul class="add-to-links">
<?php if ($this->helper('wishlist')->isAllow()) : ?>
<li><a href="<?php echo $this->helper('wishlist')->getAddUrl($_product) ?>" class="link-wishlist"><?php echo $this->__('Add to Wishlist') ?></a></li>
<?php endif; ?>
<?php if($_compareUrl=$this->getAddToCompareUrl($_product)): ?>
<li><a href="<?php echo $_compareUrl ?>" class="link-compare"><?php echo $this->__('Add to Compare') ?></a></li>
<?php endif; ?>
</ul>
</form>
</div>
Here the result:

Category list with configurable product options