1

We would like to create widgets with layout updates and load these widgets in a specific place on the category list page.

So we would like to create a widget from admin, load a CMS static block and then select in the layout updates specific categories to load the CMS static block.

Problem is that in the default widget you can only select a container, but we want to load the CMS static block on a specific position in the category list between the products. To be specific load it after product number 3. So it should be added in the list.phtml template file.

We do know the method to load the CMS static block directly in the list.phml using <?php echo $this->getLayout()->createBlock('Magento\Cms\Block\Block')->setBlockId('block_id')->toHtml();?> but in that case we can not create easily multiple CMS blocks and also can not select it to be dynamic based on the specific category.

We already found the way how to create a custom container inside widgets; https://magento.stackexchange.com/a/147982/16894

How can we achieve this?

0

2 Answers 2

2
+50

Try the code below. I've created a custom module that provides a content area and a position field. In this field, you can decide on your content and also specify a position where you want to display your content in the product list section. The content area is similar to the default page builder field, allowing you to call widgets and blocks inside the product list. You can also choose category-specific options and define the position number where you want to display the content.

app/code/VendorName/ModuleName

registration.php

<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'VendorName_ModuleName', __DIR__);

app/code/VendorName/ModuleName/etc

module.xml

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="VendorName_ModuleName" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>

app/code/VendorName/ModuleName/Setup

InstallData.php

<?php
declare(strict_types=1);

namespace VendorName\ModuleName\Setup;

use Magento\Catalog\Model\Category;
use Magento\Catalog\Setup\CategorySetupFactory;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

class InstallData implements InstallDataInterface
{
    private $categorySetupFactory;

    public function __construct(CategorySetupFactory $categorySetupFactory)
    {
        $this->categorySetupFactory = $categorySetupFactory;
    }

    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;

        $installer->startSetup();

        $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]);

        $categorySetup->addAttribute(
            Category::ENTITY,
            'custom_content',
            [
                'type' => 'text',
                'label' => 'Custom Content',
                'input' => 'textarea',
                'required' => false,
                'sort_order' => 100,
                'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
                'wysiwyg_enabled' => true,
                'is_html_allowed_on_front' => true,
                'group' => 'General Information',
            ]
        );

        $categorySetup->addAttribute(
            Category::ENTITY,
            'custom_content_position',
            [
                'type'         => 'varchar',
                'label'        => 'Content Position',
                'input'        => 'text',
                'sort_order'   => 100,
                'source'       => '',
                'global'       => 1,
                'visible'      => true,
                'required'     => false,
                'user_defined' => false,
                'default'      => null,
                'group'        => '',
                'backend'      => ''
            ]
        );

        $installer->endSetup();
    }
}

app/code/VendorName/ModuleName/view/adminhtml/ui_component

category_form.xml

<?xml version="1.0" encoding="UTF-8"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <fieldset name="product_line">
        <settings>
            <collapsible>true</collapsible>
            <label translate="true">Additional Content into Products</label>
        </settings>
        <field name="custom_content" template="ui/form/field" sortOrder="10" formElement="wysiwyg">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="wysiwygConfigData" xsi:type="array">
                        <item name="settings" xsi:type="array">
                            <item name="theme_advanced_buttons1" xsi:type="string">bold,italic,|,justifyleft,justifycenter,justifyright,|,fontselect,fontsizeselect,|,forecolor,backcolor,|,link,unlink,image,|,bullist,numlist,|,code</item>
                            <item name="theme_advanced_buttons2" xsi:type="boolean">false</item>
                            <item name="theme_advanced_buttons3" xsi:type="boolean">false</item>
                            <item name="theme_advanced_buttons4" xsi:type="boolean">false</item>
                            <item name="theme_advanced_statusbar_location" xsi:type="boolean">false</item>
                        </item>
                        <item name="height" xsi:type="string">10px</item>
                        <item name="toggle_button" xsi:type="boolean">false</item>
                        <item name="add_variables" xsi:type="boolean">true</item>
                        <item name="add_widgets" xsi:type="boolean">false</item>
                        <item name="add_images" xsi:type="boolean">true</item>
                        <item name="add_directives" xsi:type="boolean">true</item>
                    </item>
                    <item name="source" xsi:type="string">category</item>
                </item>
            </argument>
            <settings>
                <scopeLabel>[STORE VIEW]</scopeLabel>
                <label translate="true">Custom Content</label>
                <dataScope>custom_content</dataScope>
            </settings>
            <formElements>
                <wysiwyg class="Magento\Catalog\Ui\Component\Category\Form\Element\Wysiwyg">
                    <settings>
                        <rows>4</rows>
                        <wysiwyg>true</wysiwyg>
                    </settings>
                </wysiwyg>
            </formElements>
        </field>
        <field name="custom_content_position">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="required" xsi:type="boolean">false</item>
                    <item name="validation" xsi:type="array">
                        <item name="required-entry" xsi:type="boolean">false</item>
                    </item>
                    <item name="sortOrder" xsi:type="number">333</item>
                    <item name="dataType" xsi:type="string">string</item>
                    <item name="formElement" xsi:type="string">input</item>
                    <item name="notice" xsi:type="string" translate="true">Please enter potition count number where you need to show your content into product list.</item>
                    <item name="label" translate="true" xsi:type="string">Content Position</item>
                </item>
            </argument>
        </field>
    </fieldset>
