Magento, Magento 2

Magento2 add dynamic-row with select in system configuration

During working with different customer we might have requirement of capturing multiple values irrespective of the number of values from system configuration, at that time we need to use the dynamic field inside backend configuration so the admin can add multiple values using the backend.

To have this facility enabled on your Magento 2 store, we have developed codes. These codes will mitigate the function of adding dynamic-row multiselect in the configuration.

Now, follow the below steps to add dynamic-row multi-select in system configuration:

Step 1: First, we need to create a Registration.php file inside our extension on this path.
app\code\Ace\OrderDeliveryDate

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

Step 2: After that, we need to create Module.xml file inside extension etc folder
app\code\Ace\OrderDeliveryDate\etc

<?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="Ace_OrderDeliveryDate" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Sales"/>
            <module name="Magento_StoreGraphQl"/>
        </sequence>
    </module>
</config>

Step 3: Now, we have to create one more file system.xml inside the same etc folder.
app\code\Ace\OrderDeliveryDate\etc\adminhtml

<?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>
        <section id="orderdeliverydate" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10" translate="label">
            <label>OrderDeliveryDate</label>
            <tab>ace</tab>
            <resource>Ace_OrderDeliveryDate::config_ace_orderdeliverydate</resource>
            <group id="general" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10" translate="label">
                <label>General</label>
                <field id="timeslots" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Time Slots</label>
                    <frontend_model>Ace\OrderDeliveryDate\Model\Config\Source\Timeslots</frontend_model>
                    <backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>

Step 4: Now, we have to create one more file Timeslots.php inside the Model folder. app\code\Ace\OrderDeliveryDate\Model\Config\Source\Timeslots

<?php
namespace Ace\OrderDeliveryDate\Model\Config\Source;

use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;
use Magento\Framework\DataObject;
use Ace\OrderDeliveryDate\Block\Adminhtml\Form\Field\TimeslotsColumn;

class Timeslots extends AbstractFieldArray
{

    
    private $fromRenderer;
    private $toRenderer;
   
    protected function _prepareToRender()
    {
        $this->addColumn('from', [
            'label' => __('From'),
            'renderer' => $this->getFromRenderer(),
            'class' => 'required-entry',
            'style' => 'width:150px'
        ]);

        $this->addColumn('to', [
            'label' => __('To'),
            'renderer' => $this->getToRenderer(),
            'class' => 'required-entry',
            'style' => 'width:150px'
        ]);

        $this->addColumn('note', ['label' => __('Note'), 'style' => 'width:200px']);
        $this->_addAfter = false;
        $this->_addButtonLabel = __('Add');
    }

    protected function _prepareArrayRow(DataObject $row)
    {
        $options = [];

        $froms = $row->getFrom();
       
        if (is_array($froms) && count($froms) > 0) {
            foreach ($froms as $from) {
                $options['option_' . $this->getFromRenderer()->calcOptionHash($from)]
                    = 'selected="selected"';
            }
        }

     
        $tos = $row->getTo();
        if (is_array($tos) && count($tos) > 0) {
            foreach ($tos as $to) {
                $options['option_' . $this->getToRenderer()->calcOptionHash($to)]
                    = 'selected="selected"';
            }
        }

        $row->setData('option_extra_attrs', $options);
    }

    private function getFromRenderer()
    {
        if (!$this->fromRenderer) {
            $this->fromRenderer = $this->getLayout()->createBlock(                
                TimeslotsColumn::class,
                '',
                ['data' => ['is_render_to_js_template' => true]]
            );
        }
            
        return $this->fromRenderer;
    }

    private function getToRenderer()
    {
        if (!$this->toRenderer) {
            $this->toRenderer = $this->getLayout()->createBlock(
                TimeslotsColumn::class,
                '',
                ['data' => ['is_render_to_js_template' => true]]
            );
        }
            
        return $this->toRenderer;
    }
}

Step 5: Now, we have to create one more file TimeslotsColumn.php inside the Block folder. app\code\Ace\OrderDeliveryDate\Block\Adminhtml\Form\Field\TimeslotsColumn

<?php
declare(strict_types=1);
namespace Ace\OrderDeliveryDate\Block\Adminhtml\Form\Field;

use Magento\Framework\View\Element\Context;
use Magento\Framework\View\Element\Html\Select;
use Ace\OrderDeliveryDate\Model\Config\Source\TimeslotsColumn as TimeslotsColumnSource;

class TimeslotsColumn extends Select
{
    private $columnScource;

    public function __construct(
        Context $context,
        TimeslotsColumnSource $columnScource,
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->columnScource = $columnScource;
    }

    public function setInputName($value)
    {
        return $this->setName($value . '[]');
    }

    public function _toHtml(): string
    {
        if (!$this->getOptions()) {
            $this->setOptions($this->columnScource->toOptionArray());
        }
        return parent::_toHtml();
    }
}

Step 6: Now, we have to create one more file TimeslotsColumn.php inside the Model folder. app\code\Ace\OrderDeliveryDate\Model\Config\Source\TimeslotsColumn

<?php
declare(strict_types=1);
namespace Ace\OrderDeliveryDate\Model\Config\Source;

