How to add your own dynamic field to the store configuration

Dear Friend and Visitor,

Today I’m going to show you how to create (and easily extend) a custom Store Configuration that will allow you to save a table with selects and multiple rows into a single core_config_data row in the database.

You probably felt somewhere across your path a need for a dynamic and easily extensible config field.

Magento isn’t too helpful with the default types (i.e text, date, etc), but fortunately, they prepared a workaround via the Magento_Config module.

Desired Result we are looking for is following:

Desired Result

Click here to zoom.

To render this table we will use Magento_Config’s AbstractFieldArray by extending:

\Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray:class

Step 1: Create new module

app/code/PeterRusin/DynamicField/registration.php

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'PeterRusin_DynamicField',
    __DIR__
);

app/code/PeterRusin/DynamicField/etc/module.xml

<?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="PeterRusin_DynamicField"/>
</config>

Step 2: Create DynamicField class

app/code/PeterRusin/DynamicField/Block/Adminhtml/System/Config/Form/Field/DynamicField.php

<?php
declare(strict_types=1);

namespace PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field;

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

class DynamicField extends AbstractFieldArray
{
    protected function _prepareToRender()
    {
        $this->addColumn('comment', [
            'label' => __('Comment'),
            'class' => 'required-entry',
            'style' => 'width:75px'
        ]);

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

Step 3: Add DynamicField to system.xml

app/code/PeterRusin/DynamicField/etc/adminhtml/system.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="peterrusin" sortOrder="200">
            <label>Peter Rusin</label>
        </tab>
        <section id="peterrusin_dynamicfield" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1"
                 showInStore="1">
            <label>Dynamic Field</label>
            <tab>peterrusin</tab>
            <resource>PeterRusin_DynamicField::system_config</resource>
            <group id="general" translate="label" type="text" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>General Settings</label>
                <field id="dynamicfield_example" type="select" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Dynamic Field Example</label>
                    <frontend_model>PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field\DynamicField</frontend_model>
                    <backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>

app/code/PeterRusin/DynamicField/acl.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="PeterRusin_DynamicField::system_config" title="PeterRusin_DynamicField Configuration" sortOrder="10" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

So far we’ve only added one column, and the table looks like this:

Comment Column

Click here to zoom.

Step 4: Add Option Source for our select

app/code/PeterRusin/DynamicField/Model/Config/Source/Todo.php

<?php
declare(strict_types=1);

namespace PeterRusin\DynamicField\Model\Config\Source;

use GuzzleHttp\ClientFactory;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\ResponseFactory;
use Magento\Framework\Data\OptionSourceInterface;
use Magento\Framework\Serialize\SerializerInterface;

/** @link https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/create-integration-with-api.html */
class Todo implements OptionSourceInterface
{
    const API_REQUEST_URI = 'https://jsonplaceholder.typicode.com';

    /** @var ClientFactory */
    private $clientFactory;
    /** @var ResponseFactory */
    private $responseFactory;
    /** @var SerializerInterface */
    private $serializer;

    public function __construct(
        ClientFactory $clientFactory,
        ResponseFactory $responseFactory,
        SerializerInterface $serializer
    ) {
        $this->clientFactory = $clientFactory;
        $this->responseFactory = $responseFactory;
        $this->serializer = $serializer;
    }

    public function toOptionArray()
    {
        $client = $this->clientFactory->create(
            [
                'config' => [
                    'base_uri' => self::API_REQUEST_URI
                ]
            ]
        );

        try {
            $response = $client->request('GET', 'todos');
        } catch (GuzzleException $exception) {
            $response = $this->responseFactory->create([
                'status' => $exception->getCode(),
                'reason' => $exception->getMessage(),
                'body' => '[]'
            ]);
        }
        $responseBody = $response->getBody();
        $responseContent = $responseBody->getContents();

        return array_map(
            function ($todo) {
                return [
                    'label' => $todo['title'],
                    'value' => $todo['id']
                ];
            },
            $this->serializer->unserialize($responseContent)
        );
    }
}

Step 5: Add our select to the Dynamic Field

app/code/PeterRusin/DynamicField/Block/Adminhtml/System/Config/Form/Field/DynamicField.php

<?php
declare(strict_types=1);

namespace PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field;

use Magento\Backend\Block\Template\Context;
use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;
use Magento\Framework\Data\OptionSourceInterface;
use Magento\Framework\DataObject;
use Magento\Framework\View\Helper\SecureHtmlRenderer;
use PeterRusin\DynamicField\Model\Config\Source\Todo;

class DynamicField extends AbstractFieldArray
{
    /** @var Todo */
    private $todoOptions;

    public function __construct(
        Context $context,
        Todo $todoOptions,
        array $data = [],
        ?SecureHtmlRenderer $secureRenderer = null
    ) {
        parent::__construct($context, $data, $secureRenderer);
        $this->todoOptions = $todoOptions;
    }

    private function markOptionAsSelected(
        DataObject $row,
        string $optionKey,
        OptionSourceInterface $optionSource,
        array &$options
    ) {
        $optionValue = $row->getData($optionKey);
        if ($optionValue) {
            $optionHash = $this->createSelectRenderer($optionSource)->calcOptionHash($optionValue);
            $options['option_' . $optionHash] = 'selected="selected"';
        }
    }

    protected function _prepareArrayRow(DataObject $row)
    {
        $options = [];
        $this->markOptionAsSelected($row, 'todo', $this->todoOptions, $options);
        $row->setData('option_extra_attrs', $options);
    }

    protected function _prepareToRender()
    {
        $this->addColumn('todo', [
            'label' => __('Todo'),
            'class' => 'required-entry',
            'style' => 'width:150px',
            'renderer' => $this->createSelectRenderer($this->todoOptions)
        ]);

        $this->addColumn('comment', [
            'label' => __('Comment'),
            'class' => 'required-entry',
            'style' => 'width:75px'
        ]);

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

    private function createSelectRenderer(OptionSourceInterface $optionSource)
    {
        $layout = $this->getLayout();
        /** @var Select $htmlSelect */
        $htmlSelect = $layout->createBlock(
            Select::class,
            '',
            ['data' => ['is_render_to_js_template' => true]]
        );

        return $htmlSelect->setOptions(
            $optionSource->toOptionArray()
        );
    }
}

app/code/PeterRusin/DynamicField/Block/Adminhtml/System/Config/Fom/Field/Select.php

<?php
declare(strict_types=1);

namespace PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field;

class Select extends \Magento\Framework\View\Element\Html\Select
{
    public function setInputId(string $id)
    {
        $this->setId($id);

        return $this;
    }

    public function setInputName(string $name)
    {
        $this->setName($name);

        return $this;
    }
}

Reading

https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/dynamic-row-system-config.html#step-2-create-the-block-class-to-describe-custom-field-columns

0 comments… add one

Leave a Reply

Your email address will not be published.