</form>

app/code/VendorName/ModuleName/view/frontend/layout

catalog_category_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="category.products.list">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">VendorName_ModuleName::product/list.phtml</argument>
            </action>
            <block class="VendorName\ModuleName\Block\Product\ProductsAdditionalContent" name="category.products.additional.content"
                   template="VendorName_ModuleName::product/product_content.phtml" />
        </referenceBlock>
    </body>
</page>

app/code/VendorName/ModuleName/Block/Product

ProductsAdditionalContent.php

<?php

namespace VendorName\ModuleName\Block\Product;

use Magento\Framework\View\Element\Template;

class ProductsAdditionalContent extends Template
{
    protected $registry;

    public function __construct(
        Template\Context $context,
        \Magento\Framework\Registry $registry,
        array $data = []
    ) {
        $this->registry = $registry;
        parent::__construct($context, $data);
    }

    public function getCurrentCategory()
    {
        return $this->registry->registry('current_category');
    }
}

app/code/VendorName/ModuleName/view/frontend/templates/product

product_content.phtml

<?php
$category = $block->getCurrentCategory();
$contentPosition = $category->getData('custom_content_position');
?>
<?php if (($contentPosition) && ($block->getData('position') == $contentPosition)): ?>
    <li class="item product product-item">
        <div class="product-item-info">
            <?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->categoryAttribute(
                $category,
                $category->getData('custom_content'),
                'custom_content'
            ) ?>
        </div>
    </li>
<?php endif; ?>

app/code/VendorName/ModuleName/view/frontend/templates/product

list.phtml

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
use Magento\Framework\App\Action\Action;

?>
<?php
/**
 * Product list template
 *
 * @var $block \Magento\Catalog\Block\Product\ListProduct
 * @var \Magento\Framework\Escaper $escaper
 * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer
 */
?>
<?php
$_productCollection = $block->getLoadedProductCollection();
/** @var \Magento\Catalog\Helper\Output $_helper */
$_helper = $block->getData('outputHelper');
?>
<?php if (!$_productCollection->count()): ?>
    <div class="message info empty">
        <div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div>
    </div>