class TimeslotsColumn implements \Magento\Framework\Data\OptionSourceInterface
{
    /**
     * Return array of options as value-label pairs
     *
     * @return void
     */
    public function toOptionArray()
    {
        return [
            ['value' => '12:00 AM','label' => '12:00 AM'],
            ['value' => '12:30 AM','label' => '12:30 AM'],
            ['value' => '01:00 AM','label' => '01:00 AM'],
            ['value' => '01:30 AM','label' => '01:30 AM'],
            ['value' => '02:00 AM','label' => '02:00 AM'],
            ['value' => '02:30 AM','label' => '02:30 AM'],
            ['value' => '03:00 AM','label' => '03:00 AM'],
            ['value' => '03:30 AM','label' => '03:30 AM'],
            ['value' => '04:00 AM','label' => '04:00 AM'],
            ['value' => '04:30 AM','label' => '04:30 AM'],
            ['value' => '05:00 AM','label' => '05:00 AM'],
            ['value' => '05:30 AM','label' => '05:30 AM'],
            ['value' => '06:00 AM','label' => '06:00 AM'],
            ['value' => '06:30 AM','label' => '06:30 AM'],
            ['value' => '07:00 AM','label' => '07:00 AM'],
            ['value' => '07:30 AM','label' => '07:30 AM'],
            ['value' => '08:00 AM','label' => '08:00 AM'],
            ['value' => '08:30 AM','label' => '08:30 AM'],
            ['value' => '09:00 AM','label' => '09:00 AM'],
            ['value' => '09:30 AM','label' => '09:30 AM'],
            ['value' => '10:00 AM','label' => '10:00 AM'],
            ['value' => '10:30 AM','label' => '10:30 AM'],
            ['value' => '11:00 AM','label' => '11:00 AM'],
            ['value' => '11:30 AM','label' => '11:30 AM'],
            ['value' => '12:00 PM','label' => '12:00 PM'],
            ['value' => '12:30 PM','label' => '12:30 PM'],
            ['value' => '01:00 PM','label' => '01:00 PM'],
            ['value' => '01:30 PM','label' => '01:30 PM'],
            ['value' => '02:00 PM','label' => '02:00 PM'],
            ['value' => '02:30 PM','label' => '02:30 PM'],
            ['value' => '03:00 PM','label' => '03:00 PM'],
            ['value' => '03:30 PM','label' => '03:30 PM'],
            ['value' => '04:00 PM','label' => '04:00 PM'],
            ['value' => '04:30 PM','label' => '04:30 PM'],
            ['value' => '05:00 PM','label' => '05:00 PM'],
            ['value' => '05:30 PM','label' => '05:30 PM'],
            ['value' => '06:00 PM','label' => '06:00 PM'],
            ['value' => '06:30 PM','label' => '06:30 PM'],
            ['value' => '07:00 PM','label' => '07:00 PM'],
            ['value' => '07:30 PM','label' => '07:30 PM'],
            ['value' => '08:00 PM','label' => '08:00 PM'],
            ['value' => '08:30 PM','label' => '08:30 PM'],
            ['value' => '09:00 PM','label' => '09:00 PM'],
            ['value' => '09:30 PM','label' => '09:30 PM'],
            ['value' => '10:00 PM','label' => '10:00 PM'],
            ['value' => '10:30 PM','label' => '10:30 PM'],
            ['value' => '11:00 PM','label' => '11:00 PM'],
            ['value' => '11:30 PM','label' => '11:30 PM'],
        ];
    }

    /**
     * Return array of options as value-label pairs
     *
     * @return void
     */
    public function toArray()
    {
        return   [
            '12:00 AM' => '12:00 AM',
            '12:30 AM' => '12:30 AM',
            '01:00 AM' => '01:00 AM',
            '01:30 AM' => '01:30 AM',
            '02:00 AM' => '02:00 AM',
            '02:30 AM' => '02:30 AM',
            '03:00 AM' => '03:00 AM',
            '03:30 AM' => '03:30 AM',
            '04:00 AM' => '04:00 AM',
            '04:30 AM' => '04:30 AM',
            '05:00 AM' => '05:00 AM',
            '05:30 AM' => '05:30 AM',
            '06:00 AM' => '06:00 AM',
            '06:30 AM' => '06:30 AM',
            '07:00 AM' => '07:00 AM',
            '07:30 AM' => '07:30 AM',
            '08:00 AM' => '08:00 AM',
            '08:30 AM' => '08:30 AM',
            '09:00 AM' => '09:00 AM',
            '09:30 AM' => '09:30 AM',
            '10:00 AM' => '10:00 AM',
            '10:30 AM' => '10:30 AM',
            '11:00 AM' => '11:00 AM',
            '11:30 AM' => '11:30 AM',
            '12:00 PM' => '12:00 PM',
            '12:30 PM' => '12:30 PM',
            '01:00 PM' => '01:00 PM',
            '01:30 PM' => '01:30 PM',
            '02:00 PM' => '02:00 PM',
            '02:30 PM' => '02:30 PM',
            '03:00 PM' => '03:00 PM',
            '03:30 PM' => '03:30 PM',
            '04:00 PM' => '04:00 PM',
            '04:30 PM' => '04:30 PM',
            '05:00 PM' => '05:00 PM',
            '05:30 PM' => '05:30 PM',
            '06:00 PM' => '06:00 PM',
            '06:30 PM' => '06:30 PM',
            '07:00 PM' => '07:00 PM',
            '07:30 PM' => '07:30 PM',
            '08:00 PM' => '08:00 PM',
            '08:30 PM' => '08:30 PM',
            '09:00 PM' => '09:00 PM',
            '09:30 PM' => '09:30 PM',
            '10:00 PM' => '10:00 PM',
            '10:30 PM' => '10:30 PM',
            '11:00 PM' => '11:00 PM',
            '11:30 PM' => '11:30 PM'
        ];
    }
}