Today we’re going to look at the advantages of using configurable products and how we can update how their prices display on Magento 2 product listing pages to provide our customers with a more transparent shopping experience. Before we dig deeper, let’s talk briefly about what configurable products are.
Configurable products differ from simple products in that they provide the customer with numerous options that ultimately reduce down to a simple product choice. The power of configurable products really shines on product listing pages, where a single configurable product may act as a doorway to literally hundreds of simple product variations. For instance, a t-shirt configurable product with 10 color options and 3 size options would essentially allow the customer to choose from one of 30 tangible simple products, without being overwhelmed with all 30 products on the listing page itself.
The Problem
While configurable products allow us to more elegantly guide our customers’ shopping experience, they do have one notable drawback: Their potential price variations don’t translate well to listing pages. Instead, Magento natively shows the lowest possible price for each configurable product. That works well if all of our configurable product options cost the same, but if they don’t, the price Magento shows on the listing page could be very misleading. If our t-shirt prices range from $20 to $60 depending on what size and color was selected, we’re doing our customers a disservice by showing the $20 price, when what they really want may end up being $60. There’s no sense in drawing your customers in with the lowest price just to sideswipe them with a much higher price once they’ve selected their options. Showing price ranges on listing pages makes the possible price of an item more transparent, allowing your customers to more quickly compare products and continue on to checkout, which means higher conversion rates for you.
The Solution
Now that we’ve identified our general objective, we can line out the functionality we hope to see when all is said and done:
- Configurable products will display a price range rather than just the lowest possible price.
- The price range will show the lowest child product price and the highest child product price possible.
- The price range will take into account special pricing. The lowest child product special price will be shown as the minimum price in the price range.
Let’s get our bearings on how the pricing logic can be altered by looking at how this is handled in the core Magento modules. Before we jump into the directory structure, I want to briefly introduce two overarching concepts that you’ll see with regard to pricing logic: render classes and templates, and amount render classes and templates. The render classes and templates are responsible for the layout and conditional logic associated with whether a particular price type shows (e.g., strikethrough price or regular price). Meanwhile, the render amount classes and templates are responsible for the actual currency formatted amount that shows and any structured data markup needed to communicate the final amount to search engine crawlers.
That said, now we’re ready to review the primary Magento module responsible for price rendering, Mage_Catalog. If you open the module, you’ll see a directory called Pricing
. This houses the price render classes and the price amount render classes. In view/base/templates/product/price
you’ll see the price render templates, and if you dive into the view/base/templates/product/price/amount
folder you’ll find the price amount render template.
The last piece of the puzzle resides in the view/base/layout
directory: catalog_product_prices.xml
. This file tells Magento which render and amount render classes and templates to use. Before we continue, let’s state the obvious here: the code and configuration contained in view/base
will apply to both the frontend and admin areas of the site, while code and configuration contained in view/frontend
and view/adminhtml
will apply to only the frontend and adminhtml areas, respectively.
Now that we have our bearings on the base module and files involved, let’s talk about how the Magento_ConfigurableProduct module updates that behavior for configurable products. If you look in the Magento_ConfigurableProduct module, you’ll see that it has its own Pricing
directory that allows Magento to calculate the various prices (min, max, final, …), while taking into account the given configurable product’s child product prices. Most notable is the presence of view/base/layout/catalog_product_prices.xml
. In this file, you’ll see that the Magento_ConfigurableProduct module is passing in a new argument, “configurable”, to the render.product.prices
block. When Magento is calculating a final price for a configurable product, it will use the values for render_template
and amount_render_template
set here rather than the values set under the “default” in the Magento_Catalog module. These are precisely the values we’ll want to override to show price ranges on our product listing pages.
Step 1:
Let’s create a new module for this called MyCompany_ConfigurableProductPriceRanges. If you’re new to Magento 2, here’s a link to the official Magento 2 dev doc on how to create a module. At minimum, you’ll need these three files in the module root: registration.php
, composer.json
, and etc/module.xml
. Note that we’ll need to list Mage_ConfigurableProduct
as a dependency in etc/module.xml
.
Step 2:
To override the default configurable product price behavior on the product listing page, we’ll need to add our own catalog_product_prices.xml
layout file. This will allow us to specify a new render template and amount render template. Add the following code to our new module in view/frontend/layout/catalog_product_prices.xml
.
<?xml version="1.0"?>
<layout xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
<referenceBlock name="render.product.prices">
<arguments>
<argument name="configurable" xsi_type="array">
<item name="prices" xsi_type="array">
<item name="final_price" xsi_type="array">
<item name="render_template" xsi_type="string">MyCompany_ConfigurableProductPriceRanges::product/price/price_range.phtml</item>
<item name="amount_render_template" xsi_type="string">MyCompany_ConfigurableProductPriceRanges::product/price/amount/default.phtml</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</layout>
Step 3:
In the last step, we created a layout file to override the templates normally used for configurable product prices. Now we need to create those template files in our module. First, create the render template in view/frontend/templates/product/price/price_range.phtml
with the following code:
<?php
/** @var MagentoCatalogPricingRenderFinalPriceBox $block */
$idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : '';
$schema = ($block->getZone() == 'item_view') ? true : false;
$saleableProduct = $block->getSaleableItem();
$finalPriceModel = $block->getPriceType('final_price');
$minimumPrice = ($block->hasSpecialPrice()) ? $finalPriceModel->getAmount()->getValue() : $saleableProduct->getMinPrice();
$maximumPrice = $saleableProduct->getMaxPrice();
$renderAmount = $block->renderAmount($finalPriceModel->getAmount(), [
'price_id' => $block->getPriceId('product-price-' . $idSuffix),
'price_type' => 'finalPrice',
'include_container' => true,
'schema' => $schema,
'min_price' => $minimumPrice,
'max_price' => $maximumPrice,
]);
echo $renderAmount
?>
Here we’ve retrieved the minimum and maximum prices from the saleable product and also taken into account the possibility of a special price on one or more of the child products. This template will get hit first and pass our minimum and maximum price values to the amount render template via the $block->renderAmount()
call. That means we’ll need to create our amount render template next. In view/frontend/templates/product/price/amount/default.phtml
place the following code:
<?php /** @var MagentoFrameworkPricingRenderAmount $block */ ?>
<span class="price-container <?php /* @escapeNotVerified */ echo $block->getAdjustmentCssClasses() ?>"
<?php echo $block->getSchema() ? ' itemprop="offers" itemscope itemtype="http://schema.org/Offer"' : '' ?>>
<?php if ($block->getDisplayLabel()): ?>
<span class="price-label"><?php /* @escapeNotVerified */ echo $block->getDisplayLabel(); ?></span>
<?php endif; ?>
<span <?php if ($block->getPriceId()): ?> id="<?php /* @escapeNotVerified */ echo $block->getPriceId() ?>"<?php endif;?>
<?php echo($block->getPriceDisplayLabel()) ? 'data-label="' . $block->getPriceDisplayLabel() . $block->getPriceDisplayInclExclTaxes() . '"' : '' ?>
data-price-amount="<?php /* @escapeNotVerified */ echo $block->getDisplayValue(); ?>"
data-price-type="<?php /* @escapeNotVerified */ echo $block->getPriceType(); ?>"
class="price-wrapper <?php /* @escapeNotVerified */ echo $block->getPriceWrapperCss(); ?>"
<?php echo $block->getSchema() ? ' itemprop="price"' : '' ?>>
<?php if ($block->getMinPrice() && $block->getMaxPrice() && $block->getMinPrice() < $block->getMaxPrice()): ?>
<?php /* @escapeNotVerified */ echo $block->formatCurrency($block->getMinPrice(), (bool)$block->getIncludeContainer()) . ' - ' . $block->formatCurrency($block->getMaxPrice(), (bool)$block->getIncludeContainer()) ?>
<?php else: ?>
<?php /* @escapeNotVerified */ echo $block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()) ?>
<?php endif; ?>
</span>
<?php if ($block->hasAdjustmentsHtml()): ?>
<?php echo $block->getAdjustmentsHtml() ?>
<?php endif; ?>
<?php if ($block->getSchema()): ?>
<meta itemprop="priceCurrency" content="<?php /* @escapeNotVerified */ echo $block->getDisplayCurrencyCode()?>" />
<?php endif; ?>
</span>
This file is essentially a direct copy of what you would find in the Magento_Catalog module’s view/base/templates/product/price/amount/default.phtml
file. Our only modification is the addition of the if ($block->getMinPrice() && $block->getMaxPrice() && $block->getMinPrice() < $block->getMaxPrice()):
section. If the template block has access to the minimum and maximum price and they differ in value, our template will show those prices as a price range (i.e., minPrice minus maxPrice) instead of the default singular price.
Step 4:
- Enable the module with
bin/magento module:enable MyCompany_ConfigurableProductPriceRanges
- Clear your cache with
bin/magento cache:flush
Conclusion
That’s it! At this point you’ll be able to load any native product listing page on your site and see price ranges for all configurable products that have simple products with differing prices. Here’s a screenshot of this code in use:
Your customers will now be able to rapidly compare products and find what they need faster, all from the product listing page. Happy coding!
Full Code
Want the benefits without the work? You’re in luck – you can download the full code from this article on github.