How to make the fields of the fieldset multidependable?
-
29-09-2020 - |
Question
I have a fieldset in the admin panel with one parent select (has 5 options) and 2 fields, which should be displayed in case the parent value select will be 3, 4 or 5. I have not found the examples of similar logic in magento and tried to write by analogy with the usual dependence, but it doesn’t work. In my example, the dependent fields are displayed only while choosing the options with the value 5 form the select and are not displayed when choosing 1, 2, 3, or 4.
Full code (block example):
<?php
namespace Siarhey\Test\Block\Adminhtml\Promo\Quote\Edit\Tab;
class Actions extends \Magento\Backend\Block\Widget\Form\Generic implements
\Magento\Backend\Block\Widget\Tab\TabInterface
{
/**
* @param \Magento\Backend\Block\Template\Context $context
* @param \Magento\Framework\Registry $registry
* @param \Magento\Framework\Data\FormFactory $formFactory
* @param array $data
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Framework\Data\FormFactory $formFactory,
array $data = []
) {
parent::__construct($context, $registry, $formFactory, $data);
}
public function getTabLabel()
{
return __('Actions');
}
public function getTabTitle()
{
return __('Actions');
}
public function canShowTab()
{
return true;
}
public function isHidden()
{
return false;
}
protected function _prepareForm()
{
$model = $this->_coreRegistry->registry('current_promo_quote_rule');
/** @var \Magento\Framework\Data\Form $form */
$form = $this->_formFactory->create();
$form->setHtmlIdPrefix('rule_');
$fieldset = $form->addFieldset(
'action_fieldset',
['legend' => __('Rules')]
);
$parentField = $fieldset->addField(
'simple_action',
'select',
[
'label' => __('Apply'),
'name' => 'simple_action',
'options' => [
1 => __('Amount 1'),
2 => __('Discount 1'),
3 => __('Amount 2'),
4 => __('Discount 2'),
]
]
);
$childFieldOne = $fieldset->addField(
'amount',
'text',
[
'name' => 'amount',
'required' => true,
'class' => 'validate-not-negative-number',
'label' => __('Amount')
]
);
$model->setAmount($model->getAmount() * 1);
$childFieldTwo = $fieldset->addField(
'percent',
'text',
['name' => 'percent', 'label' => __('Percent')]
);
$model->setPercent($model->getPercent() * 1);
$this->setChild(
'form_after',
$this->getLayout()->createBlock(
'Magento\Backend\Block\Widget\Form\Element\Dependence'
)->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldOne->getHtmlId(),
$childFieldOne->getName()
)->addFieldMap(
$childFieldTwo->getHtmlId(),
$childFieldTwo->getName()
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'1,3'
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'2,4'
)
);
$form->setValues($model->getData());
if ($model->isReadonly()) {
foreach ($fieldset->getElements() as $element) {
$element->setReadonly(true, true);
}
}
$this->setForm($form);
return parent::_prepareForm();
}
}
Result (view):
Code sample 1 (doesn’t work):
/*
* $parentField is select with values (0,1,2,3,4,5)
*/
$this->setChild(
'form_after',
$this->getLayout()->createBlock(
'Magento\Backend\Block\Widget\Form\Element\Dependence'
)->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldOne->getHtmlId(),
$childFieldOne->getName()
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'3'
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'4'
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'5'
)->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldTwo->getHtmlId(),
$childFieldTwo->getName()
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'3'
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'4'
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'5'
)
);
Code sample 2 (doesn’t work):
/*
* $parentField is select with values (0,1,2,3,4,5)
*/
$this->setChild(
'form_after',
$this->getLayout()->createBlock(
'Magento\Backend\Block\Widget\Form\Element\Dependence'
)->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldOne->getHtmlId(),
$childFieldOne->getName()
)->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldTwo->getHtmlId(),
$childFieldTwo->getName()
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
array('3', '4', '5')
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
array('3', '4', '5')
)
);
Result:
Notice: Array to string conversion in /var/www/magento2/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php on line 95
UPDATE:
Code sample 3 (doesn't work, if selected value is not
'3,4,5'
):
// Parent field
$typeField = $fieldset->addField(
'action_type',
'select',
[
'label' => __('Type'),
'name' => 'action_type',
'options' => ['1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '3,4,5' => '3,4,5']
]
);
$this->setChild(
'form_after',
$this->getLayout()->createBlock(
'Magento\Backend\Block\Widget\Form\Element\Dependence'
)->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldOne->getHtmlId(),
$childFieldOne->getName()
)->addFieldMap(
$typeField->getHtmlId(),
$typeField->getName()
)->addFieldMap(
$childFieldTwo->getHtmlId(),
$childFieldTwo->getName()
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'3,4,5'
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'3,4,5'
)
);
Has someone faced the same issue and found a solution?
Update:
Maybe someone else can the check the presence of this issue? I’ve checked it on 3 different installations and this solution ( the line with the values coma separated ) still doesn’t work.
Solution
If you check the code that is responsible for adding the corresponding fields in accordance with the dependencies in the file lib/web/mage/adminhtml/form.js
, you will see the following scheme there:
var shouldShowUp = true;
for (var idFrom in valuesFrom) {
var from = $(idFrom);
if (from) {
var values = valuesFrom[idFrom]['values'];
var isInArray = values.indexOf(from.value) != -1;
var isNegative = valuesFrom[idFrom]['negative'];
if (!from || isInArray && isNegative || !isInArray && !isNegative) {
shouldShowUp = false;
}
}
}
In case you set comma separated values, for example:
/** @var \Magento\Backend\Block\Widget\Form\Element\Dependence $blockDependence */
$blockDependence->addFieldMap(
$actionType->getHtmlId(),
$actionType->getName()
)->addFieldMap(
$amountField->getHtmlId(),
$amountField->getName()
)->addFieldDependence(
$amountField->getName(),
$actionType->getName(),
implode(',', array(
Rule::ACTION_TYPE_OVERWRITE_COST,
Rule::ACTION_TYPE_ADD_SURCHARGE,
Rule::ACTION_TYPE_ENABLE_SM_AND_OVERWRITE_COST
))
);
then while debuging you will see that indexOf
is trying to find the existing value in the one-element array that is, in your case, a your comma-separated value. This element can't be found:
A step-by-step output of console.log
from the method:
console.log(values);
console.log('Value: '+from.value);
console.log('Is in array: '+isInArray);
To create fields multi dependency, you can use the same coma-separated value, but with some modifications. You will just need the block, that will extend \Magento\Backend\Block\Widget\Form\Element\Dependence
:
<?php
namespace Vendor\Module\Block\Widget\Form\Element;
/**
* Form element dependencies mapper
* Assumes that one element may depend on other element values.
* Will toggle as "enabled" only if all elements it depends from toggle as true.
*/
class Dependence extends \Magento\Backend\Block\Widget\Form\Element\Dependence
{
/**
* @param \Magento\Backend\Block\Context $context
* @param \Magento\Framework\Json\EncoderInterface $jsonEncoder
* @param \Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory
* @param array $data
*/
public function _construct(
\Magento\Backend\Block\Context $context,
\Magento\Framework\Json\EncoderInterface $jsonEncoder,
\Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory,
array $data = []
)
{
parent::_construct($context, $jsonEncoder, $fieldFactory, $data);
}
/**
* {@inheritdoc}
*/
protected function _toHtml()
{
if (!$this->_depends) {
return '';
}
return '<script>
require(["uiRegistry", "mage/adminhtml/form"], function(registry) {
var controller = new FormElementDependenceController(' . $this->_getDependsJson() .
($this->_configOptions ? ', ' .
$this->_jsonEncoder->encode(
$this->_configOptions
) : '') . ');
registry.set("formDependenceController", controller);
});</script>';
}
/**
* Field dependences JSON map generator * @return string
*/
protected function _getDependsJson()
{
$result = [];
foreach ($this->_depends as $to => $row) {
foreach ($row as $from => $field) {
$values = $this->_prepareValues($field->getValues());
/** @var $field \Magento\Config\Model\Config\Structure\Element\Dependency\Field */
$result[$this->_fields[$to]][$this->_fields[$from]] = [
'values' => $values,
'negative' => $field->isNegative(),
];
}
}
return $this->_jsonEncoder->encode($result);
}
/**
* @param $values
* @return array
*/
protected function _prepareValues($values)
{
if (!is_array($values)) {
return $values;
}
$result = array();
foreach ($values as $value) {
if (stripos($value, ',')) {
$result += explode(',', $value);
} else {
$result += $value;
}
}
return $result;
}
}
As you can see, the value is FORCED to be changed into a 1-value array.
The main problem lies in the addFieldDependence
of the class \Magento\Backend\Block\Widget\Form\Element\Dependence
:
The point is that the value (the line from the dependency), is transferred as the only element of the array. indexOf
tries to find the value of the corresponding chosen option, but fails to locate the exact match. As a result, it returns 'false'.
There's also no way to transfer the values as an array, as PHP returns the Notice: Array to string conversion
because of the converting of 'value' => (string)$refField
.
In our example, we have recreated the one-element array into a multi-element one, where each element consists of several dependencies.
The code of your dependence should be modified (you'll need to change the block). This is how to:
// Dependency START
/** @var \Magento\Backend\Block\Widget\Form\Element\Dependence $blockDependence */
$blockDependence = $this->getLayout()->createBlock(
// 'Magento\Backend\Block\Widget\Form\Element\Dependence'
'{Vendor}\{Module}\Block\Widget\Form\Element\Dependence'
);
$blockDependence->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldOne->getHtmlId(),
$childFieldOne->getName()
)->addFieldMap(
$childFieldTwo->getHtmlId(),
$childFieldTwo->getName()
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'1,3'
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'2,4'
);
$this->setChild('form_after', $blockDependence);
// Dependency END
The result should look like this:
UPD
If you are sure that you will be using a comma separated value in the future, the best way will be to adding the const UNIQUE_DELIMITER
with the required value of the delimiter to the class Vendor\Module\Block\Widget\Form\Element\Dependence
: E.g.
const UNIQUE_DELIMITER = '~#!~';
Next, modify the partition method:
/**
* @param $values
* @return array
*/
protected function _prepareValues($values)
{
if (!is_array($values)) {
return $values;
}
$result = array();
foreach ($values as $value) {
if (stripos($value, self::UNIQUE_DELIMITER)) {
$result += explode(self::UNIQUE_DELIMITER, $value);
} else {
$result += $value;
}
}
return $result;
}
Then, use Vendor\Module\Block\Widget\Form\Element\Dependence::UNIQUE_DELIMITER
in your class Actions
.
For your convenience, add the class Dependence
(after namespace):
use Vendor\Module\Block\Widget\Form\Element\Dependence;
And write the code this way:
// Dependency START
/** @var \Magento\Backend\Block\Widget\Form\Element\Dependence $blockDependence */
$blockDependence = $this->getLayout()->createBlock(
// 'Magento\Backend\Block\Widget\Form\Element\Dependence'
'{Vendor}\{Module}\Block\Widget\Form\Element\Dependence'
);
$childFieldOneToParentValues = implode(Dependence::UNIQUE_DELIMITER, array('1','3'));
$childFieldTwoToParentValues = implode(Dependence::UNIQUE_DELIMITER, array('2','4'));
$blockDependence->addFieldMap(
$parentField->getHtmlId(),
$parentField->getName()
)->addFieldMap(
$childFieldOne->getHtmlId(),
$childFieldOne->getName()
)->addFieldMap(
$childFieldTwo->getHtmlId(),
$childFieldTwo->getName()
)->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
$childFieldOneToParentValues
)->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
$childFieldTwoToParentValues
);
$this->setChild('form_after', $blockDependence);
// Dependency END
OTHER TIPS
I may be wrong but unfortunately I don't think you can with the default Magento\Backend\Block\Widget\Form\Element\Dependence
class.
Let me explain:
The addFieldDependence
method looks like this:
public function addFieldDependence($fieldName, $fieldNameFrom, $refField)
{
if (!is_object($refField)) {
/** @var $refField \Magento\Config\Model\Config\Structure\Element\Dependency\Field */
$refField = $this->_fieldFactory->create(
['fieldData' => ['value' => (string)$refField], 'fieldPrefix' => '']
);
}
$this->_depends[$fieldName][$fieldNameFrom] = $refField;
return $this;
}
So let's say you try this code:
addFieldDependence($child,$parent,'2,4')
The value
of the $refField
will be the following string: 2,4
so as there is no such value in your select it will never work.
If you try this code:
addFieldDependence($child,$parent,array('2,4'))
You will get the Array to string conversion
error because of the (string)$refField
code
If you try this code:
addFieldDependence($child,$parent,'2')->addFieldDependence($child,$parent,'4')
The first call will set the $refField
with the value 2 and assign it to the dependencies using the following code:
$this->_depends[$fieldName][$fieldNameFrom] = $refField;
However, the second code will overwrite that dependency because the $fieldName
and $fieldNameFrom
variables are the same than during the first call.
What solutions do you have ?
- Use preferences or plugins to modify the behavior of the
Magento\Backend\Block\Widget\Form\Element\Dependence
class
The important methods to look at here are addFieldDependence
and _getDependsJson
. Problem here is that there's a lot of chances that you also may have to modify the JavaScript FormElementDependenceController
class that handles the dependencies.
- Use several duplicate fields with different names: well this is dirty but I reckon that would work.
Example:
$parentField = $fieldset->addField(
'simple_action',
'select',
[
'label' => __('Apply'),
'name' => 'simple_action',
'options' => [
1 => __('Amount 1'),
2 => __('Discount 1'),
3 => __('Amount 2'),
4 => __('Discount 2'),
]
]
);
$childFieldOne = $fieldset->addField(
'amount',
'text',
[
'name' => 'amount',
'required' => true,
'class' => 'validate-not-negative-number',
'label' => __('Amount')
]
);
$childFieldOneCopy = $fieldset->addField(
'amount',
'text',
[
'name' => 'amount',
'required' => true,
'class' => 'validate-not-negative-number',
'label' => __('Amount')
]
);
Then use:
->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'1'
)
->addFieldDependence(
$childFieldOneCopy->getName(),
$parentField->getName(),
'3'
)
The problem here is that you will have to add several checks in the controller that processes the data to ensure you're processing the right data and not the hidden copy field.
I think that you should look class Magento\Backend\Block\Widget\Form\Element\Dependence
. You can create your own block inherited from this class and rewrite it how you want. In your code replaces block call:
$this->getLayout()->createBlock(
'Magento\Backend\Block\Widget\Form\Element\Dependence'
)
to:
this->getLayout()->createBlock(
'Siarhey\Test\Block\Widget\Form\Element\Dependence'
)
Create block Siarhey\Test\Block\Widget\Form\Element\Dependence
and you can implement in it your logic of verification.
It is only advice. I hope that it helps you.
Create a di.xml under adminhtml and add following code:
Basically need to overwrite Magento\Backend\Block\Widget\Form\Element\Dependence class
<preference for="Magento\Backend\Block\Widget\Form\Element\Dependence"
type="Vendor\Module\Block\Widget\Form\Element\Dependence" />
namespace Vendor\Module\Block\Widget\Form\Element;
class Dependence extends \Magento\Backend\Block\Widget\Form\Element\Dependence
{
/**
* Register field name dependence one from each other by specified values
*
* @param string $fieldName
* @param string $fieldNameFrom
* @param \Magento\Config\Model\Config\Structure\Element\Dependency\Field|string $refField
* @return \Magento\Backend\Block\Widget\Form\Element\Dependence
*/
public function addFieldDependence($fieldName, $fieldNameFrom, $refField)
{
if (!is_object($refField)) {
/** @var $refField \Magento\Config\Model\Config\Structure\Element\Dependency\Field */
$refField = $this->_fieldFactory->create(
['fieldData' => ['value' => (string)$refField, 'separator' => ','], 'fieldPrefix' => '']
);
}
$this->_depends[$fieldName][$fieldNameFrom] = $refField;
return $this;
}
}
Now you can use following way.
->addFieldDependence(
$childFieldTwo->getName(),
$parentField->getName(),
'2,4'
)
Clear Magento2 cache.
I'm not sure about this, I haven't tested it, but looking at the addFieldDependence
method and at \Magento\Config\Model\Config\Structure\Element\Dependency\Field
class I think it might work.
Add this in your class:
protected $fieldFactory;
public function __construct(
....
\Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory,
....
) {
$this->fieldFactory = $fieldFactory;
}
Then, instead of
->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
'1,3'
)
Try this:
$someField = $this->fieldFactory()->create([
'fieldData' => [
'separator' => ',',
'value' => '1,3',
],
'fieldPrefix' => ''
]);
->addFieldDependence(
$childFieldOne->getName(),
$parentField->getName(),
$someField
)
Define $this->_fieldFactory
and try below one, its Working for me:
$blockDependence = $this->getLayout()->createBlock(
'Magento\Backend\Block\Widget\Form\Element\Dependence'
);
$filter = $this->_fieldFactory->create([
'fieldData' => [
'separator' => ',',
'value' => '0,1',
],
'fieldPrefix' => '',
]);
$blockDependence->addFieldMap(
"discount_type",
'discount_type'
)->addFieldMap(
"discount",
'discount'
)->addFieldDependence(
'discount',
'discount_type',
$filter
);
$this->setChild('form_after', $blockDependence);