UI Component listing to drugi najczęściej używany UI Component w Magento 2. Dzisiaj pokaże Ci jak zaimplementować listing, który ma takie funkcjonalności jak m. in.:

  • wyświetlanie danych
  • filtrowanie
  • paginacja
  • masowe usuwanie i edycja danych

Niektóre czynności jak m. in. dodanie kontrolera, linku do menu, utworzenie ui componentu i layoutu tłumaczyłem i pokazywałem już w poprzednim artykule z tej serii, więc dzisiaj nie będę się na tych tematach skupiał. Cofnij się do tamtego artykułu jeśli potrzebujesz dodatkowych wyjaśnień.

Dodanie kontrolera odpowiedzialnego za wyświetlanie listingu

Chcę aby listing był dostępny pod ścieżką catalogbanners/banner/index, więc dodaje nowy kontroler Controller/Adminhtml/Banner/Index.php

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\Controller\Adminhtml\Banner;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;

/**
 * Class Index
 */
class Index extends Action implements HttpGetActionInterface
{
    /**
     * Authorization level of a basic admin session
     *
     * @see _isAllowed()
     */
    const ADMIN_RESOURCE = 'Mkwiatkowski_CatalogBanners::banner';

    /**
     * Menu identifier
     */
    const MENU_ID = 'Mkwiatkowski_CatalogBanners::banners_index';

    /**
     * @var PageFactory
     */
    protected $resultPageFactory;

    /**
     * Index constructor.
     *
     * @param Context $context
     * @param PageFactory $pageFactory
     */
    public function __construct(
        Context $context,
        PageFactory $pageFactory
    ) {
        $this->resultPageFactory = $pageFactory;

        parent::__construct($context);
    }

    /**
     * Execute.
     *
     * @return Page
     */
    public function execute() : Page
    {
        $resultPage = $this->resultPageFactory->create();
        $resultPage->setActiveMenu(static::MENU_ID);
        $resultPage->getConfig()->getTitle()->prepend(__('Catalog Banners list'));
        return $resultPage;
    }
}

Link w menu

Dodaje wpis do pliku etc/adminhtml/menu.xml 

 <add id="Mkwiatkowski_CatalogBanners::banners_index" title="Banners list" translate="title"
             module="Mkwiatkowski_CatalogBanners" parent="Mkwiatkowski_CatalogBanners::banners" sortOrder="5"
             action="catalogbanners/banner"
             resource="Mkwiatkowski_CatalogBanners::banner"/>

Link Banners list jest teraz widoczny w sekcji Catalog -> Catalog banners:

UI Component listing

Layout

Następnym krokiem jest dodanie layoutu view/adminhtml/layout/catalogbanners_banner_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <referenceContainer name="content">
        <uiComponent name="banner_listing"/>
    </referenceContainer>
</page>

Utworzenie ui componentu banner_listing

W layout dodaliśmy UI Component banner_listing, czas na jego deklarację i konfigurację

Plik XML Ui Componentu

