3

I need to create a custom matrix for every product on Magento 2 product add/edit page. I need to have both the rows and columns to be dynamic so when someone click add button for either row or column there should be a row or column created depending upon his choice.

I tried to find solution for my problem but I don't think Magento 2 supports anything like this however we can only add dynamic rows in Magento 2 but not Dynamic columns either.

So I decided to create a custom block on product page to use third-party extension for achieving my goal and then serializing the matrix data when the save button on product page is clicked and append it to form data somehow and get in my observer/plugin and process it according to my need.

Problems:

  • I tried to add a custom block in Magento 2 product page by this article but I couldn't see that it's rendered on the page (may be some xml related stuff is updated in M2.2).

  • Is there really a way we can do so with Magento2? If no then please suggest me how can I add custom block on product page and how to link the matrix data with product form so when I submit it it can be captured on back-end.

1
  • Hi Umar, Have you figured how to add dynamic rows ? please share if you have done.
    – Ajwad Syed
    Apr 28, 2019 at 9:51

2 Answers 2

3

Yes this is possible. Here is how we can add dynamic rows to Product Edit Page:

Add DataProvider class as:

etc\adminhtml\di.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
<arguments>
    <argument name="modifiers" xsi:type="array">
        <item name="customTab" xsi:type="array">
            <item name="class" xsi:type="string">Vendor\Module\Ui\DataProvider\Product\Form\Modifier\NewField</item>
            <item name="sortOrder" xsi:type="number">10</item>
        </item>
    </argument>
</arguments>

And add DataProvider at:

Vendor\Module\Ui\DataProvider\Product\Form\Modifier:

namespace Vendor\Module\Ui\DataProvider\Product\Form\Modifier;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Ui\Component\Form\Fieldset;
use Magento\Ui\Component\Form\Field;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\DynamicRows;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Element\ActionDelete;
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Element\DataType\Text;
class NewField extends AbstractModifier
{
const FIELD_IS_DELETE = 'is_delete';
const FIELD_SORT_ORDER_NAME = 'sort_order';
const FIELD_NAME_SELECT = 'select_field';

private $locator;
public function __construct(
LocatorInterface $locator
) {
$this->locator = $locator;
}
public function modifyData(array $data)
 {
return $data;
}
public function modifyMeta(array $meta)
{
 $meta = array_replace_recursive(
    $meta,
    [
        'custom_fieldset' => [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Custom Group'),
                        'componentType' => Fieldset::NAME,
                        'dataScope' => 'data.product.custom_fieldset',
                        'collapsible' => true,
                        'sortOrder' => 5,
                    ],
                ],
            ],
            'children' => [
"custom_field" => $this->getSelectTypeGridConfig(10)
],
        ]
    ]
);
return $meta;
}
protected function getSelectTypeGridConfig($sortOrder) {
return [
'arguments' => [
    'data' => [
        'config' => [
            'addButtonLabel' => __('Add Value'),
            'componentType' => DynamicRows::NAME,
            'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows',
            'additionalClasses' => 'admin__field-wide',
            'deleteProperty' => static::FIELD_IS_DELETE,
            'deleteValue' => '1',
            'renderDefaultRecord' => false,
            'sortOrder' => $sortOrder,
        ],
    ],
],
'children' => [
    'record' => [
        'arguments' => [
            'data' => [
                'config' => [
                    'componentType' => Container::NAME,
                    'component' => 'Magento_Ui/js/dynamic-rows/record',
                    'positionProvider' => static::FIELD_SORT_ORDER_NAME,
                    'isTemplate' => true,
                    'is_collection' => true,
                ],
            ],
        ],
        'children' => [
            static::FIELD_NAME_SELECT => $this->getSelectFieldConfig(1),
            static::FIELD_IS_DELETE => $this->getIsDeleteFieldConfig(3)
            //Add as many fields as you want

        ]
    ]
]
];
}
protected function getSelectFieldConfig($sortOrder)
{
return [
    'arguments' => [
        'data' => [
            'config' => [
                'label' => __('Options Select'),
                'componentType' => Field::NAME,
                'formElement' => Select::NAME,
                'component' => 'Magento_Catalog/js/custom-options-type',
                'elementTmpl' => 'ui/grid/filters/elements/ui-select',
                // 'selectType' => 'optgroup',
                'dataScope' => static::FIELD_NAME_SELECT,
                'dataType' => Text::NAME,
                'sortOrder' => $sortOrder,
                'options' => $this->_getOptions(),
                'visible' => true,
                'disabled' => false,
            ],
        ],
    ],
];
}
protected function _getOptions()
{
$options = [
    1 => [
        'label' => __('Option 1'),
        'value' => 1
    ],
    2 => [
        'label' => __('Option 2'),
        'value' => 2
    ],
    3 => [
        'label' => __('Option 3'),
        'value' => 3
    ],
];

return $options;
}
protected function getIsDeleteFieldConfig($sortOrder)
{
return [
    'arguments' => [
        'data' => [
            'config' => [
                'componentType' => ActionDelete::NAME,
                'fit' => true,
                'sortOrder' => $sortOrder
            ],
        ],
    ],
];
}
}
2
  • @ajawad, I need this, i was searching for same. May i know how to save this in database? Aug 19, 2019 at 7:27
  • @Amitkumarsolanki did you find out how to save in database?
    – Mohit Rane
    Dec 28, 2020 at 10:18
