-> '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 
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>