Tworzymy plik view/adminhtml/ui_component/banner_listing.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
 * @copyright Copyright (C) 2020 Marcin Kwiatkowski (https://marcin-kwiatkowski.com)
 */
-->
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">banner_listing.banner_listing_data_source</item>
        </item>
    </argument>
    <settings>
        <buttons>
            <button name="add">
                <url path="*/*/form"/>
                <class>primary</class>
                <label translate="true">Add New Banner</label>
            </button>
        </buttons>
        <spinner>banner_columns</spinner>
        <deps>
            <dep>banner_listing.banner_listing_data_source</dep>
        </deps>
    </settings>
    <dataSource name="banner_listing_data_source" component="Magento_Ui/js/grid/provider">
        <settings>
            <storageConfig>
                <param name="indexField" xsi:type="string">banner_id</param>
            </storageConfig>
            <updateUrl path="mui/index/render"/>
        </settings>
        <aclResource>Mkwiatkowski_CatalogBanners::banner</aclResource>
        <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="banner_listing_data_source">
            <settings>
                <requestFieldName>banner_id</requestFieldName>
                <primaryFieldName>banner_id</primaryFieldName>
            </settings>
        </dataProvider>
    </dataSource>
    <listingToolbar name="listing_top">
        <settings>
            <sticky>true</sticky>
        </settings>
        <bookmark name="bookmarks"/>
        <columnsControls name="columns_controls"/>
        <filters name="listing_filters">
            <settings>
                <templates>
                    <filters>
                        <select>
                            <param name="template" xsi:type="string">ui/grid/filters/elements/ui-select</param>
                            <param name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</param>
                        </select>
                    </filters>
                </templates>
            </settings>
            <filterSelect name="category_id" provider="${ $.parentName }">
                <settings>
                    <captionValue>0</captionValue>
                    <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/>
                    <label translate="true">Category id</label>
                    <dataScope>category_id</dataScope>
                    <imports>
                        <link name="visible">componentType = column, index = ${ $.index }:visible</link>
                    </imports>
                </settings>
            </filterSelect>
        </filters>
        <massaction name="listing_massaction">
            <action name="delete">
                <settings>
                    <confirm>
                        <message translate="true">Are you sure you want to delete selected items?</message>
                        <title translate="true">Delete items</title>
                    </confirm>
                    <url path="catalogbanners/banner/massDelete"/>
                    <type>delete</type>
                    <label translate="true">Delete</label>
                </settings>
            </action>
            <action name="edit">
                <settings>
                    <callback>
                        <target>editSelected</target>
                        <provider>banner_listing.banner_listing.banner_columns_editor</provider>
                    </callback>
                    <type>edit</type>
                    <label translate="true">Edit</label>
                </settings>
            </action>
        </massaction>
        <paging name="listing_paging"/>
    </listingToolbar>
    <columns name="banner_columns">
        <settings>
            <editorConfig>
                <param name="clientConfig" xsi:type="array">
                    <item name="saveUrl" xsi:type="url" path="catalogbanners/banner/inlineEdit"/>
                    <item name="validateBeforeSave" xsi:type="boolean">false</item>
                </param>
                <param name="indexField" xsi:type="string">banner_id</param>
                <param name="enabled" xsi:type="boolean">true</param>
                <param name="selectProvider" xsi:type="string">banner_listing.banner_listing.banner_columns.ids</param>
            </editorConfig>
            <childDefaults>
                <param name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">banner_listing.banner_listing.banner_columns_editor</item>
                    <item name="target" xsi:type="string">startEdit</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
                        <item name="1" xsi:type="boolean">true</item>
                    </item>
                </param>
            </childDefaults>
        </settings>
        <selectionsColumn name="ids">
            <settings>
                <indexField>banner_id</indexField>
            </settings>
        </selectionsColumn>
        <column name="banner_id">
            <settings>
                <filter>textRange</filter>
                <label translate="true">ID</label>
                <sorting>asc</sorting>
            </settings>
        </column>
        <column name="is_active" component="Magento_Ui/js/grid/columns/select">
            <settings>
                <options class="Mkwiatkowski\CatalogBanners\Model\Banner\Source\IsActive"/>
                <filter>select</filter>
                <editor>
                    <editorType>select</editorType>
                </editor>
                <dataType>select</dataType>
                <label translate="true">Status</label>
            </settings>
        </column>
        <column name="category_id" class="Mkwiatkowski\CatalogBanners\Ui\Component\Listing\Column\Category">
            <settings>
                <label translate="true">Category</label>
            </settings>
        </column>
        <actionsColumn name="actions" class="Mkwiatkowski\CatalogBanners\Ui\Component\Listing\Column\BannerActions">
            <settings>
                <indexField>banner_id</indexField>
            </settings>
        </actionsColumn>
    </columns>
</listing>

Pokrótce wyjaśnie najważniejsze elementy tego pliku.

W czternastej lini dodajemy przycisk Add new banner, który przenosi na stronę dodawania nowego banneru.

ui component

W dwudziestej szóstej lini konfigurujemy wszystko co związane z danymi dostarczanymi do naszego uiComponentu. Jak widzisz wykorzystuję tutaj defaultowy DataProvider Magento: Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider (linia 34).

Linie od 41 do 94j to konfiguracja Toolbara, która zawiera m.in konfigurację filtrów, paginacji i mass akcji.

Ui component listing - toolbar

Od lini dziewiędziesiątej piątej konfigurujemy kolumny jakie mają być widoczne na listingu. Tutaj też konfiguruje się inline editor.

Konfiguracja kolekcji poprzez Dependency Injection

Aby nasza konfiguracja była kompletna musimy dodać kolekcję danych, która będzie używana w naszym listingu. Mamy już kolekcję Model/ResourceModel/Banner/Collection, jednak kolekcja, która jest wykorzystywana przez DataProvider (Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider) musi implementować interfejs Magento\Framework\Api\Search\SearchResultInterface, dlatego musimy utworzyć nową kolekcję Model/ResourceModel/Banner/Grid/Collection.php, która dziedziczy po naszej bazowej kolekcji i implementuje ten interfejs.

<?php
namespace Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner\Grid;

use Magento\Framework\Api\Search\AggregationInterface;
use Magento\Framework\Api\Search\SearchResultInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
use Magento\Framework\Data\Collection\EntityFactoryInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Event\ManagerInterface;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
use Magento\Framework\View\Element\UiComponent\DataProvider\Document;
use Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner\Collection as BannerCollection;
use Psr\Log\LoggerInterface;

/**
 * Class Collection
 */
class Collection extends BannerCollection implements SearchResultInterface
{
    /**
     * @var AggregationInterface
     */
    protected $aggregations;

    /**
     * Collection constructor.
     *
     * @param EntityFactoryInterface $entityFactory
     * @param LoggerInterface $logger
     * @param FetchStrategyInterface $fetchStrategy
     * @param ManagerInterface $eventManager
     * @param string $mainTable
     * @param string $eventPrefix
     * @param string $eventObject
     * @param string $resourceModel
     * @param string $model
     * @param AdapterInterface|null $connection
     * @param AbstractDb|null $resource
     */
    public function __construct(
        EntityFactoryInterface $entityFactory,
        LoggerInterface $logger,
        FetchStrategyInterface $fetchStrategy,
        ManagerInterface $eventManager,
        $mainTable,
        $eventPrefix,
        $eventObject,
        $resourceModel,
        $model = Document::class,
        AdapterInterface $connection = null,
        AbstractDb $resource = null
    ) {
        parent::__construct(
            $entityFactory,
            $logger,
            $fetchStrategy,
            $eventManager,
            $connection,
            $resource
        );

        $this->_eventPrefix = $eventPrefix;
        $this->_eventObject = $eventObject;
        $this->_init($model, $resourceModel);
        $this->setMainTable($mainTable);
    }

    /**
     * @inheritDoc
     */
    public function setItems(array $items = null)
    {
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getAggregations()
    {
        return $this->aggregations;
    }

    /**
     * @inheritDoc
     */
    public function setAggregations($aggregations)
    {
        $this->aggregations = $aggregations;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getSearchCriteria()
    {
        return null;
    }

    /**
     * @inheritDoc
     */
    public function setSearchCriteria(SearchCriteriaInterface $searchCriteria)
    {
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getTotalCount()
    {
        return $this->getSize();
    }

    /**
     * @inheritDoc
     */
    public function setTotalCount($totalCount)
    {
        return $this;
    }
}

Teraz czas na konfiguracje naszej kolekcji i UI Componentu w pliku etc/di.xml 

<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
    <arguments>
        <argument name="collections" xsi:type="array">
            <item name="banner_listing_data_source" xsi:type="string">Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner\Collection</item>
        </argument>
    </arguments>
</type>
<type name="Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner\Grid\Collection">
    <arguments>
        <argument name="mainTable" xsi:type="string">catalog_banners</argument>
        <argument name="eventPrefix" xsi:type="string">banner_grid_collection</argument>
        <argument name="eventObject" xsi:type="string">banner_grid_collection</argument>
        <argument name="resourceModel" xsi:type="string">Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner</argument>
    </arguments>
</type>

Kolumna kategorii

W formularzu dodawania/edycji banneru administrator ma możliwość wybrania kategorii z listy, a w bazie danych zapisywane jest ID kategorii, natomiast na listingu chcemy pokazywać nazwę kategorii, ponieważ ID nic nam nie mówi. W konfiguracji XML komponentu nasza kolumna kategorii wygląda tak:

<column name="category_id" class="Mkwiatkowski\CatalogBanners\Ui\Component\Listing\Column\Category">
    <settings>
        <label translate="true">Category</label>
    </settings>
</column>

Czas na zadeklarowanie klasy  Ui/Component/Listing/Column/Category.php , której zadaniem będzie pobranie nazwy kategorii na podstawie ID.

<?php
declare(strict_types=1);

namespace Mkwiatkowski\CatalogBanners\Ui\Component\Listing\Column;

use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;

/**
 * Class Category
 */
class Category extends Column
{
    /**
     * @var CategoryRepositoryInterface
     */
    private $categoryRepository;

    /**
     * Category constructor.
     *
     * @param ContextInterface $context
     * @param UiComponentFactory $uiComponentFactory
     * @param CategoryRepositoryInterface $categoryRepository
     * @param array $components
     * @param array $data
     */
    public function __construct(
        ContextInterface $context,
        UiComponentFactory $uiComponentFactory,
        CategoryRepositoryInterface $categoryRepository,
        array $components = [],
        array $data = []
    ) {
        parent::__construct($context, $uiComponentFactory, $components, $data);
        $this->categoryRepository = $categoryRepository;
    }

    /**
     * Prepare data source.
     *
     * @param array $dataSource
     * @return array
     */
    public function prepareDataSource(array $dataSource) : array
    {
        if (isset($dataSource['data']['items'])) {
            foreach ($dataSource['data']['items'] as &amp; $item) {
                if (isset($item['banner_id'])) {
                    try {
                        $category = $this->categoryRepository->get($item['category_id']);
                        $item[$this->getData('name')] = $category->getName();
                    } catch (NoSuchEntityException $e) {
                    }

                }
            }
        }

        return $dataSource;
    }
}

Kolumna z akcjami

Teraz zadeklarujemy kolumnę Actions. Chcemy mieć tam dwie akcje: Edycja i Usuwanie. Edycja przenosi do formularza edycji bannera, natomiast usuwanie usuwa wybrany wiersz. Deklarujemy klasę Ui/Component/Listing/Column/BannerActions.php 

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\Ui\Component\Listing\Column;

use Magento\Framework\Escaper;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;

/**
 * Class BannerActions
 */
class BannerActions extends Column
{
    /**
     * Url path
     */
    const URL_PATH_EDIT = 'catalogbanners/banner/form';
    const URL_PATH_DELETE = 'catalogbanners/banner/delete';

    /**
     * @var UrlInterface
     */
    protected $urlBuilder;

    /**
     * @var Escaper
     */
    private $escaper;

    /**
     * Constructor
     *
     * @param ContextInterface $context
     * @param UiComponentFactory $uiComponentFactory
     * @param UrlInterface $urlBuilder
     * @param Escaper $escaper
     * @param array $components
     * @param array $data
     */
    public function __construct(
        ContextInterface $context,
        UiComponentFactory $uiComponentFactory,
        UrlInterface $urlBuilder,
        Escaper $escaper,
        array $components = [],
        array $data = []
    ) {
        $this->urlBuilder = $urlBuilder;
        $this->escaper = $escaper;

        parent::__construct($context, $uiComponentFactory, $components, $data);
    }

    /**
     * Prepare data source.
     *
     * @param array $dataSource
     * @return array
     */
    public function prepareDataSource(array $dataSource) : array
    {
        if (isset($dataSource['data']['items'])) {
            foreach ($dataSource['data']['items'] as &amp; $item) {
                if (isset($item['banner_id'])) {
                    $bannerId = $this->escaper->escapeHtml($item['banner_id']);
                    $item[$this->getData('name')] = [
                        'edit' => [
                            'href' => $this->urlBuilder->getUrl(
                                static::URL_PATH_EDIT,
                                [
                                    'banner_id' => $item['banner_id']
                                ]
                            ),
                            'label' => __('Edit')
                        ],
                        'delete' => [
                            'href' => $this->urlBuilder->getUrl(
                                static::URL_PATH_DELETE,
                                [
                                    'banner_id' => $item['banner_id']
                                ]
                            ),
                            'label' => __('Delete'),
                            'confirm' => [
                                'title' => __('Delete banner with %1', $bannerId),
                                'message' => __('Are you sure you want to delete a banner with %1 id?', $bannerId)
                            ],
                            'post' => true
                        ]
                    ];
                }
            }
        }

        return $dataSource;
    }
}

Metoda prepareDataSource dodaje akcje edycji i usuwania, dodatkowo dla akcji delete dodajemy popup confirm, tak aby administrator musiał potwierdzić akcję usunięcia.Dzięki temu zapobiegniemy usunięcia bannera przez przypadkowe kliknięcie.

Oto nasze akcje w akcji:

Actions

Usunięcie banneru

Przycisk akcji usunięcia kieruje do ścieżki catalogbanners/banner/delete, więć musimy zaimplementować kontroler, który obsłuży tą akcję. Tworzymy nową klasę Controller/Adminhtml/Banner/Delete.php 

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\Controller\Adminhtml\Banner;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\View\Result\Redirect;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Mkwiatkowski\CatalogBanners\Api\BannerRepositoryInterface;

/**
 * Class Delete
 */
class Delete extends Action implements HttpPostActionInterface
{
    /**
     * @var BannerRepositoryInterface
     */
    private $bannerRepository;

    /**
     * Delete constructor.
     *
     * @param Context $context
     * @param BannerRepositoryInterface $bannerRepository
     */
    public function __construct(
        Context $context,
        BannerRepositoryInterface $bannerRepository
    ) {
        $this->bannerRepository = $bannerRepository;

        parent::__construct($context);
    }

    /**
     * Execute.
     *
     * @return Redirect
     */
    public function execute() : Redirect
    {
        /** @var Redirect $resultRedirect */
        $resultRedirect = $this->resultRedirectFactory->create();
        $id = $this->getRequest()->getParam('banner_id');

        if ($id) {
            try {
                $this->bannerRepository->deleteById((int) $id);
                $this->messageManager->addSuccessMessage(__('You deleted the banner.'));

                return $resultRedirect->setPath('*/*/');
            } catch (\Exception $e) {
                $this->messageManager->addErrorMessage($e->getMessage());

                return $resultRedirect->setPath('*/*/');
            }
        }

        $this->messageManager->addErrorMessage(__('We can\'t find a banner to delete.'));

        return $resultRedirect->setPath('*/*/');
    }
}

Inline Editor

Pora na dokonfigurowanie inline editora. Jest to funkcjonalność, dzięki której możemy edytować niektóre informacje w wierszach bez konieczności wchodzenia na formularz edycji. W naszym przypadku chcemy edytować status. Przypomnijmy sobie konfigurację kolumny Status

<column name="is_active" component="Magento_Ui/js/grid/columns/select">
    <settings>
        <options class="Mkwiatkowski\CatalogBanners\Model\Banner\Source\IsActive"/>
        <filter>select</filter>
        <editor>
            <editorType>select</editorType>
        </editor>
        <dataType>select</dataType>
        <label translate="true">Status</label>
    </settings>
</column>

W piątej lini definiujemy typ edytora (tutaj jest to select), a w trzeciej podajemy dla węzła <options> klasę  Model/Banner/Source/IsActive.php, której obiekt będzie dostarczał te opcje.

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\Model\Banner\Source;

use Magento\Framework\Data\OptionSourceInterface;

/**
 * Class IsActive
 */
class IsActive implements OptionSourceInterface
{

    /**
     * Get options array.
     *
     * @return array
     */
    public function toOptionArray() : array
    {
        return [
            [
                'value' => 0,
                'label' => __('Disabled')
            ],
            [
                'value' => 1,
                'label' => __('Enabled'),
            ]

        ];
    }
}

Teraz przypomnijmy sobie konfigurację dla Inline editora w naszym komponencie:

<editorConfig>
    <param name="clientConfig" xsi:type="array">
        <item name="saveUrl" xsi:type="url" path="catalogbanners/banner/inlineEdit"/>
        <item name="validateBeforeSave" xsi:type="boolean">false</item>
    </param>
    <param name="indexField" xsi:type="string">banner_id</param>
    <param name="enabled" xsi:type="boolean">true</param>
    <param name="selectProvider" xsi:type="string">banner_listing.banner_listing.banner_columns.ids</param>
</editorConfig>

Jak widzisz w trzeciej lini kodu jest definiowana klasa Controller/Adminhtml/Banner/InlineEdit.php, która będzie odpowiedzialna za zapis zmian.

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\Controller\Adminhtml\Banner;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\Result\Json;
use Mkwiatkowski\CatalogBanners\Api\BannerRepositoryInterface;
use Mkwiatkowski\CatalogBanners\Api\Data\BannerInterface;
use Mkwiatkowski\CatalogBanners\Model\Banner;

/**
 * Class InlineEdit
 */
class InlineEdit extends Action implements HttpPostActionInterface
{
    /**
     * Authorization level of a basic admin session
     *
     * @see _isAllowed()
     */
    const ADMIN_RESOURCE = 'Mkwiatkowski_CatalogBanners::banner';

    /**
     * @var BannerRepositoryInterface
     */
    private $bannerRepository;

    /**
     * @var JsonFactory
     */
    private $jsonFactory;

    /**
     * InlineEdit constructor.
     *
     * @param Context $context
     * @param BannerRepositoryInterface $bannerRepository
     * @param JsonFactory $jsonFactory
     */
    public function __construct(
        Context $context,
        BannerRepositoryInterface $bannerRepository,
        JsonFactory $jsonFactory
    ) {
        $this->bannerRepository = $bannerRepository;
        $this->jsonFactory = $jsonFactory;

        parent::__construct($context);
    }

    /**
     * Execute.
     *
     * @return Json
     */
    public function execute() : Json
    {
        $resultJson = $this->jsonFactory->create();
        $error = false;
        $messages = [];

        if ($this->getRequest()->getParam('isAjax')) {
            $postItems = $this->getRequest()->getParam('items', []);
            if (!count($postItems)) {
                $messages[] = __('Please correct the data sent.');
                $error = true;
            } else {
                foreach (array_keys($postItems) as $bannerId) {
                    try {
                        /** @var Banner $banner */
                        $banner = $this->bannerRepository->getById($bannerId);
                        $banner->setData(array_merge($banner->getData(), $postItems[$bannerId]));
                        $this->bannerRepository->save($banner);
                    } catch (\Exception $e) {
                        $messages[] = $this->getErrorWithBannerId(
                            $banner,
                            __($e->getMessage())
                        );
                        $error = true;
                    }
                }
            }
        }

        return $resultJson->setData([
            'messages' => $messages,
            'error' => $error
        ]);
    }

    /**
     * Add banner title to error message
     *
     * @param BannerInterface $banner
     * @param string $errorText
     * @return string
     */
    private function getErrorWithBannerId(BannerInterface $banner, $errorText) : string
    {
        return '[Banner ID: ' . $banner->getId() . '] ' . $errorText;
    }
}

Gotowy formularz edycji wygląda tak:

Inline edit

Dzięki mass akcją jest możliwość edytowania kilku wierszy w jednym czasie:

Mass actions

No właśnie… czas na zaimplementowania mass akcji!

Mass actions

Konfiguracja mass akcji w naszym komponencie wygląda tak:

<massaction name="listing_massaction">
    <action name="delete">
        <settings>
            <confirm>
                <message translate="true">Are you sure you want to delete selected items?</message>
                <title translate="true">Delete items</title>
            </confirm>
            <url path="catalogbanners/banner/massDelete"/>
            <type>delete</type>
            <label translate="true">Delete</label>
        </settings>
    </action>
    <action name="edit">
        <settings>
            <callback>
                <target>editSelected</target>
                <provider>banner_listing.banner_listing.banner_columns_editor</provider>
            </callback>
            <type>edit</type>
            <label translate="true">Edit</label>
        </settings>
    </action>
</massaction>

Jedyne co nam pozostało do zrobienia to kontroler Controller/Adminhtml/Banner/MassDelete.php (patrz linia 8), który będzie odpowiedzialny za masowe usuwanie.

<?php
declare(strict_types=1);
namespace Mkwiatkowski\CatalogBanners\Controller\Adminhtml\Banner;

use Magento\Backend\Model\View\Result\Redirect;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Ui\Component\MassAction\Filter;
use Mkwiatkowski\CatalogBanners\Model\ResourceModel\Banner\CollectionFactory;

/**
 * Class MassDelete
 */
class MassDelete extends Action implements HttpPostActionInterface
{
    /**
     * Authorization level of a basic admin session
     *
     * @see _isAllowed()
     */
    const ADMIN_RESOURCE = 'Mkwiatkowski_CatalogBanners::banner';

    /**
     * @var Filter
     */
    private $filter;

    /**
     * @var CollectionFactory
     */
    private $collectionFactory;

    /**
     * MassDelete constructor.
     *
     * @param Context $context
     * @param Filter $filter
     * @param CollectionFactory $collectionFactory
     */
    public function __construct(
        Context $context,
        Filter $filter,
        CollectionFactory $collectionFactory
    ) {
        $this->filter = $filter;
        $this->collectionFactory = $collectionFactory;

        parent::__construct($context);
    }

    /**
     * Execute.
     *
     * @return Redirect
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function execute() : Redirect
    {
        $collection = $this->filter->getCollection($this->collectionFactory->create());
        $collectionSize = $collection->getSize();

        foreach ($collection as $banner) {
            $banner->delete();
        }

        $this->messageManager->addSuccessMessage(__('A total of %1 record(s) have been deleted.', $collectionSize));

        /** @var Redirect $resultRedirect */
        $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);

        return $resultRedirect->setPath('*/*/');
    }
}

UI component listing: podsumowanie

Uff… wyszedł bardzo długi artykuł, tak samo jak poprzednio gdy pisałem o UI komponencie form, co tylko potwierdza fakt, że UI componenty to bardzo złożony temat i wymaga jeszcze wielu wyjaśnień. Cały kod dotyczący tego ui componentu znajdziesz w repozytorium projektu na Githubie.