2

-> 'discount_frequency' create new product attribute as 'textarea' from admin and assign to attribute set

BELOW CODE TO CREATE DYNAMIC ROW FOR text, textarea, dropdown select, wysiwyg HTML editor **

-> app/code/VENDOR/MODULE/etc/adminhtml/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="discountFrequency" xsi:type="array">
                    <item name="class" xsi:type="string">VENDOR\MODULE\Ui\DataProvider\Product\Form\Modifier\Frequency</item>
                    <item name="sortOrder" xsi:type="number">100</item>     
                </item>
            </argument>
        </arguments>
    </virtualType>   
</config>

-> app/code/VENDOR/MODULE/Ui/DataProvider/Product/Form/Modifier/Frequency.php

<?php
namespace VENDOR\MODULE\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\CatalogInventory\Api\Data\StockItemInterface;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Element\DataType\Number;
use Magento\Ui\Component\Form\Element\DataType\Text;
use Magento\Ui\Component\Form\Element\Textarea;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Field;
use Magento\Ui\Component\Modal;


class Frequency extends AbstractModifier
{
    const DISCOUNT_FREQUENCY_FIELD = 'discount_frequency'; //attribute code

    /**
     * @var LocatorInterface
     */
    private $locator;

    /**
     * @var ArrayManager
     */
    private $arrayManager;

    /**
     * @var array
     */
    private $meta = [];

    /**
     * @var string
     */
    protected $scopeName;   

    /**
     * @param LocatorInterface $locator
     * @param ArrayManager $arrayManager
     */
    public function __construct(
        LocatorInterface $locator,
        ArrayManager $arrayManager,
        $scopeName = ''
    ) {
        $this->locator = $locator;
        $this->arrayManager = $arrayManager;
        $this->scopeName = $scopeName;
    }

