Magento 2 multiple products chooser.

В одном из предидущих постов мы рассматривали использование виджетов. Предлагаю рассмотреть специфическую задачу и возможные способы ее реализации.

Как вы наверно догадались с названия поста, речь пойдет о множественном выборе продуктов и последующей обработке выбранных данных.

И так, предположим, у нас есть задача, в которой нас просят написать виджет. Желаемый функционал должен позволять пользователю выбирать продукты в админке, которые потом будут выводится на домашней странице. Вывод будет тремя колонками по два продукта в каждой. Выбор в админке должен быть не только 2х продуктов в каждой колонке, а и с возможностью выбора большего колличества с указанием очередности вывода. Соответственно, если один из продуктов будет отсутсвовать на стоке, должен выводится следующий из списка.

Первым, что приходит в голову, использование дефолтного product chooser шесть раз и вставки после каждых 2х филда, в который пользователь будет вносить sku запасных продуктов для каждой колонки.

<parameter name="id_path" xsi:type="block" visible="true" required="true" sort_order="10">
                <label translate="true">Product</label>
                <block class="Magento\Catalog\Block\Adminhtml\Product\Widget\Chooser">
                    <data>
                        <item name="button" xsi:type="array">
                            <item name="open" xsi:type="string" translate="true">Select Product...</item>
                        </item>
                    </data>
                </block>
</parameter>

соответственно, в блоке мы будем получать 6ть выбранных продуктов и список для замены. Собираем коллекцию продуктов и отдаем ее в темплейт, где выбираем нужные данные продуктов.

Все отлично, кроме того, что вводить sku запасных продуктов довольно не удобно для пользователя. И задача чуть усложняется – выбор продуктов не для одного, а для многих. Дополнительно необходимо добавить в грид поле “позиция”, где мы будем задавать очередность вывода продуктов в колонке.

Соответвенно выше упомянутый чусер нам не совсем подходит, так как он для одного продукта и нам будет необходимо переписать UI компонент, который будет брать все продукты с их позициями. Думаю, многие из вас сталкивались с дефолтными UI компанентами и понимают, что это не самая простая задача.

И так нам нужен селектор для множества продуктов. На ум приходит функционал условий, который используется для правил цен. (vendor/magento/module-catalog-widget). Чуть глубже ознакомившись с данным вариантом, становится понятно, что тут так же много придется кастомизировать. С учетов того, что он состоит из 2х частей, 1я выбор фильтра и 2я сам грид по выбранному фильтру. Соответственно нам нужно будет кастомизировать выбор опшинов, что бы был только sku и затем так же переписывать UI компанент, который в оригинале сохраняет выбранные sku в отдельное поле. А нам нужны еще позиции. Проанализировав объем работы, становится понятно, что данный тип решения ближе к пожеланиям клиента, но довольно трудоемкий.

Пробежавшись по дефолтным виджетам, находим “Dynamic Blocks Rotator”, есть грид и позиции. Остается поменять коллекцию на продукты и чуть заморичиться с тремя чусерами одновременно.

Похоже на то, что это оптимальный вариант для нашей задачи.

И так, создаем app/code/[Company]/[module]/etc/widget.xml

<?xml version="1.0" encoding="UTF-8"?>

<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget id="[some widget id]" class="[Company]\[module]\Block\Widget\[block name]"
            placeholder_image="Magento_Banner::widget_banner.png">
        <label translate="true">Label for our widget here</label>
        <description translate="true">Description for our widget here</description>
        <parameters>
            <parameter xsi:type="block" name="[id for 1st column products]" visible="true">
                <label translate="true">[Label for 1st column  products]</label>
                <block class="FirstColumnChooser" />
            </parameter>
            <parameter xsi:type="block" name="[id for 2nd column  products]" visible="true">
                <label translate="true">[Label for 2nd column  products]</label>
                <block class="SecondColumnChooser" />
            </parameter>
            <parameter xsi:type="block" name="[id for 3rd column  products]" visible="true">
                <label translate="true">[Label for 2nd column  products]</label>
                <block class="ThirdColumnChooser" />
            </parameter>
        </parameters>
    </widget>
</widgets>

