-
-
Notifications
You must be signed in to change notification settings - Fork 2
'Edit attributes' page
Notes taken against Magento 2.2.8 (Paving Direct).
- The loading process is tightly bound to the Magento_Swatches module - is this due to the attribute (material) we're loading? Is Magento_Swatches used to render other attributes?
The block class (\Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options
) calls \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options::getJsonConfig
, which returns an array of metadata relating to the attribute options. This includes the attribute options themselves (including all the data needed for the JS to build the grid later) and also two flags; isSortable
and isReadOnly
.
getJsonConfig
calls getOptionValues
, which gets the attribute option value collection and passes it to \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options::_prepareOptionValues
.
This method returns an array of data objects, each one representing an option value. It sets the default value and input type for each value. For attributes with select
frontend types, the input type is set to 'radio'. If it is multiselect
, it is set to 'checkbox'. For all other frontend types the input type is set to an empty string.
The items in the option values collection is processed in batches of 200.
System attributes and user-defined attributes are subject to different processing. \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options::_prepareUserDefinedAttributeOptionValues
also sets whether checkboxes are checked or not on page load.
The form is built from a combination of 'classic' PHTML templates and JS components.
The option values grid uses a different template for different types of attribute. There are three in the core:
html/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
html/vendor/magento/module-swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml
html/vendor/magento/module-swatches/view/adminhtml/templates/catalog/product/attribute/visual.phtml
The form for checkbox attributes is defined in html/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
.
The thead
and tfooter
are generated using standard PHTML, but the tbody
is populated using JS.
A template of each row is defined in the script id="row-template" type="text/x-magento-template"
block in the PHTML template.
The values of each row are provided by the method \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options::getOptionValues
. These values are passed through htmlspecialchars_decode
before being output to the template as a json_encoded
string.
The PHTML template defines a script type="text/x-magento-init"
block, which initialises two UI components:
-
Magento_Catalog/js/options
: Constructs and populates the option values grid; -
Magento_Catalog/catalog/product/attribute/unique-validate
: Presumably validates that multiselect attributes have a value selected
<script type="text/x-magento-init">
{
"*": {
"Magento_Catalog/js/options": {
"attributesData": <?= /* @noEscape */ json_encode($values, JSON_HEX_QUOT) ?>,
"isSortable": <?= (int)(!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) ?>,
"isReadOnly": <?= (int)$block->getReadOnly() ?>
},
"Magento_Catalog/catalog/product/attribute/unique-validate": {
"element": "required-dropdown-attribute-unique",
"message": "<?= $block->escapeHtml(__("The value of Admin must be unique.")) ?>"
}
}
}
</script>
Values from the PHTML template/block class are passed into these AMD modules by assigning them to properties of a config
object, which is passed into the constructor of the AMD component:
define([ /* blah, blah */], function (jQuery, mageTemplate, rg) {
'use strict';
return function (config) {
var optionPanel = jQuery('#manage-options-panel'),
attributeOption = {
// Note how 'config.isReadOnly' is defined by the "isReadOnly" property set in the PHTML template
isReadOnly: config.isReadOnly,
The whole option values table is wrapped in a div
with an id of manage-options-panel
. A callback is registered on the onRender
event which encapsulates all the logic to load and display the grid:
optionPanel.on('render', function () {
attributeOption.ignoreValidate();
if (attributeOption.rendered) {
return false;
}
jQuery('body').trigger('processStart');
attributeOption.renderWithDelay(config.attributesData, 0, 100, 300);
attributeOption.bindRemoveButtons();
});
The grid is validated using the jQuery validator plugin. The ignoreValidate
method call tells the plugin to ignore validating input, select and textarea form fields inside the grid.
The AMD component defines an attributeOption
object, which encapsulates all the logic necessary to render each option (and therefore row) of the grid.
attributeOption.rendered
is a flag which determines which stops the field from being re-rendered if it has already been rendered. It is initialised to 0
on creation of the component and is set to 1
when the component detects the field has already been rendered in the renderWithDelay
method.
processStart
is an event registered on the body
element. It triggers all the events registered on that element.
The rendering process is kicked off in the attributeOption.renderWithDelay
method. It does three things:
- Creates the DOM elements for the rows;
- Inserts them into the table
- Recursively calls itself to render all the rows of the grid using steps one and two.
It also makes use of
setTimeout
to render the rows in batches of 100 at a time, spaced 300 milliseconds apart. The delay is the fourth parameter ofattributeOption.renderWithDelay
, the batch size is defined by the second and third parameters (the first parameter is theconfig.attributesData
json object passed in from the PHTML template).
attributeOption.bindRemoveButtons
appears to be a hidden dependency on the Magento_Swatches
module. It registers events to the delete buttons added on each row of the table grid - but only for elements with an id of swatch-visual-options-panel
(e.g. Like the one defined in html/vendor/magento/module-swatches/view/adminhtml/templates/catalog/product/attribute/visual.phtml
).
In the Magento_Catalog/js/options
component checks for an element with the id of add_new_option_button
. If it exists, an onClick
event is registered, which binds the attributeOption.add
method to it. The arguments passed to that method when clicked are the attributeOption
object (which represents the row being added), an empty object of data and the flag true
.
The last parameter is important because the attributeOption.add
method is used both to create the table grid on page load and also to add new rows after the page has finished loading. If the flag is true, the attributeOption.render
method is executed at the end of the add
method so that the new row is added to the table grid immediately.
An interesting feature of the table grid is that the DOm elements for the rows are created according to a template, which is defined in the PHTML template (html/vendor/magento/module-catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml):
<script id="row-template" type="text/x-magento-template">
<tr <% if (data.rowClasses) { %>class="<%- data.rowClasses %>"<% } %>>
<td class="col-draggable">
<?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?>
<div data-role="draggable-handle" class="draggable-handle"
title="<?= $block->escapeHtmlAttr(__('Sort Option')) ?>">
</div>
<?php endif; ?>
<input data-role="order" type="hidden" name="option[order][<%- data.id %>]" value="<%- data.sort_order %>" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()) :?> disabled="disabled"<?php endif; ?>/>
</td>
<td class="col-default control-table-actions-cell">
<input class="input-radio" type="<%- data.intype %>" name="default[]" value="<%- data.id %>" <%- data.checked %><?php if ($block->getReadOnly()) :?>disabled="disabled"<?php endif;?>/>
</td>
<?php foreach ($stores as $_store) :?>
<td class="col-<%- data.id %>"><input name="option[value][<%- data.id %>][<?= (int) $_store->getId() ?>]" value="<%- data.store<?= /* @noEscape */ (int) $_store->getId() ?> %>" class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> required-option required-unique<?php endif; ?>" type="text" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()) :?> disabled="disabled"<?php endif;?>/></td>
<?php endforeach; ?>
<td id="delete_button_container_<%- data.id %>" class="col-delete">
<input type="hidden" class="delete-flag" name="option[delete][<%- data.id %>]" value="" />
<?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?>
<button id="delete_button_<%- data.id %>" title="<?= $block->escapeHtmlAttr(__('Delete')) ?>" type="button"
class="action- scalable delete delete-option"
>
<span><?= $block->escapeHtml(__('Delete')) ?></span>
</button>
<?php endif;?>
</td>
</tr>
</script>
This template uses its own template variables (the data
referenced inside <%- ... -%>
blocks), as well as the usual PHP variables (enclosed within <?php ... ?>
).
In effect, what Magento have done here is create a template which is rendered twice - once by PHP (on the server side) and once by JavaScript (on the client side).
PHP is used to generate the columns of the table row and then attributeOption.add
passes the data from config.attributesData
to mage/template
(html/lib/web/mage/template
). mage/template
is basically a wrapper for Underscore.js.
mage/template
renders the template, populating the dynamic aspects of the table grid (like form field names, values, IDs and the delete buttons for each row). The returned string is added to the elements
object, ready to be added to the DOM by the attributeOption.render
method.
On save, all the form fields in the table are serialised and posted under the value 'serialized_options', e.g:
serialized_options: ["option%5Border%5D%5B406%5D=1&option%5Bvalue%5D%5B406%5D%5B0%5D=Concrete&option%5Bvalue%5D%5B406%5D%5B1%5D=&crawlable%5B%5D=406&option%5Bdelete%5D%5B406%5D=","option%5Border%5D%5B458%5D=2&option%5Bvalue%5D%5B458%5D%5B0%5D=Granite&option%5Bvalue%5D%5B458%5D%5B1%5D=&crawlable%5B%5D=458&option%5Bdelete%5D%5B458%5D=","option%5Border%5D%5B870%5D=3&option%5Bvalue%5D%5B870%5D%5B0%5D=Grout&option%5Bvalue%5D%5B870%5D%5B1%5D=&option%5Bdelete%5D%5B870%5D=","option%5Border%5D%5B650%5D=4&option%5Bvalue%5D%5B650%5D%5B0%5D=Limestone+%2F+Sandstone&option%5Bvalue%5D%5B650%5D%5B1%5D=&crawlable%5B%5D=650&option%5Bdelete%5D%5B650%5D=","option%5Border%5D%5B387%5D=5&option%5Bvalue%5D%5B387%5D%5B0%5D=Limestone&option%5Bvalue%5D%5B387%5D%5B1%5D=&option%5Bdelete%5D%5B387%5D=","option%5Border%5D%5B766%5D=6&option%5Bvalue%5D%5B766%5D%5B0%5D=Liquid&option%5Bvalue%5D%5B766%5D%5B1%5D=&option%5Bdelete%5D%5B766%5D=","option%5Border%5D%5B774%5D=7&option%5Bvalue%5D%5B774%5D%5B0%5D=Marble&option%5Bvalue%5D%5B774%5D%5B1%5D=&option%5Bdelete%5D%5B774%5D=","option%5Border%5D%5B719%5D=8&option%5Bvalue%5D%5B719%5D%5B0%5D=Metal&option%5Bvalue%5D%5B719%5D%5B1%5D=&option%5Bdelete%5D%5B719%5D=","option%5Border%5D%5B718%5D=9&option%5Bvalue%5D%5B718%5D%5B0%5D=Pointfix&option%5Bvalue%5D%5B718%5D%5B1%5D=&option%5Bdelete%5D%5B718%5D=","option%5Border%5D%5B872%5D=10&option%5Bvalue%5D%5B872%5D%5B0%5D=Polymer+Bond&option%5Bvalue%5D%5B872%5D%5B1%5D=&option%5Bdelete%5D%5B872%5D=","option%5Border%5D%5B459%5D=11&option%5Bvalue%5D%5B459%5D%5B0%5D=Porcelain&option%5Bvalue%5D%5B459%5D%5B1%5D=&option%5Bdelete%5D%5B459%5D=","option%5Border%5D%5B461%5D=12&option%5Bvalue%5D%5B461%5D%5B0%5D=Reconstituted&option%5Bvalue%5D%5B461%5D%5B1%5D=&option%5Bdelete%5D%5B461%5D=","option%5Border%5D%5B160%5D=13&option%5Bvalue%5D%5B160%5D%5B0%5D=Sandstone&option%5Bvalue%5D%5B160%5D%5B1%5D=&option%5Bdelete%5D%5B160%5D=","option%5Border%5D%5B803%5D=14&option%5Bvalue%5D%5B803%5D%5B0%5D=Semiconductor&option%5Bvalue%5D%5B803%5D%5B1%5D=&option%5Bdelete%5D%5B803%5D=","option%5Border%5D%5B398%5D=15&option%5Bvalue%5D%5B398%5D%5B0%5D=Slate&option%5Bvalue%5D%5B398%5D%5B1%5D=&option%5Bdelete%5D%5B398%5D=","option%5Border%5D%5B765%5D=16&option%5Bvalue%5D%5B765%5D%5B0%5D=Stainless+Steel&option%5Bvalue%5D%5B765%5D%5B1%5D=&option%5Bdelete%5D%5B765%5D=","option%5Border%5D%5B720%5D=17&option%5Bvalue%5D%5B720%5D%5B0%5D=Wood&option%5Bvalue%5D%5B720%5D%5B1%5D=&option%5Bdelete%5D%5B720%5D=","option%5Border%5D%5B804%5D=18&option%5Bvalue%5D%5B804%5D%5B0%5D=N%2FA&option%5Bvalue%5D%5B804%5D%5B1%5D=&option%5Bdelete%5D%5B804%5D=","option%5Border%5D%5B1030%5D=19&option%5Bvalue%5D%5B1030%5D%5B0%5D=Cast+Stone&option%5Bvalue%5D%5B1030%5D%5B1%5D=&option%5Bdelete%5D%5B1030%5D=1"]
All options are serialised, regardless of whether they were changed or not.
In the save controller, \Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save::execute
, the post data is unserialized, revealing an array structure that looks something like this:
Array
(
[option] => Array
(
[order] => Array
(
[31] => 0
[143] => 0
[32] => 1
[144] => 1
[33] => 2
[145] => 2
[34] => 3
[146] => 3
[147] => 4
[35] => 4
[148] => 5
[36] => 5
[149] => 6
[37] => 6
[150] => 7
[38] => 7
[39] => 8
[151] => 8
[152] => 9
[40] => 9
[153] => 10
[41] => 10
[42] => 11
[154] => 11
[43] => 12
[155] => 12
[156] => 13
[44] => 13
[157] => 14
[45] => 14
[158] => 15
[46] => 15
[47] => 16
[159] => 16
[48] => 17
[160] => 17
)
[value] => Array
(
[31] => Array
(
[0] => Burlap
[1] =>
[4] =>
)
[143] => Array
(
[0] => Cocona® performance fabric
[1] =>
[4] =>
)
[32] => Array
(
[0] => Canvas
[1] =>
[4] =>
)
[144] => Array
(
[0] => Wool
[1] =>
[4] =>
)
[33] => Array
(
[0] => Cotton
[1] =>
[4] =>
)
[145] => Array
(
[0] => Fleece
[1] =>
[4] =>
)
[34] => Array
(
[0] => Faux Leather
[1] =>
[4] =>
)
[146] => Array
(
[0] => Hemp
[1] =>
[4] =>
)
[147] => Array
(
[0] => Jersey
[1] =>
[4] =>
)
[35] => Array
(
[0] => Leather
[1] =>
[4] =>
)
[148] => Array
(
[0] => LumaTech™
[1] =>
[4] =>
)
[36] => Array
(
[0] => Mesh
[1] =>
[4] =>
)
[149] => Array
(
[0] => Lycra®
[1] =>
[4] =>
)
[37] => Array
(
[0] => Nylon
[1] =>
[4] =>
)
[150] => Array
(
[0] => Microfiber
[1] =>
[4] =>
)
[38] => Array
(
[0] => Polyester
[1] =>
[4] =>
)
[39] => Array
(
[0] => Rayon
[1] =>
[4] =>
)
[151] => Array
(
[0] => Spandex
[1] =>
[4] =>
)
[152] => Array
(
[0] => HeatTec®
[1] =>
[4] =>
)
[40] => Array
(
[0] => Ripstop
[1] =>
[4] =>
)
[153] => Array
(
[0] => EverCool™
[1] =>
[4] =>
)
[41] => Array
(
[0] => Suede
[1] =>
[4] =>
)
[42] => Array
(
[0] => Foam
[1] =>
[4] =>
)
[154] => Array
(
[0] => Organic Cotton
[1] =>
[4] =>
)
[43] => Array
(
[0] => Metal
[1] =>
[4] =>
)
[155] => Array
(
[0] => TENCEL
[1] =>
[4] =>
)
[156] => Array
(
[0] => CoolTech™
[1] =>
[4] =>
)
[44] => Array
(
[0] => Plastic
[1] =>
[4] =>
)
[157] => Array
(
[0] => Khaki
[1] =>
[4] =>
)
[45] => Array
(
[0] => Rubber
[1] =>
[4] =>
)
[158] => Array
(
[0] => Linen
[1] =>
[4] =>
)
[46] => Array
(
[0] => Synthetic
[1] =>
[4] =>
)
[47] => Array
(
[0] => Stainless Steel
[1] =>
[4] =>
)
[159] => Array
(
[0] => Wool
[1] =>
[4] =>
)
[48] => Array
(
[0] => Silicone
[1] =>
[4] =>
)
[160] => Array
(
[0] => Terry
[1] =>
[4] =>
)
)
[delete] => Array
(
[31] =>
[143] =>
[32] =>
[144] =>
[33] =>
[145] =>
[34] =>
[146] =>
[147] =>
[35] =>
[148] =>
[36] =>
[149] =>
[37] =>
[150] =>
[38] =>
[39] =>
[151] =>
[152] =>
[40] =>
[153] =>
[41] =>
[42] =>
[154] =>
[43] =>
[155] =>
[156] =>
[44] =>
[157] =>
[45] =>
[158] =>
[46] =>
[47] =>
[159] => 1
[48] =>
[160] =>
)
)
[default] => Array
(
[0] => 31
)
[crawlable] => Array
(
[0] => 31
)
)
option[order]
contains the sort order of the option values, option[values]
contains the frontend labels for each store and options[delete]
contains flags for each row which was deleted (see below for more on deletion). default
refers to the Is Default
checkbox/radio button form control, and crawlable
is a custom form field we've added.
The values from the rest of the form (e.g. 'Default Label') are POSTed as well. The two arrays of values are merged together.
There is some logic to create a new attribute set.
Three fields are checked to see if they need to be 'invalidated'. This field is then checked after a successful save in \Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute::afterSave
. If it was set to true, the \Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID
(catalogsearch_fulltext
) is set to invalidated and the config cache is cleared.
Pressing the delete button of a row executes some JS which hides the row. A hidden input field is also set which is submitted along with the other fields when the form is saved.