<?php else: ?>
    <?= $block->getToolbarHtml() ?>
    <?= $block->getAdditionalHtml() ?>
    <?php
    if ($block->getMode() === 'grid') {
        $viewMode = 'grid';
        $imageDisplayArea = 'category_page_grid';
        $showDescription = false;
        $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW;
    } else {
        $viewMode = 'list';
        $imageDisplayArea = 'category_page_list';
        $showDescription = true;
        $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::FULL_VIEW;
    }
    /**
     * Position for actions regarding image size changing in vde if needed
     */
    $pos = $block->getPositioned();
    ?>
    <div class="products wrapper <?= /* @noEscape */ $viewMode ?> products-<?= /* @noEscape */ $viewMode ?>">
        <ol class="products list items product-items">
            <?php $contentPosition = 1; ?>
            <?php /** @var $_product \Magento\Catalog\Model\Product */ ?>
            <?php foreach ($_productCollection as $_product): ?>

            <?php // Custom code For Content Show start  ?>
            <?php if ($contentBlock = $block->getChildBlock("category.products.additional.content")) : ?>
                <?php $contentBlock->setData('position', $contentPosition); ?>
                <?= $block->getChildHtml("category.products.additional.content", false); ?>
                <?php $contentPosition++; ?>
            <?php endif; ?>
            <?php // Custom code For Content Show over  ?>

            <li class="item product product-item">
                <div class="product-item-info"
                     id="product-item-info_<?= /* @noEscape */ $_product->getId() ?>"
                     data-container="product-<?= /* @noEscape */ $viewMode ?>">
                    <?php
                    $productImage = $block->getImage($_product, $imageDisplayArea);
                    if ($pos != null) {
                        $position = 'left:' . $productImage->getWidth() . 'px;'
                            . 'top:' . $productImage->getHeight() . 'px;';
                    }
                    ?>
                    <?php // Product Image ?>
                    <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"
                       class="product photo product-item-photo"
                       tabindex="-1">
                        <?= $productImage->toHtml() ?>
                    </a>
                    <div class="product details product-item-details">
                        <?php $_productNameStripped = $block->stripTags($_product->getName(), null, true); ?>
                        <strong class="product name product-item-name">
                            <a class="product-item-link"
                               href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>">
                                <?=/* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name')?>
                            </a>
                        </strong>
                        <?= $block->getReviewsSummaryHtml($_product, $templateType) ?>
                        <?= /* @noEscape */ $block->getProductPrice($_product) ?>

                        <?= $block->getProductDetailsHtml($_product) ?>

                        <div class="product-item-inner">
                            <div class="product actions product-item-actions">
                                <div class="actions-primary">
                                    <?php if ($_product->isSaleable()):?>
                                        <?php $postParams = $block->getAddToCartPostParams($_product); ?>
                                        <form data-role="tocart-form"
                                              data-product-sku="<?= $escaper->escapeHtml($_product->getSku()) ?>"
                                              action="<?= $escaper->escapeUrl($postParams['action']) ?>"
                                              data-mage-init='{"catalogAddToCart": {}}'
                                              method="post">
                                            <?php $options = $block->getData('viewModel')->getOptionsData($_product); ?>
                                            <?php foreach ($options as $optionItem): ?>
                                                <input type="hidden"
                                                       name="<?= $escaper->escapeHtml($optionItem['name']) ?>"
                                                       value="<?= $escaper->escapeHtml($optionItem['value']) ?>">
                                            <?php endforeach; ?>
                                            <input type="hidden"
                                                   name="product"
                                                   value="<?= /* @noEscape */ $postParams['data']['product'] ?>">
                                            <input type="hidden"
                                                   name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>"
                                                   value="<?=
                                                   /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED]
                                                    ?>">
                                            <?= $block->getBlockHtml('formkey') ?>
                                            <button type="submit"
                                                    title="<?= $escaper->escapeHtmlAttr(__('Add to Cart')) ?>"
                                                    class="action tocart primary"
                                                    disabled>
                                                <span><?= $escaper->escapeHtml(__('Add to Cart')) ?></span>
                                            </button>
                                        </form>
                                    <?php else:?>
                                        <?php if ($_product->isAvailable()):?>
                                            <div class="stock available">
                                                <span><?= $escaper->escapeHtml(__('In stock')) ?></span></div>
                                        <?php else:?>
                                            <div class="stock unavailable">
                                                <span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div>
                                        <?php endif; ?>
                                    <?php endif; ?>
                                </div>
                                <?= ($pos && strpos($pos, $viewMode . '-primary')) ?
                                    /* @noEscape */ $secureRenderer->renderStyleAsTag(
                                        $position,
                                        'product-item-info_' . $_product->getId() . ' div.actions-primary'
                                    ) : '' ?>
                                <div data-role="add-to-links" class="actions-secondary">
                                    <?php if ($addToBlock = $block->getChildBlock('addto')): ?>
                                        <?= $addToBlock->setProduct($_product)->getChildHtml() ?>
                                    <?php endif; ?>
                                </div>
                                <?= ($pos && strpos($pos, $viewMode . '-secondary')) ?
                                    /* @noEscape */ $secureRenderer->renderStyleAsTag(
                                        $position,
                                        'product-item-info_' . $_product->getId() . ' div.actions-secondary'
                                    ) : '' ?>
                            </div>
                            <?php if ($showDescription): ?>
                                <div class="product description product-item-description">
                                    <?= /* @noEscape */ $_helper->productAttribute(
                                        $_product,
                                        $_product->getShortDescription(),
                                        'short_description'
                                    ) ?>
                                    <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"
                                       title="<?= /* @noEscape */ $_productNameStripped ?>"
                                       class="action more"><?= $escaper->escapeHtml(__('Learn More')) ?></a>
                                </div>
                            <?php endif; ?>
                        </div>
                    </div>
                </div>
                <?= ($pos && strpos($pos, $viewMode . '-actions')) ?
                /* @noEscape */ $secureRenderer->renderStyleAsTag(
                    $position,
                    'product-item-info_' . $_product->getId() . ' div.product-item-actions'
                ) : '' ?>
            </li>
            <?php endforeach; ?>
        </ol>
    </div>
    <?= $block->getChildBlock('toolbar')->setIsBottom(true)->toHtml() ?>
    <?php // phpcs:ignore Magento2.Legacy.PhtmlTemplate ?>
