I need to display the only Size attribute drop-down on a category page listing. Size is a configurable option & Catalog Input Type for Store Owner value is Dropdown.
I have enabled Used in Product Listing property enable.
This All depends if your looking to have swatches or dropdowns the answer for dropdowns is not so easy.
First create a custom module. And add the following to your di, file this class stops configurable products getting the correct add to cart.
<type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable">
<plugin name="remove_possible_by_from_list" type="GremlinTech\CategoryConfigurable\Plugin\Configurable" sortOrder="1" disabled="false"/>
</type>
Then create a custom php class Under Plugin called configurable
<?php
namespace GremlinTech\CategoryConfigurable\Plugin;
class Configurable
{
public function afterIsPossibleBuyFromList(\Magento\ConfigurableProduct\Model\Product\Type\Configurable $subject, $result)
{
return true;
}
}
Next step is to overwrite the Magento_Catalog::templates/list.phtml and add the following under the hidden input fields that are already there.
<input type="hidden" name="selected_configurable_option" value="" />
<input type="hidden" name="related_product" id="related-products-field" value="" />
<input type="hidden" name="item" value="<?= /* @noEscape */ $postParams['data']['product'] ?>" />
Also you need to modify the form element itself to look like below
<form data-role="tocart-form"
id="product_addtocart_form-<?=$_product->getId(); ?>"
data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>"
action="<?= $block->getAddToCartUrl($_product) ?>"
<?php if ($_product->getOptions()) :?> enctype="multipart/form-data"<?php endif; ?>
method="post">
Now your list view has its actual fields, you need to add a catalog_category_view.xml layout either in the theme or in the module created above, and add the following. This will call the product view configurable template into your list.
<referenceBlock name="category.products.list">
<block class="Magento\ConfigurableProduct\Block\Product\View\Type\Configurable" name="category.product.configurable" as="configurable_options" template="Magento_Catalog::product/list/view/type/options/configurable.phtml" />
</referenceBlock>
You will need to do the same for catalogsearch_result_index.xml as the xml is different for search results though uses the same catalog list template
<referenceBlock name="search_result_list">
<block class="Magento\ConfigurableProduct\Block\Product\View\Type\Configurable" name="category.product.configurable" as="configurable_options" template="Magento_Catalog::product/list/view/type/options/configurable.phtml" />
</referenceBlock>
Now we have this you need to overide the Magento_Catalog::product/list/view/type/options/configurable.phtml template again in the theme or module, you then need to make it look like the below.
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
?>
<?php
/** @var $block \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable*/
$_product = $block->getProduct();
if($_product->getTypeId() === 'configurable') :
$_attributes = $block->decorateArray($block->getAllowAttributes());
?>
<?php if ($_product->isSaleable() && count($_attributes)) :?>
<?php foreach ($_attributes as $_attribute) : ?>
<div class="field configurable required">
<label class="label" for="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>">
<span><?= $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel()) ?></span>
</label>
<div class="control">
<select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]"
data-selector="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]"
data-validate="{required:true}"
id="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>"
class="super-attribute-select">
<option value=""><?= $block->escapeHtml(__('Choose an Option...')) ?></option>
</select>
</div>
</div>
<?php endforeach; ?>
<script type="text/x-magento-init">
{
"[data-role=priceBox][data-price-box=product-id-<?= $block->escapeJs($_product->getId()) ?>]": {
"priceBox": {}
}
}
</script>
<script type="text/x-magento-init">
{
"#product_addtocart_form-<?=$_product->getId(); ?>": {
"configurable": {
"selectSimpleProduct" : ".cart-price.product-<?=$_product->getId(); ?> [name=selected_configurable_option]",
"priceHolderSelector" : ".cart-price.product-<?=$_product->getId(); ?> > .price-box",
"spConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>,
"gallerySwitchStrategy": "<?= $block->escapeJs($block->getVar(
'gallery_switch_strategy',
'Magento_ConfigurableProduct'
) ?: 'replace'); ?>"
}
},
"*" : {
"Magento_ConfigurableProduct/js/catalog-add-to-cart": {}
}
}
</script>
<?php endif;?>
<?php endif;?>
What has changed from default configurable.phtml
We added the below init script this makes the prices dynamic on the grid itself, without the below prices fail to inizalize and configurable options with static prices would fail.
<script type="text/x-magento-init">
{
"[data-role=priceBox][data-price-box=product-id-<?= $block->escapeJs($_product->getId()) ?>]": {
"priceBox": {}
}
}
</script>
We have also modified the paramatersin the configurable x-init to make them more specific to the grid inidviual items otherwise all prices would change on each item and so would the hidden configurable_selected hidden item.
Final Thing to do is to call the options in the loop in your list.phtml again, within the scope of the form,
<?= $block->getChildBlock("configurable_options")->setData('product', $_product)->toHtml(); ?>
What we are doing is getting our child block and setting the specific list item as the product on the block otherwise it would try and use the registry like it does on product view.
Thats it hope this helps.
unsetData()
, so in this case it's $block->getChildBlock("configurable_options")->unsetData()->setData('product', $_product)->toHtml()
. My guess is that once data has been set against a block, Magento caches it, so it has to be explicitly cleared.
In the latest Magento version, seems like Magento will add configurable options on the Listing page only if there is at least one Swatch type attribute used in the configurable product.
In case if you want to show the dropdowns as options on PLP where only attributes of type dropdown used in a configurable product please follow the below-mentioned steps:
Add preference on vendor/magento/module-swatches/Block/Product/Renderer/Listing/Configurable.php
Override method _toHtml
Add another condition to check if it's swatch and dropdown.
if ($this->isProductHasSwatchAttribute()) { $output = parent::_toHtml(); }
That's all, it should work fine now.
I would like to improvise the answer given by pradeep mishra, In latest Magento version ( I checked this modification only on this version 2.3.5-p2 ) the configurable dropdown field option will be displayed automatically if the product has one or more swatch option(s). So, in order to bring the dropdown irrespective of the swatch option, we can customize this class
\Magento\Swatches\Block\Product\Renderer\Listing\Configurable
with the help of the custom module, we can write a preference in our di.xml file to override the class as below.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" type="Vendor\Module\Block\Rewrite\Product\Renderer\Listing\Configurable" />
</config>
and then we need to override the class file as below
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types = 1);
namespace Vendor\Module\Block\Rewrite\Product\Renderer\Listing;
use Magento\Catalog\Block\Product\Context;
use Magento\Catalog\Helper\Product as CatalogProduct;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Layer\Resolver;
use Magento\ConfigurableProduct\Helper\Data;
use Magento\ConfigurableProduct\Model\ConfigurableAttributeData;
use Magento\Customer\Helper\Session\CurrentCustomer;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Json\EncoderInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Stdlib\ArrayUtils;
use Magento\Swatches\Helper\Data as SwatchData;
use Magento\Swatches\Helper\Media;
use Magento\Swatches\Model\SwatchAttributesProvider;
/**
* Swatch renderer block in Category page
*
* @api
* @since 100.0.2
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Configurable extends \Magento\Swatches\Block\Product\Renderer\Listing\Configurable{
/**
* @var \Magento\Framework\Locale\Format
*/
private $localeFormat;
/**
* @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices
*/
private $variationPrices;
/**
* @var \Magento\Catalog\Model\Layer\Resolver
*/
private $layerResolver;
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @param Context $context
* @param ArrayUtils $arrayUtils
* @param EncoderInterface $jsonEncoder
* @param Data $helper
* @param CatalogProduct $catalogProduct
* @param CurrentCustomer $currentCustomer
* @param PriceCurrencyInterface $priceCurrency
* @param ConfigurableAttributeData $configurableAttributeData
* @param SwatchData $swatchHelper
* @param Media $swatchMediaHelper
* @param array $data
* @param SwatchAttributesProvider|null $swatchAttributesProvider
* @param \Magento\Framework\Locale\Format|null $localeFormat
* @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices
* @param Resolver $layerResolver
*/
public function __construct(
Context $context,
ArrayUtils $arrayUtils,
EncoderInterface $jsonEncoder,
Data $helper,
CatalogProduct $catalogProduct,
CurrentCustomer $currentCustomer,
PriceCurrencyInterface $priceCurrency,
ConfigurableAttributeData $configurableAttributeData,
SwatchData $swatchHelper,
Media $swatchMediaHelper,
array $data = [],
SwatchAttributesProvider $swatchAttributesProvider = null,
\Magento\Framework\Locale\Format $localeFormat = null,
\Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null,
Resolver $layerResolver = null
) {
parent::__construct(
$context,
$arrayUtils,
$jsonEncoder,
$helper,
$catalogProduct,
$currentCustomer,
$priceCurrency,
$configurableAttributeData,
$swatchHelper,
$swatchMediaHelper,
$data,
$swatchAttributesProvider,
$localeFormat,
$variationPrices,
$layerResolver
);
$this->localeFormat = $localeFormat ?: ObjectManager::getInstance()->get(
\Magento\Framework\Locale\Format::class
);
$this->variationPrices = $variationPrices ?: ObjectManager::getInstance()->get(
\Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class
);
$this->layerResolver = $layerResolver ?: ObjectManager::getInstance()->get(Resolver::class);
}
/**
* @inheritdoc
*/
protected function getRendererTemplate()
{
return $this->_template;
}
protected function isProductHasSwatchAttribute()
{
return true;
}
/**
* Render block hook
*
* Produce and return block's html output
*
* @return string
* @since 100.1.5
*/
protected function _toHtml()
{
$output = '';
if ($this->isProductHasSwatchAttribute()) {
$output = parent::_toHtml();
}
return $output;
}
/**
* @inheritdoc
*/
protected function getSwatchAttributesData()
{
$result = [];
$swatchAttributeData = parent::getSwatchAttributesData();
foreach ($swatchAttributeData as $attributeId => $item) {
if (!empty($item['used_in_product_listing'])) {
$result[$attributeId] = $item;
}
}
return $result;
}
/**
* Composes configuration for js
*
* @return string
*/
public function getJsonConfig()
{
$this->unsetData('allow_products');
return parent::getJsonConfig();
}
/**
* Composes configuration for js price format
*
* @return string
* @since 100.2.3
*/
public function getPriceFormatJson()
{
return $this->jsonEncoder->encode($this->localeFormat->getPriceFormat());
}
/**
* Composes configuration for js price
*
* @return string
* @since 100.2.3
*/
public function getPricesJson()
{
return $this->jsonEncoder->encode(
$this->variationPrices->getFormattedPrices($this->getProduct()->getPriceInfo())
);
}
/**
* Do not load images for Configurable product with swatches due to its loaded by request
*
* @return array
* @since 100.2.0
*/
protected function getOptionImages()
{
return [];
}
/**
* Add images to result json config in case of Layered Navigation is used
*
* @return array
* @SuppressWarnings(PHPMD.RequestAwareBlockMethod)
* @since 100.2.0
*/
protected function _getAdditionalConfig()
{
$config = parent::_getAdditionalConfig();
if (!empty($this->getRequest()->getQuery()->toArray())) {
$config['preSelectedGallery'] = $this->getProductVariationWithMedia(
$this->getProduct(),
$this->getRequest()->getQuery()->toArray()
);
}
return $config;
}
/**
* Get product images for chosen variation based on selected product attributes
*
* @param Product $configurableProduct
* @param array $additionalAttributes
* @return array
*/
private function getProductVariationWithMedia(
Product $configurableProduct,
array $additionalAttributes = []
) {
$configurableAttributes = $this->getLayeredAttributesIfExists($configurableProduct, $additionalAttributes);
if (!$configurableAttributes) {
return [];
}
$product = $this->swatchHelper->loadVariationByFallback($configurableProduct, $configurableAttributes);
return $product ? $this->swatchHelper->getProductMediaGallery($product) : [];
}
/**
* Get product attributes which uses in layered navigation and present for given configurable product
*
* @param Product $configurableProduct
* @param array $additionalAttributes
* @return array
*/
private function getLayeredAttributesIfExists(Product $configurableProduct, array $additionalAttributes)
{
$configurableAttributes = $this->swatchHelper->getAttributesFromConfigurable($configurableProduct);
$layeredAttributes = [];
$configurableAttributes = array_map(
function ($attribute) {
return $attribute->getAttributeCode();
},
$configurableAttributes
);
$commonAttributeCodes = array_intersect(
$configurableAttributes,
array_keys($additionalAttributes)
);
foreach ($commonAttributeCodes as $attributeCode) {
$layeredAttributes[$attributeCode] = $additionalAttributes[$attributeCode];
}
return $layeredAttributes;
}
}
here I've modified this file by setting the following protected method to return true, that will help to display the configurable dropdown option in list view page
protected function isProductHasSwatchAttribute()
{
return true;
}
and then in your theme you need to override this file
app/design/frontend/Vendor/theme/Magento_Swatches/templates/product/listing/renderer.phtml
<script type="text/x-magento-init">
{
"[data-role=swatch-option-<?= $block->escapeJs($productId) ?>]": {
"Magento_Swatches/js/swatch-renderer": {
"selectorProduct": ".product-item-details",
"onlySwatches": false,
"enableControlLabel": false,
"numberToShow": <?= $block->escapeJs($block->getNumberSwatchesPerProduct()) ?>,
"jsonConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>,
"jsonSwatchConfig": <?= /* @noEscape */ $block->getJsonSwatchConfig() ?>,
"mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>",
"jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>,
"showTooltip": 1
}
}
}
</script>
In which we have to set "onlySwatches" to false. And finally clear cache. you will see the dropdown shows on the list view page of the magento catalog.
In order to bring the dropdown option in related products, cross-sell etc,. on the product detail page you need to edit the following file
/app/design/frontend/Vendor/theme/Magento_Catalog/templates/product/list/items.phtml
and place the following code inside the foreach statement
<?php if($_item->getTypeId() == \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE){
$swatchBlock = $this->getLayout()->createBlock("Magento\Swatches\Block\Product\Renderer\Listing\Configurable")->setTemplate("Magento_Swatches::product/listing/renderer.phtml");
echo $swatchBlock->setProduct($_item)->toHtml();
} ?>
Makesure that the file is exact copy of the Magento core in some case if the file is customised if it is custom theme. to make sure review if the following codes are present
At the top of the file
use Magento\Catalog\ViewModel\Product\Listing\PreparePostData;
use Magento\Framework\App\ActionInterface;
/* @var $block \Magento\Catalog\Block\Product\AbstractProduct */
/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */
Inside foreach statement where add-to-cart functions resides ( just after if ($_item->isSaleable()): )
getTypeInstance()->isPossibleBuyFromList($_item)):?> <button
class="action tocart primary button btn-cart pull-left-none"
data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>">
<span class="tooltip"><?= $block->escapeHtml(__('Add to Cart')) ?></span>
</button>
<?php else :?>
<?php
/** @var $viewModel PreparePostData */
$viewModel = $block->getViewModel();
$postArray = $viewModel->getPostData(
$block->escapeUrl($block->getAddToCartUrl($_item)),
['product' => $_item->getEntityId()]
);
$value = $postArray['data'][ActionInterface::PARAM_NAME_URL_ENCODED];
?>
<form data-role="tocart-form"
data-product-sku="<?= $block->escapeHtmlAttr($_item->getSku()) ?>"
action="<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"
method="post">
<input type="hidden" name="product"
value="<?= /* @noEscape */ (int)$_item->getEntityId() ?>">
<input type="hidden"
name="<?= /* @noEscape */ ActionInterface::PARAM_NAME_URL_ENCODED?>"
value="<?= /* @noEscape */ $value ?>">
<?= $block->getBlockHtml('formkey') ?>
<button type="submit"
title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"
class="action tocart primary button btn-cart pull-left-none">
<span class="tooltip"><?= $block->escapeHtml(__('Add to Cart')) ?></span>
</button>
</form>
<?php endif; ?>
And at the bottom of the file copy the following script ( Thanks Rakesh Jesadiya for the findings :-) referred from here ) which will make add to cart function work as expected.
<script type="text/javascript">
require(["jquery"],function($){
jQuery(window).load(function(){
jQuery('.swatch-option').click(function(){
var optionid = jQuery(this).attr('option-id');
var contextClass = jQuery(this).parents('.swatch-attribute').parent().attr('class');
var getProductId = contextClass.replace ( /[^\d.]/g, '');
var currentAttributeid = jQuery(this).parents('.swatch-attribute').attr('attribute-id');
var formAction = jQuery('.'+contextClass).siblings('.product-item-inner').find("form input[name='super_attribute["+currentAttributeid+"]']").val(optionid);
});
jQuery('.product-item-inner form').each(function(){
var getId = jQuery(this).find("input[name='product']").val();
//check for swatch is available or not?
if(jQuery('.swatch-opt-'+getId).length > 0){
console.log('getProductid '+getId);
var getLength = jQuery('.swatch-opt-'+getId).children().length;
var domInput = '';
for(inc = 0; inc < getLength; inc++){
var getAttributeId = jQuery('.swatch-opt-'+getId).children().eq(inc).attr('attribute-id');
console.log('getAttributeId '+getAttributeId);
domInput += "<input type='hidden' value='' name='super_attribute["+getAttributeId+"]'>";
}
jQuery('.swatch-opt-'+getId).siblings('.product-item-inner').find('form').append(domInput);
} //if end here
});
});
});
</script>
Now the dropdown will start showing in the related product as well as add to cart work as expected.