И так у нас есть три чусера и блок, в котором мы обрабатываем выбранные данные. Для того, что бы чесеры не перекрывали друг друга, добавим app/code/[Company]/[module]/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <virtualType name="FirstColumnChooser" type="[Company]\[module]\Block\Adminhtml\Widget\ColumnChooser">
        <arguments>
            <argument name="gridId" xsi:type="string">bannerGrid</argument>
        </arguments>
    </virtualType>
    <virtualType name="SecondColumnChooser" type="[Company]\[module]\Block\Adminhtml\Widget\ColumnChooser">
        <arguments>
            <argument name="gridId" xsi:type="string">bannerGrid2</argument>
        </arguments>
    </virtualType>
    <virtualType name="ThirdColumnChooser" type="[Company]\[module]\Block\Adminhtml\Widget\ColumnChooser">
        <arguments>
            <argument name="gridId" xsi:type="string">bannerGrid3</argument>
        </arguments>
    </virtualType>
</config>

где добавим виртальные типы для на наших чусеров и передадим им разные айди. Это позволит нам не плодить одинаковый код лишь для того, что бы изменить айди грида.

Продолжаем, вдохновившись коровским, vendor/magento/module-banner/Block/Adminhtml/Banner/Grid.php созданим свой app/code/[Company]/[module]/Block/Adminhtml/Product/Grid.php

<?php

namespace [Company]\[module]\Block\Adminhtml\Product;

use Magento\Backend\Block\Template\Context;
use Magento\Backend\Block\Widget\Grid\Column;
use Magento\Backend\Block\Widget\Grid\Extended;
use Magento\Backend\Helper\Data;
use Magento\Banner\Model\Config;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\DataObject;


class Grid extends Extended
{
    protected $_bannerColFactory = null;

    protected $productConfig = null;

    public function __construct(
        Context $context,
        Data $backendHelper,
        CollectionFactory $bannerColFactory,
        Config $bannerConfig,
        array $data = []
    ) {
        parent::__construct($context, $backendHelper, $data);
        $this->_bannerColFactory = $bannerColFactory;
        $this->productConfig = $bannerConfig;
    }

    protected function _construct()
    {
        parent::_construct();
        $this->setId('bannerGrid');
        $this->setDefaultSort('banner_id');
        $this->setDefaultDir('desc');
        $this->setSaveParametersInSession(true);
        $this->setUseAjax(true);
        $this->setVarNameFilter('banner_filter');
    }

    protected function _prepareCollection()
    {
        $collection = $this->_bannerColFactory->create()->addAttributeToSelect('name');
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn(
            'banner_id',
            [
                'header' => __('ID'),
                'type' => 'number',
                'index' => 'entity_id',
                'header_css_class' => 'col-id',
                'column_css_class' => 'col-id',
                'sortable' => true,
            ]
        );

        $this->addColumn(
            'banner_name',
            ['header' => __('Product name'), 'type' => 'text', 'index' => 'name', 'escape' => true]
        );

        $this->addColumn(
            'banner_sku',
            ['header' => __('Product sku'), 'type' => 'text', 'index' => 'sku', 'escape' => true]
        );

        return parent::_prepareColumns();
    }

    protected function _prepareMassaction()
    {
        $this->setMassactionIdField('banner_id');
        $this->getMassactionBlock()->setFormFieldName('banner');

        $this->getMassactionBlock()->addItem(
            'delete',
            [
                'label' => __('Delete'),
                'url' => $this->getUrl('adminhtml/*/massDelete'),
                'confirm' => __('Are you sure you want to delete these dynamic blocks?')
            ]
        );

        return $this;
    }

    public function getRowUrl($row)
    {
        return $this->getUrl('adminhtml/*/edit', ['id' => $row->getEntityId()]);
    }

    public function getGridUrl()
    {
        return $this->getUrl('adminhtml/*/grid', ['_current' => true]);
    }

    protected function _addColumnFilterToCollection($column)
    {
        parent::_addColumnFilterToCollection($column);
        return $this;
    }
}

Соответственно подменив коллекцию банеров на продукты и переименовав/добавив поля грида на наши. Далее по аналогии создадим app/code/[company]/[module]/Block/Adminhtml/Widget/ColumnChooser.php

