How to create a dynamic generated admin fields?

In your module, you need this code bellow; Assuming that your module name is : {Vendor}/{Name} => Amir/Prince

app/code/Amir/Prince/etc/adminhtml/system.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="am_general" translate="label" sortOrder="10">
            <label>Prince configuration</label>
        </tab>
        <section id="am_shipping" translate="label" sortOrder="130" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top am-general-admin-tab-general</class>
            <label>Section 1</label>
            <tab>am_general</tab>
            <resource>Amir_Prince::general_config</resource>
            <group id="prices" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
                <label>Shipping cost settings</label>
                <field id="add_item" translate="label" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Shipping cost based on price</label>
                    <frontend_model>Amir\Prince\Block\Adminhtml\Menu\Field\AdditionalItem</frontend_model>
                    <backend_model>Amir\Prince\Block\Adminhtml\Menu\Config\Backend\AdditionalItem</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>

app/code/Amir/Prince/Block/Adminhtml/Menu/Field/AdditionalItem.php

<?php 
namespace Amir\Prince\Block\Adminhtml\Menu\Field;

use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;

class AdditionalItem extends AbstractFieldArray
{
    protected $_typeblockOptions;
    protected $_cmsblockOptions;
    /**
     * {@inheritdoc}
     */
    protected function _prepareToRender()
    {
        $this->addColumn(
            'fromprice',
            [
                'label' => __('From Price'),
                'size' => '200px',
                'class' => 'required-entry' //remove this class if the field is not required
            ]
        );

        $this->addColumn(
            'toprice',
            [
                'label' => __('To Price'),
                'size' => '200px',
                'class' => 'required-entry'
            ]
        );

        $this->addColumn(
            'productcost',
            [
                'label' => __('Product Cost'),
                'size' => '200px',
                'class' => 'required-entry'
            ]
        );

        $this->_addAfter = false;
        $this->_addButtonLabel = __('Add');
    }
}

app/code/Amir/Prince/Block/Adminhtml/Menu/Config/Backend/AdditionalItem.php

<?php

namespace Amir\Prince\Block\Adminhtml\Menu\Config\Backend;

use Magento\Framework\App\Cache\TypeListInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Value as ConfigValue;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use Magento\Framework\Serialize\SerializerInterface;

class AdditionalItem extends ConfigValue
{

    protected $serializer;

    public function __construct(
        SerializerInterface $serializer,
        Context $context,
        Registry $registry,
        ScopeConfigInterface $config,
        TypeListInterface $cacheTypeList,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        $this->serializer = $serializer;
        parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
    }

    public function beforeSave()
    {
        $value = $this->getValue();
        unset($value['__empty']);
        $encodedValue = $this->serializer->serialize($value);

        $this->setValue($encodedValue);
    }

    protected function _afterLoad()
    {
        /** @var string $value */
        $value = $this->getValue();
        if($value) {
            $decodedValue = $this->serializer->unserialize($value);

            $this->setValue($decodedValue);
        }

    }
}

Now to get the values in FRONT

Let's doing things neatly:

app/code/Amir/Prince/Helper/Data.php

<?php

namespace Amir\Prince\Helper;

use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\App\Config\ScopeConfigInterface;


class Data extends \Magento\Framework\App\Helper\AbstractHelper

{
    protected $_serialize;
    protected $_scopeConfig;

    public function __construct(
        Json $serialize,
        ScopeConfigInterface $scopeConfig
    ) {
        $this->_serialize = $serialize;
        $this->_scopeConfig = $scopeConfig;
    }


    public function getStoreConfig($config_path)
    {
        return $this->_serialize->unserialize($this->_scopeConfig->getValue(
            $config_path,
            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
        ));
    }
}

Now in the PHTML that you want to manage that data, in theme for exemple:

app/design/frontend/Vendor/theme/Magento_Theme/templates/custom_shipping.phtml

$helper = $this->helper('Amir\Prince\Helper\Data');
$customShippingCost = $helper->getStoreConfig('am_shipping/prices/add_item');
var_dump($customShippingCost);

enter image description here

Enjoy !


