Back to Top

Create product dynamic row attribute (Magento 2)

Updated 24 September 2024

In this blog we will learn how we can create custom attributes in Magento 2 with dynamic rows at the admin add/edit product page.

This blog, will cover each and every point to create dynamic attributes so stay till the end.

First, we need to create a basic module.

After creating the module our module structure will look something like this.

Module folder structure.
Module Structure

Now create the di.xml file in the etc folder. This file is used to start the rendering of the dynamic row attribute when the product add/edit page opens at the admin end.

Searching for an experienced
Magento 2 Company ?
Find out More

File Path: VendorName/ModuleName/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="add-attributes" xsi:type="array">
                    <item name="class" xsi:type="string">Webkul\CustomAttribute\Ui\DataProvider\Product\Form\Modifier\DynamicRowAttribute</item>
                    <item name="sortOrder" xsi:type="number">20</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

Now we will create a UI file that is actually responsible for rendering the attribute with the dynamic rows.

File Path: VendorName/ModuleName/Ui/DataProvider/Product/Form/Modifier/DynamicRowAttribute.php

<?php

namespace Webkul\CustomAttribute\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\Field;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\DynamicRows;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Element\DataType\Text;

use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory as AttributeSetCollection;

use Magento\Framework\Stdlib\ArrayManager;

class DynamicRowAttribute extends AbstractModifier
{
    public const PRODUCT_ATTRIBUTE_CODE = 'dynamic_row_attribute';
    public const FIELD_IS_DELETE = 'is_delete';
    public const FIELD_SORT_ORDER_NAME = 'sort_order';

    /**
     * Dependency Initilization
     *
     * @param LocatorInterface $locator
     * @param AttributeSetCollection $attributeSetCollection
     * @param \Magento\Framework\Serialize\SerializerInterface $serializer
     * @param ArrayManager $arrayManager
     */
    public function __construct(
        private LocatorInterface $locator,
        protected AttributeSetCollection $attributeSetCollection,
        protected \Magento\Framework\Serialize\SerializerInterface $serializer,
        protected ArrayManager $arrayManager,
    ) {
    }

    /**
     * Modify Data
     *
     * @param array $data
     * @return array
     */
    public function modifyData(array $data)
    {
        $fieldCode = self::PRODUCT_ATTRIBUTE_CODE;

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

        $highlightsData = $model->getDynamicRowAttribute();

        if ($highlightsData) {
            $highlightsData = $this->serializer->unserialize($highlightsData, true);
            $path = $modelId . '/' . self::DATA_SOURCE_DEFAULT . '/' . $fieldCode;
            $data = $this->arrayManager->set($path, $data, $highlightsData);
        }
        return $data;
    }

    /**
     * Modify Meta
     *
     * @param array $meta
     * @return array
     */
    public function modifyMeta(array $meta)
    {
        $highlightsPath = $this->arrayManager->findPath(
            self::PRODUCT_ATTRIBUTE_CODE,
            $meta,
            null,
            'children'
        );

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

        return $meta;
    }

    /**
     * Add Attribute Grid Config
     *
     * @param int $sortOrder
     * @return array
     */
    protected function addAttributeGridConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'addButtonLabel' => __('Add Attribute'),
                        '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' => [
                        'attribute_type' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'componentType' => Field::NAME,
                                        'formElement' => Input::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Attribute Type'),
                                        'enableLabel' => true,
                                        'dataScope' => 'attribute_type',
                                        'sortOrder' => 40,
                                        'validation' => [
                                            'required-entry' => true,
                                        ],
                                    ],
                                ],
                            ],
                        ],
                        'attribute_lable' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'componentType' => Field::NAME,
                                        'formElement' => Input::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Attribute'),
                                        'enableLabel' => true,
                                        'dataScope' => 'attribute_lable',
                                        'sortOrder' => 40,
                                        'validation' => [
                                            'required-entry' => true,
                                        ],
                                    ],
                                ],
                            ],
                        ],
                        'actionDelete' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'componentType' => 'actionDelete',
                                        'dataType' => Text::NAME,
                                        'label' => '',
                                        'sortOrder' => 50,
                                    ],
                                ],
                            ],
                        ],
                    ]
                ]
            ]
        ];
    }

    /**
     * Get attraction highlights dynamic rows structure
     *
     * @param array $meta
     * @param string $highlightsPath
     * @return array
     */
    protected function initHighlightFieldStructure($meta, $highlightsPath)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'componentType' => 'dynamicRows',
                        'label' => __('Custom Dynamic Rows'),
                        'renderDefaultRecord' => false,
                        'recordTemplate' => 'record',
                        'dataScope' => '',
                        'dndConfig' => [
                            'enabled' => false,
                        ],
                        'disabled' => false,
                        'sortOrder' =>
                        $this->arrayManager->get($highlightsPath . '/arguments/data/config/sortOrder', $meta),
                    ],
                ],
            ],
            'children' => [
                'record' => [
                    'arguments' => [
                        'data' => [
                            'config' => [
                                'componentType' => Container::NAME,
                                'isTemplate' => true,
                                'is_collection' => true,
                                'component' => 'Magento_Ui/js/dynamic-rows/record',
                                'dataScope' => '',
                            ],
                        ],
                    ],
                    'children' => [
                        'title' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'formElement' => Input::NAME,
                                        'componentType' => Field::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Title'),
                                        'dataScope' => 'title',
                                        'require' => '1',
                                    ],
                                ],
                            ],
                        ],

                        'value' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'formElement' => Input::NAME,
                                        'componentType' => Field::NAME,
                                        'dataType' => Text::NAME,
                                        'label' => __('Value'),
                                        'dataScope' => 'value',
                                        'require' => '1',
                                    ],
                                ],
                            ],
                        ],
                        'actionDelete' => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'componentType' => 'actionDelete',
                                        'dataType' => Text::NAME,
                                        'label' => '',
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ];
    }
}

