Magento – Custom select table field in system.xml not showing selected option

magento2

I've created a module with a custom config table. One of the columns is a select box with few options. When I select option and save configuration, selected option is saved in a DB table but not showing in admin.

My setup is like this:

system.xml

<field id="document_name_map" translate="label comment" sortOrder="21" showInDefault="1" showInWebsite="1" showInStore="0">
                <label>Document Name Mapping</label>
                <frontend_model>Custommodule\Name\Block\Adminhtml\System\Config\Form\Field\DocumentNameMapping</frontend_model>
                <backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>
            </field>

DocumentNameMapping.php

protected function _prepareToRender() {
    $this->addColumn('document_name', ['label' => __('Document Name'), 'type' => 'store']);
    $this->addColumn(
        'component_code',
        [
            'label' => __('Primgroup Document Component Code'),
            'renderer' => $this->_getComponentCodeRenderer()
        ]
    );
    $this->_addAfter = false;
    $this->_addButtonLabel = __('Add');
}

/**
 * @return \Magento\Framework\View\Element\BlockInterface
 */
protected function  _getComponentCodeRenderer()
{
    if (!$this->_typeRenderer) {
        $this->_typeRenderer = $this->getLayout()->createBlock(
            '\Primegroup\Oneflow\Block\Adminhtml\System\Config\Form\Field\Type\ComponentCode',
            '',
            ['data' => ['is_render_to_js_template' => true]]

        );
    }
    return $this->_typeRenderer;
}

/**
 *
 */
protected function _prepareArrayRow(\Magento\Framework\DataObject $row)
{
    $componentCode = $row->getData('component_code');
    $options = [];
    if ($componentCode) {
        $key = 'option_' . $this->_getComponentCodeRenderer()->calcOptionHash($componentCode);
        $options[$key] = 'selected="selected"';
    }
    $row->setData('option_extra_attrs', $options);
}

and ComponentCode.php is creating options from product attribute.

So all this creates this table:
enter image description here

So, when I choose option and save I can see that right selected option is saved in a DB but here always showing just first.

In DB:a:1:{s:18:"_1498484025538_538";a:2:{s:13:"document_name";s:5:"jhgjg";s:14:"component_code";s:3:"INN";}}

I noticed that selected field looks like:

<select name="groups[rendering_settings][fields][document_name_map][value][_1498462459122_122][component_code]" id="" class="" title=""><option value="TAG">TAG</option><option value="WRAP">WRAP</option><option value="WRP">WRP</option><option value="INN">INN</option><option value="CVR">CVR</option><option value="ITM">ITM</option></select>

I really think that some JS needs to be changed, because in that select box code I cannot see option_

EDIT [SOLVED]

So if your config table contains input fields only, you can use this as a
<backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>

But If your config table contains fields like dropdowns, multiselect…you will need to make a custom backend_module that will do serialisation of a data. In my case I've created this:
<backend_model>Custommodule\Name\Model\Adminhtml\System\Config\DocumentNameMapping</backend_model>
that looks like this:

<?php

namespace Custommodule\Name\Model\Adminhtml\System\Config;

use Magento\Framework\App\Cache\TypeListInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Value;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Math\Random;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;

class DocumentNameMapping extends Value
{
    /**
     * @var Random
     */
    protected $mathRandom;

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

    /**
     * Prepare data before save
     *
     * @return $this
     */
    public function beforeSave()
    {
        $value = $this->getValue();
        $result = [];
        foreach ($value as $data) {
            if (empty($data['document_name']) || empty($data['component_code'])) {
                continue;
            }
            $documentName = $data['document_name'];
            if (array_key_exists($documentName, $result)) {
                $result[$documentName] = $this->appendUniqueCompCodes($result[$documentName], $data['component_code']);
            } else {
                $result[$documentName] = $data['component_code'];
            }
        }
        $this->setValue(serialize($result));
        return $this;
    }

    /**
     * Process data after load
     *
     * @return $this
     */
    public function afterLoad()
    {
        $value = unserialize($this->getValue());
        if (is_array($value)) {
            $value = $this->encodeArrayFieldValue($value);
            $this->setValue($value);
        }
        return $this;
    }

    /**
     * Encode value to be used in \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray
     *
     * @param array $value
     * @return array
     */
    protected function encodeArrayFieldValue(array $value)
    {
        $result = [];
        foreach ($value as $documentName => $componentCode) {
            $id = $this->mathRandom->getUniqueHash('_');
            $result[$id] = ['document_name' => $documentName, 'component_code' => $componentCode];
        }
        return $result;
    }

    /**
     * Append unique countries to list of exists and reindex keys
     *
     * @param array $docNamesList
     * @param array $compCodesList
     * @return array
     */
    private function appendUniqueCompCodes(array $docNamesList, array $compCodesList)
    {
        $result = array_merge($docNamesList, $compCodesList);
        return array_values(array_unique($result));
    }

}

Example suggested by Aaron was perfect for solving this problem.
Thank you

Best Answer

In situations like this, it's a good idea to try and find an example in the magento core code of what you're trying to do. In this case, the countrycreditcard field in the Magento\Braintree module will do. This is the field declaration:

<field id="countrycreditcard" translate="label" sortOrder="220" showInDefault="1" showInWebsite="1" showInStore="0">
    <label>Country Specific Credit Card Types</label>
    <frontend_model>Magento\Braintree\Block\Adminhtml\Form\Field\CountryCreditCard</frontend_model>
    <backend_model>Magento\Braintree\Model\Adminhtml\System\Config\CountryCreditCard</backend_model>
    <config_path>payment/braintree/countrycreditcard</config_path>
</field>

The main point where your code deviates from the example is the backend_model being used. You should look at Magento\Braintree\Model\Adminhtml\System\Config\CountryCreditCard and adapt it to your specific field to create a custom backend model to use (instead of Magento\Config\Model\Config\Backend\Serialized\ArraySerialized). It should be mostly a matter of replacing 'country_id' and 'cc_types' with your own column ids.

Related Topic