Magento 2 allows you to create custom fields in system configurations. There are two components needed:

Frontend model – the element renderer class responsible for the field view. This class must implement the Magento\Framework\Data\Form\Element\Renderer\RendererInterface interface. The custom renderer class needs to be specified in the frontend_model node of the field definition in system.xml file.

Backend model – the config data model class provides the backend logic for system configuration value. In most cases, it is used for the field data preparation before saving and after loading. The configuration field backend model must implement the Magento\Framework\App\Config\ValueInterface interface. Let’s try to create a simple custom field. There is already an available frontend model for dynamic fields block. We just need to extend the Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray class and override the _prepareToRender method in order to add columns to dynamic fields block, change “add button” label etc.

namespace Div\DynamicFields\Block\Adminhtml\Form\Field;

use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;

/**
 * Class AdditionalEmail
 */
class AdditionalEmail extends AbstractFieldArray
{
    /**
     * {@inheritdoc}
     */
    protected function _prepareToRender()
    {
        $this->addColumn('firstname', ['label' => __('First Name'), 'class' => 'required-entry']);
        $this->addColumn('lastname', ['label' => __('Last Name')]);
        $this->addColumn('email',['label' => __('Email'), 'size' => '50px', 'class' => 'required-entry validate-email']);
        $this->_addAfter = false;
        $this->_addButtonLabel = __('Add Email');
    }
}

The data is saved in one record of the core_config_data table in the database for each system configuration field. That’s why we need to serialize the data before saving and unserialize this value back after loading. The backend model extends Magento\Framework\App\Config\Value base config data model class and overrides two methods: beforeSave and _afterLoad. The field value will be serialized to JSON since the Magento\Framework\Serialize\Serializer\Json is used as serializer by default starting from Magento 2.2.x version and up.

namespace Div\DynamicFields\Config\Backend;

use Magento\Framework\App\Cache\TypeListInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Value as ConfigValue;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use Magento\Framework\Serialize\SerializerInterface;

/**
 * Class AdditionalEmail
 */
class AdditionalEmail extends ConfigValue
{
    /**
     * Json Serializer
     *
     * @var SerializerInterface
     */
    protected $serializer;

    /**
     * ShippingMethods constructor
     *
     * @param SerializerInterface $serializer
     * @param Context $context
     * @param Registry $registry
     * @param ScopeConfigInterface $config
     * @param TypeListInterface $cacheTypeList
     * @param AbstractResource|null $resource
     * @param AbstractDb|null $resourceCollection
     * @param array $data
     */
    public function __construct(
        SerializerInterface $serializer,
        Context $context,
        Registry $registry,
        ScopeConfigInterface $config,
        TypeListInterface $cacheTypeList,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        $this->serializer = $serializer;
        parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
    }

    /**
     * Prepare data before save
     *
     * @return void
     */
    public function beforeSave()
    {
        /** @var array $value */
        $value = $this->getValue();
        unset($value['__empty']);
        $encodedValue = $this->serializer->serialize($value);

        $this->setValue($encodedValue);
    }

    /**
     * Process data after load
     *
     * @return void
     */
    protected function _afterLoad()
    {
        /** @var string $value */
        $value = $this->getValue();
        if($value){
            $decodedValue = $this->serializer->unserialize($value);
            $this->setValue($decodedValue);
        }
    }
}

Now we need to define the field in etc/adminhtml/system.xml file of our module.

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="sample" translate="label" sortOrder="50">
            <label>Sample</label>
        </tab>
        <section id="atwix_sample" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top</class>
            <label>Div Sample</label>
            <tab>sample</tab>
            <resource>Div_DynamicFields::dynamic_fields</resource>
            <group id="additional" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Additional</label>
                <field id="emails" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Additional Emails</label>
                    <frontend_model>Div\DynamicFields\Block\Adminhtml\Form\Field\AdditionalEmail</frontend_model>
                    <backend_model>Div\DynamicFields\Config\Backend\AdditionalEmail</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>

Let’s check the result in Stores -> Configuration -> SAMPLE -> Div Sample -> Additional.