Now we will create a new file to create product attribute that stores the data of the dynamic rows in the JSON format.

File Path: VendorName\ModuleName\Setup\Patch\Data\ProductAttribute.php

<?php

namespace Webkul\CustomAttribute\Setup\Patch\Data;

use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Catalog\Setup\CategorySetupFactory;
use Magento\Eav\Setup\EavSetupFactory;
use Webkul\CustomAttribute\Ui\DataProvider\Product\Form\Modifier\DynamicRowAttribute;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;

class ProductAttribute implements DataPatchInterface
{
    /**
     * Dependency Initilization
     *
     * @param ModuleDataSetupInterface $moduleDataSetup
     * @param CategorySetupFactory $categorySetupFactory
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(
        private ModuleDataSetupInterface $moduleDataSetup,
        private CategorySetupFactory $categorySetupFactory,
        private EavSetupFactory $eavSetupFactory
    ) {
    }

    /**
     * @inheritdoc
     */
    public function apply()
    {
        $eavSetup = $this->eavSetupFactory->create();
        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            DynamicRowAttribute::PRODUCT_ATTRIBUTE_CODE,
            [
                'label' => 'Custom Dynamic Rows',
                'type'  => 'text',
                'default'  => '',
                'input' => 'text',
                'required' => false,
                'sort_order' => 1,
                'user_defined' => true,
                'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
                'used_in_product_listing' => true,
                'visible_on_front' => true,
                'visible' => true
            ]
        );
        $eavSetup->addAttributeToGroup(
            \Magento\Catalog\Model\Product::ENTITY,
            'Default',
            'General', // group
            DynamicRowAttribute::PRODUCT_ATTRIBUTE_CODE,
            1000 // sort order
        );
    }

    /**
     * @inheritdoc
     */
    public static function getDependencies()
    {
        return [];
    }

    /**
     * @inheritdoc
     */
    public function getAliases()
    {
        return [];
    }
}

Till now our Dynamic Row Attribute will look something like this.

Dynamic rows not visible after saving the product.

But wait if we save our product now our dynamic row values will not save and simply disappear.

So to save these dynamic row values we need to add an event observer to the event catalog_product_save_before that will save the dynamic row value in our created product dynamic row attribute.

So, now we will create our event and observer file to save our dynamic rows value.

Event File Path: VendorName\ModuleName\etc\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="product_dynamic_row_attribute" instance="Webkul\CustomAttribute\Observer\SaveDynamicRowValues" />
    </event>
</config>

Observer File Path: VendorName\ModuleName\Observer\SaveDynamicRowValues.php

<?php

namespace Webkul\CustomAttribute\Observer;

use Webkul\CustomAttribute\Ui\DataProvider\Product\Form\Modifier\DynamicRowAttribute;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\App\RequestInterface;

class SaveDynamicRowValues implements ObserverInterface
{
    /**
     * Dependency Initilization
     *
     * @param RequestInterface $request
     * @param \Magento\Framework\Serialize\SerializerInterface $serializer
     */
    public function __construct(
        protected RequestInterface $request,
        protected \Magento\Framework\Serialize\SerializerInterface $serializer,
    ) {
    }

    /**
     * Execute
     *
     * @param Observer $observer
     * @return this
     */
    public function execute(Observer $observer)
    {
        /** @var $product \Magento\Catalog\Model\Product */
        $product = $observer->getEvent()->getDataObject();
        $wholeRequest = $this->request->getPost();
        $post = $wholeRequest['product'];

        if (empty($post)) {
            $post = !empty($wholeRequest['variables']['product']) ? $wholeRequest['variables']['product'] : [];
        }
        $highlights = isset(
            $post[DynamicRowAttribute::PRODUCT_ATTRIBUTE_CODE]
        ) ? $post[DynamicRowAttribute::PRODUCT_ATTRIBUTE_CODE] : '';

        $product->setDynamicRowAttribute($highlights);
        $requiredParams = ['title', 'value'];
        if (is_array($highlights)) {
            $highlights = $this->removeEmptyArray($highlights, $requiredParams);
            $product->setDynamicRowAttribute($this->serializer->serialize($highlights));
        }
    }

    /**
     * Function to remove empty array from the multi dimensional array
     *
     * @param array $attractionData
     * @param array $requiredParams
     * @return array
     */
    private function removeEmptyArray($attractionData, $requiredParams)
    {
        $requiredParams = array_combine($requiredParams, $requiredParams);
        $reqCount = count($requiredParams);

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

Now after saving again the Dynamic Rows values will look something like this.

Dynamic rows visible after saving the product.

That is all you need to create the product dynamic rows attribute at the admin end. Hope this will help.

File structure: till now

File structure till now.
File Structure

Conclusion

This blog has provided a comprehensive guide on creating custom attributes with dynamic rows in Magento 2’s admin add/edit product page.

By following the steps from setting up a basic module to implementing necessary files such as di.xml, UI components, and event observers you can enhance the product management experience.

The dynamic row attributes not only improve data entry flexibility but also ensure that all values are saved correctly.

This tutorial aims to empower you with the knowledge to implement these features seamlessly.

For further learning and assistance, you can explore more Magento 2 tutorials or consider hiring a dedicated Magento developer. Thank you for reading, and happy coding!

You may visit other Magento 2 tutorials on the Webkul blog. We also offer end-to-end services on Magento 2 and We are an Adobe commerce partner as well.

Also, you may Hire Magento Developer for dedicated customization services.

Thanks 🙂

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home