    /**
     * {@inheritdoc}
     */
    public function modifyData(array $data)
    {
        $fieldCode = self::DISCOUNT_FREQUENCY_FIELD;

        $model = $this->locator->getProduct();
        $modelId = $model->getId();

        $frequencyData = $model->getDiscountFrequency();

        if ($frequencyData) {
            $frequencyData = json_decode($frequencyData, true);
            $path = $modelId . '/' . self::DATA_SOURCE_DEFAULT . '/'. self::DISCOUNT_FREQUENCY_FIELD;
            $data = $this->arrayManager->set($path, $data, $frequencyData);
        }
        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function modifyMeta(array $meta)
    {
        $this->meta = $meta;
        $this -> initDiscountFrequencyFields();
        return $this->meta;
    }


    protected function initDiscountFrequencyFields()
    {
        $frequencyPath = $this->arrayManager->findPath(
            self::DISCOUNT_FREQUENCY_FIELD,
            $this->meta,
            null,
            'children'
        );

        if ($frequencyPath) {
            $this->meta = $this->arrayManager->merge(
                $frequencyPath,
                $this->meta,
                $this->initFrquencyFieldStructure($frequencyPath)
            );
            $this->meta = $this->arrayManager->set(
                $this->arrayManager->slicePath($frequencyPath, 0, -3)
                . '/' . self::DISCOUNT_FREQUENCY_FIELD,
                $this->meta,
                $this->arrayManager->get($frequencyPath, $this->meta)
            );
            $this->meta = $this->arrayManager->remove(
                $this->arrayManager->slicePath($frequencyPath, 0, -2),
                $this->meta
            );
        }

        return $this;
    }   


    protected function initFrquencyFieldStructure($frequencyPath)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'componentType' => 'dynamicRows',
                        'label' => __('Discount Frequency'),
                        'renderDefaultRecord' => false,
                        'recordTemplate' => 'record',
                        'dataScope' => '',
                        'dndConfig' => [
                            'enabled' => false,
                        ],
                        'disabled' => false,
                        'sortOrder' =>
                            $this->arrayManager->get($frequencyPath . '/arguments/data/config/sortOrder', $this->meta),
                    ],
                ],
            ],
            'children' => [
                'record' => [
                    'arguments' => [
                        'data' => [
                            'config' => [
                                'componentType' => Container::NAME,
                                'isTemplate' => true,
                                'is_collection' => true,
                                'component' => 'Magento_Ui/js/dynamic-rows/record',
                                'dataScope' => '',
                            ],
                        ],
                    ],
                    'children' => [
                        'discount' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'formElement' => Input::NAME,
                                        'componentType' => Field::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Discount percentage'),
                                        'dataScope' => 'discount',
                                        'require' => '1',
                                    ],
                                ],
                            ],
                        ],
                        'frequency' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'formElement' => Textarea::NAME,
                                        'componentType' => Field::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Frequency'),
                                        'dataScope' => 'frequency',
                                        'require' => '1',
                                    ],
                                ],
                            ],
                        ],
                        'frequency_type' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'formElement' => Select::NAME,
                                        'componentType' => Field::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Frequency Type'),
                                        'dataScope' => 'frequency_type',
                                        'options' => $this->FrequencyType(),
                                    ],
                                ],
                            ],
                        ],
                        // ONLY FOR 'wysiwyg' HTML Editor FIELD USE THIS FIELD

                        // 'value' => [
                        //     'arguments' => [
                        //         'data' => [
                        //             'config' => [
                        //                 'componentType' => Field::NAME,
                        //                 'formElement' => 'wysiwyg',
                        //                 'wysiwygConfigData' => [
                        //                     'height' => '100px',
                        //                     'add_variables' => true,
                        //                     'add_widgets' => true,
                        //                     'add_images' => true,
                        //                     'add_directives' => true,
                        //                     'dynamic_id' => true,
                        //                 ],
                        //                 'dataType' => Text::NAME,
                        //                 'label' => __('Value'),
                        //                 'enableLabel' => true,
                        //                 'dataScope' => 'value',
                        //                 'sortOrder' => 30,
                        //                 'validation' => [
                        //                     'required-entry' => true,
                        //                 ],
                        //             ],
                        //         ],
                        //     ],
                        // ],                         
                        'actionDelete' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'componentType' => 'actionDelete',
                                        'dataType' => Text::NAME,
                                        'label' => '',
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ];
    }

    public function FrequencyType()
    {
        $regionOptions = array();
        $regionOptions[] = ['label' =>  'Month', 'value' => 1];
        $regionOptions[] = ['label' =>  'Week', 'value' => 2];
        $regionOptions[] = ['label' =>  'Year', 'value' => 3];
        return $regionOptions;
    }
}
?>

-> app/code/VENDOR/MODULE/etc/adminhtml/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">     
    <event name="catalog_product_save_before">
        <observer name="discount_frequency_save_before" instance="VENDOR\MODULE\Observer\SerializedDiscountFrequency" />
    </event>
</config>

-> app/code/VENDOR/MODULE/Observer/SerializedDiscountFrequency.php

<?php
namespace VENDOR\MODULE\Observer;

use \Magento\Framework\Event\Observer;
use \Magento\Framework\Event\ObserverInterface;

class SerializedDiscountFrequency implements ObserverInterface
{
    const ATTR_DISCOUNT_FREQUENCY_CODE = 'discount_frequency'; //attribute code

    /**
     * @var  \Magento\Framework\App\RequestInterface
     */
    protected $request;

    /**
     * Constructor
     */
    public function __construct(
        \Magento\Framework\App\RequestInterface $request
    )
    {
        $this->request = $request;
    }

    public function execute(Observer $observer)
    {
        /** @var $product \Magento\Catalog\Model\Product */
        $product = $observer->getEvent()->getDataObject();
        $post = $this->request->getPost();
        $post = $post['product'];
        $frequencyData = isset($post[self::ATTR_DISCOUNT_FREQUENCY_CODE]) ? $post[self::ATTR_DISCOUNT_FREQUENCY_CODE] : '';
        $product->setDiscountFrequency($frequencyData);
        $requiredParams = ['discount','frequency']; // PARAMS you defined in Frequency.php file
        if (is_array($frequencyData)) {
            $frequencyData = $this->removeEmptyArray($frequencyData, $requiredParams);
            $product->setDiscountFrequency(json_encode($frequencyData));
        }
    }