<?php

namespace [company]\[module]\Block\Adminhtml\Widget;

use Exception;
use Magento\Backend\Block\Widget\Grid\Column;
use [company]\[module]\Block\Adminhtml\Product\Grid;

class ColumnChooser extends \[company]\[module]\Block\Adminhtml\Product\Grid
{
    protected $_elementFactory;

    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Backend\Helper\Data $backendHelper,
        \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $bannerColFactory,
        \Magento\Banner\Model\Config $bannerConfig,
        \Magento\Framework\Data\Form\Element\Factory $elementFactory,
        $gridId = '',
        array $data = []
    ) {
        parent::__construct($context, $backendHelper, $bannerColFactory, $bannerConfig, $data);
        $this->_elementFactory = $elementFactory;
        $this->setId($gridId);
    }

    protected $_selectedBanners = [];

    protected $_elementValueId = '';

    public function _construct()
    {
        parent::_construct();
        $this->setDefaultFilter(['in_banners' => 1]);
    }

    public function prepareElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element)
    {
        $this->_elementValueId = "{$element->getId()}";
        $this->_selectedBanners = explode(',', $element->getValue());

        //Create hidden field that store selected banner ids
        $hidden = $this->_elementFactory->create('hidden', ['data' => $element->getData()]);
        $hidden->setId($this->_elementValueId)->setForm($element->getForm());
        $hiddenHtml = $hidden->getElementHtml();

        $element->setValue('')->setValueClass('value2');
        $element->setData('css_class', 'grid-chooser');
        $element->setData('after_element_html', $hiddenHtml . $this->toHtml());
        $element->setData('no_wrap_as_addon', true);

        return $element;
    }

    public function getRowInitCallback()
    {
        return '
        function(grid, row){
            if(!grid.selBannersIds){
                grid.selBannersIds = {};
                if($(\'' .
            $this->_elementValueId .
            '\').value != \'\'){
                    var elementValues = $(\'' .
            $this->_elementValueId .
            '\').value.split(\',\');
                    for(var i = 0; i < elementValues.length; i++){
                        grid.selBannersIds[elementValues[i]] = i+1;
                    }
                }
                grid.reloadParams = {};
                grid.reloadParams[\'selected_banners[]\'] = Object.keys(grid.selBannersIds);
            }
            var inputs      = Element.select($(row), \'input\');
            var checkbox    = inputs[0];
            var position    = inputs[1];
            var bannersNum  = grid.selBannersIds.length;
            var bannerId    = checkbox.value;

            inputs[1].checkboxElement = checkbox;

            var indexOf = Object.keys(grid.selBannersIds).indexOf(bannerId);
            if(indexOf >= 0){
                checkbox.checked = true;
                if (!position.value) {
                    position.value = indexOf + 1;
                }
            }

            Event.observe(position,\'change\', function(){
                var checkb = Element.select($(row), \'input\')[0];
                if(checkb.checked){
                    grid.selBannersIds[checkb.value] = this.value;
                    var idsclone = Object.clone(grid.selBannersIds);
                    var bans = Object.keys(grid.selBannersIds);
                    var pos = Object.values(grid.selBannersIds).sort(sortNumeric);
                    var banners = [];
                    var k = 0;

                    for(var j = 0; j < pos.length; j++){
                        for(var i = 0; i < bans.length; i++){
                            if(idsclone[bans[i]] == pos[j]){
                                banners[k] = bans[i];
                                k++;
                                delete(idsclone[bans[i]]);
                                break;
                            }
                        }
                    }
                    $(\'' .
            $this->_elementValueId .
            '\').value = banners.join(\',\');
                }
            });
        }
        ';
    }

    public function getRowClickCallback()
    {
        return '
            function (grid, event) {
                if(!grid.selBannersIds){
                    grid.selBannersIds = {};
                }

                var trElement   = Event.findElement(event, "tr");
                var isInput     = Event.element(event).tagName == \'INPUT\';
                var inputs      = Element.select(trElement, \'input\');
                var checkbox    = inputs[0];
                var position    = inputs[1].value || 1;
                var checked     = isInput ? checkbox.checked : !checkbox.checked;
                checkbox.checked = checked;
                var bannerId    = checkbox.value;

                if(checked){
                    if(Object.keys(grid.selBannersIds).indexOf(bannerId) < 0){
                        grid.selBannersIds[bannerId] = position;
                    }
                }
                else{
                    delete(grid.selBannersIds[bannerId]);
                }

                var idsclone = Object.clone(grid.selBannersIds);
                var bans = Object.keys(grid.selBannersIds);
                var pos = Object.values(grid.selBannersIds).sort(sortNumeric);
                var banners = [];
                var k = 0;
                for(var j = 0; j < pos.length; j++){
                    for(var i = 0; i < bans.length; i++){
                        if(idsclone[bans[i]] == pos[j]){
                            banners[k] = bans[i];
                            k++;
                            delete(idsclone[bans[i]]);
                            break;
                        }
                    }
                }
                $(\'' .
            $this->_elementValueId .
            '\').value = banners.join(\',\');
                grid.reloadParams = {};
                grid.reloadParams[\'selected_banners[]\'] = banners;
            }
        ';
    }

    public function getCheckboxCheckCallback()
    {
        return 'function (grid, element, checked) {
                    if(!grid.selBannersIds){
                        grid.selBannersIds = {};
                    }
                    var checkbox    = element;

                    checkbox.checked = checked;
                    var bannerId    = checkbox.value;
                    if(bannerId == \'on\'){
                        return;
                    }
                    var trElement   = element.up(\'tr\');
                    var inputs      = Element.select(trElement, \'input\');
                    var position    = inputs[1].value || 1;

                    if(checked){
                        if(Object.keys(grid.selBannersIds).indexOf(bannerId) < 0){
                            grid.selBannersIds[bannerId] = position;
                        }
                    }
                    else{
                        delete(grid.selBannersIds[bannerId]);
                    }

                    var idsclone = Object.clone(grid.selBannersIds);
                    var bans = Object.keys(grid.selBannersIds);
                    var pos = Object.values(grid.selBannersIds).sort(sortNumeric);
                    var banners = [];
                    var k = 0;
                    for(var j = 0; j < pos.length; j++){
                        for(var i = 0; i < bans.length; i++){
                            if(idsclone[bans[i]] == pos[j]){
                                banners[k] = bans[i];
                                k++;
                                delete(idsclone[bans[i]]);
                                break;
                            }
                        }
                    }
                    $(\'' .
            $this->_elementValueId .
            '\').value = banners.join(\',\');
                    grid.reloadParams = {};
                    grid.reloadParams[\'selected_banners[]\'] = banners;
                }';
    }

    protected function _prepareColumns()
    {
        $this->addColumn(
            'in_banners',
            [
                'header_css_class' => 'col-select',
                'column_css_class' => 'col-select',
                'type' => 'checkbox',
                'name' => 'in_banners',
                'values' => $this->getSelectedBanners(),
                'filter' => false,
                'sortable' => false,
                'index' => 'banner_id'
            ]
        );

        $this->addColumn(
            'position',
            [
                'header' => __('Position'),
                'name' => 'position',
                'type' => 'number',
                'validate_class' => 'validate-number',
                'index' => 'position',
                'editable' => true,
                'filter' => false,
                'edit_only' => true,
                'sortable' => false
            ]
        );
        $this->addColumnsOrder('position', 'banner_sku');

        return parent::_prepareColumns();
    }

    protected function _prepareMassaction()
    {
        return $this;
    }

    public function getGridUrl()
    {
        return $this->getUrl(
            '[our admin route]/product_widget/chooser',
            [
                'banners_grid' => true,
                '_current' => true,
                'uniq_id' => $this->getId(),
                'selected_banners' => join(',', $this->getSelectedBanners())
            ]
        );
    }

    public function setSelectedBanners($selectedBanners)
    {
        if (is_string($selectedBanners)) {
            $selectedBanners = explode(',', $selectedBanners);
        }
        $this->_selectedBanners = $selectedBanners;
        return $this;
    }
    protected function _prepareCollection()
    {
        parent::_prepareCollection();

        foreach ($this->getCollection() as $item) {
            foreach ($this->getSelectedBanners() as $pos => $banner) {
                if ($banner == $item->getBannerId()) {
                    $item->setPosition($pos + 1);
                }
            }
        }
        return $this;
    }
    public function getSelectedBanners()
    {
        if ($selectedBanners = $this->getRequest()->getParam('selected_banners', $this->_selectedBanners)) {
            $this->setSelectedBanners($selectedBanners);
        }
        return $this->_selectedBanners;
    }
}

в котором мы так же чуть подкорректировали коллекцию, колонки грида и изменили фильтрацию на нашу app/code/[company]/[module]/Controller/Adminhtml/Product/Widget/Chooser.php

<?php

namespace [company]\[module]\Controller\Adminhtml\Product\Widget;
class Chooser extends \Magento\Backend\App\Action
{
    const ADMIN_RESOURCE = 'Magento_Banner::magento_banner';

    public function execute()
    {
        $uniqId = $this->getRequest()->getParam('uniq_id');

        $bannersGrid = $this->_view->getLayout()->createBlock(
            \[company]\[module]\Block\Adminhtml\Widget\ColumnChooser::class,
            '',
            ['data' => ['id' => $uniqId]]
        );
        $html = $bannersGrid->toHtml();

        $this->getResponse()->setBody($html);
    }

}

И так, чусер и грид подкорректированны (php docs, корректировка нейминга и удаление лишнего пропущено сознательно). Переходим к блоку виджета, где мы будем принимать выбранные данные.

app/code/[company]/[module]/Block/Widget/[block name].php

<?php

namespace [company]\[module]\Block\Widget;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\ProductRepositoryInterfaceFactory;
use Magento\Catalog\Block\Product\ListProduct;
use Magento\Catalog\Helper\ImageFactory as ImageHelper;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductFactory;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\View\Element\Template\Context;
use Magento\Widget\Helper\Conditions;

class [block name] extends \Magento\Framework\View\Element\Template implements \Magento\Widget\Block\BlockInterface
{
    protected $_template = "[company]_[module]::product/widget/product_[block name].phtml";

    protected $productRepositoryFactory;

    private $listProduct;

    protected $productFactory;

    protected $imageHelper;

    protected $conditionsHelper;

    protected $searchCriteriaBuilder;
    
    private $productRepositoryInterface;

    public function __construct(
        ListProduct $listProduct,
        Context $context,
        ProductRepositoryInterface $productRepositoryInterface,
        ProductRepositoryInterfaceFactory $productRepositoryFactory,
        ProductFactory $productFactory,
        ImageHelper $imageHelper,
        SearchCriteriaBuilder $searchCriteriaBuilder
    ) {
        $this->listProduct = $listProduct;
        $this->productRepositoryInterface = $productRepositoryInterface;
        $this->productRepositoryFactory = $productRepositoryFactory;
        $this->productFactory = $productFactory;
        $this->imageHelper = $imageHelper;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        parent::__construct($context, []);
    }

    public function getSelectedProductsIds()
    {
        $paramNames = ['[id for 1st column products]',  '[id for 2nd column products]', '[id for 3rd column products]'];
        $sortedSelections = [];
        foreach ($paramNames as $paramName) {
            $ids = $this->getData($paramName);
            $sortedSelections[$paramName]=$ids;
        }
        return $sortedSelections;
    }
    
    public function getAllProducts($productsInColumn)
    {
        $columnsIds = $this->getSelectedProductsIds();
        $resultCollection = [];
        foreach ($columnsIds as $column => $ids) {
            $ids = explode(',', $ids);
            $criteria = $this->searchCriteriaBuilder->addFilter('entity_id', $ids, 'in')->create();
            $productFactory = $this->productRepositoryFactory->create();
            $collection = $productFactory->getList($criteria);
            $resultCollection[] = array_slice($collection->getItems(), 0, $productsInColumn);
        }
        return $resultCollection;
    }

    public function getProductUrl($product)
    {
        return  $product->getUrlModel()->getUrl($product);
    }

    public function getProductImage($product)
    {
        $imageUrlGrid = $this->imageHelper->create()->init($product, 'category_page_grid')->getUrl();

        return $imageUrlGrid;
    }
}

в нем мы берем выбранные продукты и добавляем методы для некоторых значений продуктов.

Приятным бонусом, является то, что коллекция у нас уже создается только с теми продуктами, которые на стоке и включены к показу. Так же мы укорачиваем ее по значению продуктов, которые необходимо отображать в темплейте app/code/[company]/[module]/view/frontend/templates/product/widget/product_[block name].phtml

<?php

/**
 * @var [company]\[module]\block\Widget\[block name] $block
 */
?>
<?php $productsInColumn = 2; ?>
<?php $allProducts = $block->getAllProducts($productsInColumn); ?>
<?php $firstColumnProducts = $allProducts[0]; ?>
<?php $secondColumnProducts = $allProducts[1]; ?>
<?php $thirdColumnProducts = $allProducts[2]; ?>
<?php if ($firstColumnProducts) : ?>
    <?php foreach ($firstColumnProducts as $firstColumnProduct) : ?>
        <div>
            <a href="<?php /* @escapeNotVerified */
            echo $block->getProductUrl($firstColumnProduct); ?>">
                <img src="<?php /* @escapeNotVerified */
                echo $block->getProductImage($firstColumnProduct); ?>" alt="<?php /* @escapeNotVerified */
                echo $firstColumnProduct->getName(); ?>">
            </a>
            <p><?= /* @escapeNotVerified */
                __('Product name: %1', $firstColumnProduct->getName()); ?></p>
            <p><?= /* @escapeNotVerified */
                __('Product SKU: %1', $firstColumnProduct->getSku()); ?></p>
            <p><?= /* @escapeNotVerified */
                __('Product price: %1', $firstColumnProduct->getFinalPrice()); ?></p>
        </div>
    <?php endforeach; ?>
<?php endif; ?>
<?php if ($secondColumnProducts) : ?>
    <?php foreach ($secondColumnProducts as $secondColumnProduct) : ?>
        <div>
            <a href="<?php /* @escapeNotVerified */
            echo $block->getProductUrl($secondColumnProduct); ?>">
                <img src="<?php /* @escapeNotVerified */
                echo $block->getProductImage($secondColumnProduct); ?>" alt="<?php /* @escapeNotVerified */
                echo $secondColumnProduct->getName(); ?>">
            </a>
            <p><?= /* @escapeNotVerified */
                __('Product name: %1', $secondColumnProduct->getName()); ?></p>
            <p><?= /* @escapeNotVerified */
                __('Product SKU: %1', $secondColumnProduct->getSku()); ?></p>
            <p><?= /* @escapeNotVerified */
                __('Product price: %1', $secondColumnProduct->getFinalPrice()); ?></p>
        </div>
    <?php endforeach; ?>
<?php endif; ?>
<?php if ($thirdColumnProducts) : ?>
    <?php foreach ($thirdColumnProducts as $thirdColumnProduct) : ?>
        <div>
            <a href="<?php /* @escapeNotVerified */
            echo $block->getProductUrl($thirdColumnProduct); ?>">
                <img src="<?php /* @escapeNotVerified */
                echo $block->getProductImage($thirdColumnProduct); ?>" alt="<?php /* @escapeNotVerified */
                echo $thirdColumnProduct->getName(); ?>">
            </a>
            <p><?= /* @escapeNotVerified */
                __('Product name: %1', $thirdColumnProduct->getName()); ?></p>
            <p><?= /* @escapeNotVerified */
                __('Product SKU: %1', $thirdColumnProduct->getSku()); ?></p>
            <p><?= /* @escapeNotVerified */
                __('Product price: %1', $thirdColumnProduct->getFinalPrice()); ?></p>
        </div>
    <?php endforeach; ?>
<?php endif; ?>

Темплейт пока выводит все 6ть продуктов в одну колонку, соответственно его необходимо дорабатывать по части фронта. Показана лишь основная идея.

Так же для использования данного примера наобходимо заменить [*] на свои названия.

Собственно мы получили виджет, позволяющий используя множественный выбор продуктов сортировать согласно позициям и выводить необходимое колличество на фронте.

Надеюсь приведенная информация поможет упростить написание похожего функционала для вас.

Leave a Reply