<?php endif; ?>

OUTPUT enter image description here enter image description here

Click Here To Download Custom Module

7
  • Thanks a lot! This works perfect. Only one last question, how can we change the number of products on a list. The Products per Page on Grid Allowed Values setting. Because this should be reduced by 1 when the block is visible.
    – JGeer
    Dec 4, 2023 at 9:41
  • Yes, you can find your required settings here: magento.stackexchange.com/a/198196/82670
    – Msquare
    Dec 4, 2023 at 11:18
  • Thanks! We do know this setting, but we would like to change the value, when the block form your module is displayed. How to achieve that?
    – JGeer
    Dec 4, 2023 at 12:11
  • @JGeer We need to check where the 'Products per Page on Grid Allowed Values' are coming from and customize it. Perhaps you could ask a new question related to your requirement; that would be helpful for us.
    – Msquare
    Dec 4, 2023 at 12:17
  • Thanks. But it's really connected to this request, because only then we would like to change the value. Is this an easy fix using custom xml layout? I just award the bounty points
    – JGeer
    Dec 4, 2023 at 15:11
1

The way I see it, layout updates would be a worse and less extensible way of doing it. It would be better if you can just add an additional field/parameter in the widget and have that drive the location of the widget within list.phtml something like below

<?xml version="1.0" encoding="UTF-8"?>
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget id="your_widget_id" class="Vendor\Module\Model\YourWidgetClass">
       <parameters>
            <parameter name="location" xsi:type="text" visible="true">
                 <label translate="true">Location</label>
            </parameter>
        </parameters>
    </widget>
</widgets>

Above code will create a text field in your widget whose values you can set to the desired product after which you want to render the widget. Now, you can load the widget in list.phtml and show it in the location based on the value in the loop within list.phtml which iterates through products, something like below;

<?php $iterator = 1; ?>        
<?php foreach ($_productCollection as $_product): ?>
// code that renders product 
    <?php $myWidget = $block->getLayout()->createBlock('Vendor\Module\Model\YourWidgetClass') ?>
    <?php if ($myWidget->getLocation() == $iterator): ?>
        <?= $myWidget->toHtml() ?>
    <?php endif ?>
    <?php $iterator++ ?>
<?php endforeach; ?>

Note that I have removed most of the code from list.phtml, and just kept the code that would get the point across.

EDIT

The best way of adding this in category would be creating a category attribute something like this - You will have to create a new category attribute, something like this - https://www.mageplaza.com/devdocs/magento-2-category-attributes-programmatically

Now you have to create a widget with your desired location value and add it to a cms block. You can add cms block's identifier to the the category attribute we just created and just render the block on the category page by getting the value of cateogry attribute, this will in turn render your widget with desired parameters as well.

5
  • Thanks a lot! Sounds like a good method to achieve this. To create the input in our widget, we then need to create a custom module. Can you tell what we need to create this custom module?
    – JGeer
    Nov 28, 2023 at 9:06
  • Problem then will be the widget ID I guess. Because we will create multiple widgets, to load different widgets based on specific categories. So the widget ID can not be static. How to achieve that?
    – JGeer
    Nov 28, 2023 at 9:07
  • You can always create a new widget instance for each category, also you will need a field on category itself where you can choose the widget. While rendering the category listing page just get the widget id from category object and render the corresponding widget. Nov 28, 2023 at 14:19
  • Thanks! can you provide a example how to achieve this?
    – JGeer
    Nov 30, 2023 at 8:48
  • Check the edit. Dec 1, 2023 at 5:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.