    private function removeEmptyArray($discountData, $requiredParams) {
        $requiredParams = array_combine($requiredParams, $requiredParams);
        $reqCount = count($requiredParams);

        foreach ($discountData as $key => $values) {
            $values = array_filter($values);
            $inersectCount = count(array_intersect_key($values, $requiredParams));
            if ($reqCount != $inersectCount) {
                unset($discountData[$key]);
            }  
        }
        return $discountData;
    }
}

-> remove generated and flush cache

The above will generate enter image description here


ALTERNATE METHOD:

-> only create this file and it will save the data automatically app/code/VENDOR/MODULE/view/adminhtml/ui_component/product_form.xml

-> 'discount_frequency' is attribute name

<?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">
    <dataSource name="product_form_data_source">
        <argument name="data" xsi:type="array">
            <item name="js_config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item>
            </item>
        </argument>
        <dataProvider class="Magento\Catalog\Ui\DataProvider\Product\Form\ProductDataProvider"
                      name="product_form_data_source">
            <settings>
                <requestFieldName>id</requestFieldName>
                <primaryFieldName>entity_id</primaryFieldName>
            </settings>
        </dataProvider>
    </dataSource>
    
    <!--Here discount_frequency is attribute name-->
    <fieldset sortOrder="50" name="discount_frequency">
        <settings>
            <label>Fieldset label</label>
            <collapsible>true</collapsible>

            <!--Here discount_frequency is attribute name-->
             <dataScope>data.product.discount_frequency</dataScope>    
        </settings>

        <dynamicRows name="dynamic_row">
            <settings>
                <visible>true</visible>
                <addButtonLabel translate="true">Add</addButtonLabel>
                <additionalClasses>
                    <class name="admin__field-wide">true</class>
                </additionalClasses>
                <componentType>dynamicRows</componentType>
                <elementTmpl>ui/dynamic-rows/templates/grid</elementTmpl>
            </settings>
            <container name="record" component="Magento_Ui/js/dynamic-rows/record">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="isTemplate" xsi:type="boolean">true</item>
                        <item name="is_collection" xsi:type="boolean">true</item>
                        <item name="componentType" xsi:type="string">container</item>
                        <item name="'positionProvider'" xsi:type="string">container_option</item>
                    </item>
                </argument>

                <field name="title" sortOrder="20" formElement="input">
                    <argument name="data" xsi:type="array">
                        <item name="config" xsi:type="array">
                            <item name="source" xsi:type="string">block</item>
                        </item>
                    </argument>
                    <settings>
                        <validation>
                            <rule name="required-entry" xsi:type="boolean">true</rule>
                        </validation>
                        <dataType>text</dataType>
                        <label translate="true">Title</label>
                        <dataScope>title</dataScope>
                    </settings>
                </field>

                <field name="value" formElement="wysiwyg" sortOrder="40">
                    <argument name="data" xsi:type="array">
                        <item name="config" xsi:type="array">
                            <item name="wysiwygConfigData" xsi:type="array">
                                <item name="height" xsi:type="string">100px</item>
                                <item name="add_variables" xsi:type="boolean">true</item>
                                <item name="add_widgets" xsi:type="boolean">true</item>
                                <item name="add_images" xsi:type="boolean">true</item>
                                <item name="add_directives" xsi:type="boolean">true</item>
                                <item name="dynamic_id" xsi:type="boolean">true</item>
                            </item>
                            <item name="source" xsi:type="string">value</item>
                        </item>
                    </argument>
                    <settings>
                        <validation>
                            <rule name="required-entry" xsi:type="boolean">true</rule>
                        </validation>
                        <label translate="true">Value</label>
                        <dataScope>value</dataScope>
                    </settings>
                    <formElements>
                        <wysiwyg class="Magento\Ui\Component\Form\Element\Wysiwyg">
                            <settings>
                                <rows>8</rows>
                                <wysiwyg>true</wysiwyg>
                            </settings>
                        </wysiwyg>
                    </formElements>
                </field>

                <actionDelete>
                    <argument name="data" xsi:type="array">
                        <item name="config" xsi:type="array">
                            <item name="componentType" xsi:type="string">actionDelete</item>
                            <item name="dataType" xsi:type="string">text</item>
                            <item name="fit" xsi:type="boolean">false</item>
                            <item name="label" xsi:type="string">Actions</item>
                            <item name="sortOrder" xsi:type="string">60</item>
                            <item name="additionalClasses" xsi:type="string">data-grid-actions-cell</item>
                            <item name="template" xsi:type="string">Magento_Backend/dynamic-rows/cells/action-delete
                            </item>
                        </item>
                    </argument>
                </actionDelete>
            </container>
        </dynamicRows>
    </fieldset>
</form>
1
  • This a great example of dynamic rows
    – Joel Davey
    Jan 22, 2023 at 19:41

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.