Implementing the Delete Controller and Confirmation Dialog
Implementing the Delete Controller and Confirmation Dialog
~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapter 8 already built MassDelete for several selected rows. Deleting a single row directly from the actions column needs its own delete controller plus a client-side confirmation dialog that works without any custom JavaScript - Magento_Ui already ships both.
The delete link in the actions column
The generic Actions column class from chapter 6 only generates an edit link by default. A custom column renderer adds the delete link with a confirmation dialog - chapter 19 goes deeper into the full custom renderer pattern; here's the concrete actions class for testimonials up front:
<?php
declare(strict_types=1);
namespace Mironsoft\Testimonial\Ui\Component\Listing\Column;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;
/**
* Renders edit and delete links for each testimonial grid row.
*/
class TestimonialActions extends Column
{
private const URL_PATH_EDIT = 'mironsoft_testimonial/testimonial/edit';
private const URL_PATH_DELETE = 'mironsoft_testimonial/testimonial/delete';
/**
* @param ContextInterface $context Rendering context.
* @param UiComponentFactory $uiComponentFactory Factory for nested UI components.
* @param UrlInterface $urlBuilder Builds admin URLs for the row actions.
* @param array<string, mixed> $components Nested UI components.
* @param array<string, mixed> $data Additional UI Component data configuration.
*/
public function __construct(
ContextInterface $context,
UiComponentFactory $uiComponentFactory,
private readonly UrlInterface $urlBuilder,
array $components = [],
array $data = [],
) {
parent::__construct($context, $uiComponentFactory, $components, $data);
}
/**
* Adds edit/delete URLs to every grid row of this column.
*
* @param array<string, mixed> $dataSource Raw grid data source.
* @return array<string, mixed>
*/
public function prepareDataSource(array $dataSource): array
{
if (!isset($dataSource['data']['items'])) {
return $dataSource;
}
foreach ($dataSource['data']['items'] as &$item) {
$itemId = (int) $item['testimonial_id'];
$item[$this->getData('name')]['edit'] = [
'href' => $this->urlBuilder->getUrl(self::URL_PATH_EDIT, ['id' => $itemId]),
'label' => __('Edit'),
];
$item[$this->getData('name')]['delete'] = [
'href' => $this->urlBuilder->getUrl(self::URL_PATH_DELETE, ['id' => $itemId]),
'label' => __('Delete'),
'confirm' => [
'title' => __('Delete Testimonial'),
'message' => __('Are you sure you want to delete testimonial "%1"?', $item['customer_name']),
],
];
}
return $dataSource;
}
}The confirm configuration in the array is already fully sufficient to trigger Magento_Ui/js/modal/confirm on the client before the delete link actually fires - no custom JavaScript needed.
<actionsColumn name="actions" class="Mironsoft\Testimonial\Ui\Component\Listing\Column\TestimonialActions">
<settings>
<indexField>testimonial_id</indexField>
<resizeEnabled>false</resizeEnabled>
<sortable>false</sortable>
</settings>
</actionsColumn>Delete controller
<?php
declare(strict_types=1);
namespace Mironsoft\Testimonial\Controller\Adminhtml\Testimonial;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Mironsoft\Testimonial\Api\TestimonialRepositoryInterface;
/**
* Deletes a single testimonial identified via the id request parameter.
*/
class Delete extends Action
{
public const ADMIN_RESOURCE = 'Mironsoft_Testimonial::delete';
/**
* @param Context $context Backend action context.
* @param TestimonialRepositoryInterface $testimonialRepository Loads/deletes testimonials.
*/
public function __construct(
Context $context,
private readonly TestimonialRepositoryInterface $testimonialRepository,
) {
parent::__construct($context);
}
/**
* Deletes the requested testimonial and redirects back to the grid.
*
* @return ResultInterface
*/
public function execute(): ResultInterface
{
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$id = (int) $this->getRequest()->getParam('id');
if ($id) {
try {
$testimonial = $this->testimonialRepository->getById($id);
$this->testimonialRepository->delete($testimonial);
$this->messageManager->addSuccessMessage(__('You deleted the testimonial.'));
} catch (NoSuchEntityException $exception) {
$this->messageManager->addErrorMessage(__('This testimonial no longer exists.'));
}
}
return $resultRedirect->setPath('mironsoft_testimonial/testimonial/index');
}
}This requires adding delete() to TestimonialRepositoryInterface - a thin method that internally calls $this->resource->delete($testimonial), analogous to save() from chapter 16.
Worth noting: ADMIN_RESOURCE here deliberately points at Mironsoft_Testimonial::delete, not the general ::testimonial node from chapter 14 - that separation gets formally reflected in the ACL in chapter 25.
Tipp: For a single row, the confirmation dialog leads to exactly one GET request to the delete URL after confirming - an accidental mass deletion via double-click is practically ruled out, as long as the dialog isn't accidentally left out.