I see that in the latest versions of Magento 2, the add/edit form for categories, blocks and pages are generated via a ui component form xml.
Here is the example for cms pages.
I want to use that for my own module like the big boys do.
I got most of the things figured out, but I wasn't able to add a fieldset that retrieves its contents via AJAX.
For example, I want to associate my custom entity "many to many" with the products, so I need a fieldset that contains nothing at first, but when is clicked it will retrieve the list of products via an ajax call.
I don't need the actual products grid code. All I need is a way to do an ajax call when clicking the fieldset header.
Magento 2 Form UI Component with AJAX Fieldset
ajaxmagento2uicomponent
Related Solutions
Using dataPersistor to get Form data, thought you're using getSession
$data = $this->getSession()->getFormData();
Save action
<?php
namespace Vendor\Module\Controller\Adminhtml\Scheduler;
use Magento\Framework\Exception\LocalizedException;
class Save extends \Magento\Backend\App\Action
{
protected $dataPersistor;
/**
* @param \Magento\Backend\App\Action\Context $context
* @param \Magento\Framework\App\Request\DataPersistorInterface $dataPersistor
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
\Magento\Framework\App\Request\DataPersistorInterface $dataPersistor
) {
$this->dataPersistor = $dataPersistor;
parent::__construct($context);
}
/**
* Save actionSave action
*
* @return \Magento\Framework\Controller\ResultInterface
*/
public function execute()
{
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
$data = $this->getRequest()->getPostValue();
if ($data) {
$id = $this->getRequest()->getParam('scheduler_id');
$model = $this->_objectManager->create('Vendor\Module\Model\Scheduler')->load($id);
if (!$model->getId() && $id) {
$this->messageManager->addErrorMessage(__('This Scheduler no longer exists.'));
return $resultRedirect->setPath('*/*/');
}
$model->setData($data);
try {
$model->save();
$this->messageManager->addSuccessMessage(__('You saved the Scheduler.'));
$this->dataPersistor->clear('engraving_scheduler');
if ($this->getRequest()->getParam('back')) {
return $resultRedirect->setPath('*/*/edit', ['scheduler_id' => $model->getId()]);
}
return $resultRedirect->setPath('*/*/');
} catch (LocalizedException $e) {
$this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
$this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the Scheduler.'));
}
$this->dataPersistor->set('engraving_scheduler', $data);
return $resultRedirect->setPath('*/*/edit', ['scheduler_id' => $this->getRequest()->getParam('scheduler_id')]);
}
return $resultRedirect->setPath('*/*/');
}
}
DataProvider
<?php
namespace Vendor\Module\Model\Scheduler;
use Vendor\Module\Model\ResourceModel\Scheduler\CollectionFactory;
use Magento\Framework\App\Request\DataPersistorInterface;
class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider
{
protected $dataPersistor;
protected $loadedData;
protected $collection;
/**
* Constructor
*
* @param string $name
* @param string $primaryFieldName
* @param string $requestFieldName
* @param CollectionFactory $collectionFactory
* @param DataPersistorInterface $dataPersistor
* @param array $meta
* @param array $data
*/
public function __construct(
$name,
$primaryFieldName,
$requestFieldName,
CollectionFactory $collectionFactory,
DataPersistorInterface $dataPersistor,
array $meta = [],
array $data = []
) {
$this->collection = $collectionFactory->create();
$this->dataPersistor = $dataPersistor;
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
}
/**
* Get data
*
* @return array
*/
public function getData()
{
if (isset($this->loadedData)) {
return $this->loadedData;
}
$items = $this->collection->getItems();
foreach ($items as $model) {
$this->loadedData[$model->getId()] = $model->getData();
}
$data = $this->dataPersistor->get('engraving_scheduler');
if (!empty($data)) {
$model = $this->collection->getNewEmptyItem();
$model->setData($data);
$this->loadedData[$model->getId()] = $model->getData();
$this->dataPersistor->clear('engraving_scheduler');
}
return $this->loadedData;
}
}
ui form xml
<?xml version="1.0" ?>
<form 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">engraving_scheduler_form.scheduler_form_data_source</item>
<item name="deps" xsi:type="string">engraving_scheduler_form.scheduler_form_data_source</item>
</item>
<item name="label" translate="true" xsi:type="string">General Information</item>
<item name="config" xsi:type="array">
<item name="dataScope" xsi:type="string">data</item>
<item name="namespace" xsi:type="string">engraving_scheduler_form</item>
</item>
<item name="template" xsi:type="string">templates/form/collapsible</item>
<item name="buttons" xsi:type="array">
<item name="back" xsi:type="string">Vendor\Module\Block\Adminhtml\Scheduler\Edit\BackButton</item>
<item name="delete" xsi:type="string">Vendor\Module\Block\Adminhtml\Scheduler\Edit\DeleteButton</item>
<item name="save" xsi:type="string">Vendor\Module\Block\Adminhtml\Scheduler\Edit\SaveButton</item>
<item name="save_and_continue" xsi:type="string">Vendor\Module\Block\Adminhtml\Scheduler\Edit\SaveAndContinueButton</item>
</item>
</argument>
<dataSource name="scheduler_form_data_source">
<argument name="dataProvider" xsi:type="configurableObject">
<argument name="class" xsi:type="string">Vendor\Module\Model\Scheduler\DataProvider</argument>
<argument name="name" xsi:type="string">scheduler_form_data_source</argument>
<argument name="primaryFieldName" xsi:type="string">scheduler_id</argument>
<argument name="requestFieldName" xsi:type="string">scheduler_id</argument>
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="submit_url" path="*/*/save" xsi:type="url"/>
</item>
</argument>
</argument>
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/form/provider</item>
</item>
</argument>
</dataSource>
<fieldset name="General">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="label" xsi:type="string"/>
</item>
</argument>
<field name="label">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="dataType" xsi:type="string">text</item>
<item name="label" translate="true" xsi:type="string">Title</item>
<item name="formElement" xsi:type="string">input</item>
<item name="source" xsi:type="string">Scheduler</item>
<item name="sortOrder" xsi:type="number">10</item>
<item name="dataScope" xsi:type="string">label</item>
<item name="validation" xsi:type="array">
<item name="required-entry" xsi:type="boolean">true</item>
</item>
</item>
</argument>
</field>
</fieldset>
</form>
I've investigated this subject a little deeper and tried multiple solutions to fix the problem. Injecting a subform (UI component fields) via AJAX didn't work without having to write a ton of code.
Module\Name\Plugin\Magento\Framework\View\Element\UiComponentFactory
<?php
declare(strict_types=1);
namespace Module\Name\Plugin\Magento\Framework\View\Element;
use Module\Name\Registry\ActionHookEventSelect as ActionHookEventSelectRegistry;
use Magento\Framework\Config\DataInterface;
use Magento\Framework\Config\DataInterfaceFactory;
use Magento\Framework\View\Element\UiComponentFactory as Subject;
use Magento\Framework\View\Element\UiComponentInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Ui\Component\Form as FormComponent;
use Psr\Log\LoggerInterface;
/**
* Class UiComponentFactory
*
* @package Module\Name\Plugin\Magento\Framework\View\Element
*/
class UiComponentFactory
{
public const FORM_COMPONENT_NS = 'your_form_namespace';
/** @var ActionHookEventSelectRegistry $actionHookEventSelectRegistry */
protected $actionHookEventSelectRegistry;
/** @var LoggerInterface $loggerInterface */
protected $loggerInterface;
/** @var DataInterfaceFactory $configFactory */
protected $configFactory;
/**
* UiComponentFactory constructor.
*
* @param ActionHookEventSelectRegistry $actionHookEventSelectRegistry
* @param LoggerInterface $loggerInterface
* @param DataInterfaceFactory $configFactory
*/
public function __construct(
ActionHookEventSelectRegistry $actionHookEventSelectRegistry,
LoggerInterface $loggerInterface,
DataInterfaceFactory $configFactory
) {
$this->actionHookEventSelectRegistry = $actionHookEventSelectRegistry;
$this->loggerInterface = $loggerInterface;
$this->configFactory = $configFactory;
}
/**
* Injects the selection event options UI Component
* configuration into the form UI Component. A default
* will be set if no ../adminhtml/ui_component/*.xml exists
*
* @param Subject $subject
* @param UiComponentInterface $component
*
* @return UiComponentInterface
*/
public function afterCreate(Subject $subject, UiComponentInterface $component): UiComponentInterface
{
if ($this->actionHookEventSelectRegistry->has()
&& $component->getName() === self::FORM_COMPONENT_NS) {
/** @var string $identifier */
$identifier = $this->actionHookEventSelectRegistry->get();
/** @var DataInterface $componentData */
$componentData = $this->configFactory->create(['componentName' => $identifier]);
if ($componentData->get($identifier) === null) {
$identifier = $this->actionHookEventSelectRegistry->getDefault(true);
}
/** @var array $childComponents */
$childComponents = $component->getChildComponents();
if (isset($childComponents[WebhookFormDataProvider::AREA_OPTIONS])) {
/** @var null|FormComponent $optionsContainer */
$optionsContainer = null;
try {
$optionsContainer = $subject->create($identifier);
} catch (LocalizedException $exception) {
$this->loggerInterface->error($exception->getMessage());
}
if ($optionsContainer) {
/** @var FormComponent\Fieldset $parentContainer */
$parentContainer = $childComponents[WebhookFormDataProvider::AREA_OPTIONS];
// Add custom Event Options container UI component to the 'custom_options'
$parentContainer->addComponent($identifier, $optionsContainer);
// Re-assign the parent container 'custom_options' back into the form
$component->addComponent(WebhookFormDataProvider::AREA_OPTIONS, $parentContainer);
}
}
}
return $component;
}
}
Module\Name\Registry\ActionHookEventSelect
<?php
declare(strict_types=1);
namespace Module\Name\Registry;
use Magento\Framework\Exception\AlreadyExistsException;
/**
* Class ActionHookEventSelect
*
* @package Module\Name\Registry
*/
class ActionHookEventSelect
{
public const DEFAULT_IDENTIFIER = 'missing';
public const NAMESPACE_PREFIX = 'form_event_options_';
/** @var null|string $identifier */
private $identifier;
/** @var string $namespacePrefix */
private $namespacePrefix;
/**
* ActionHookEventSelect constructor.
*
* @param string $namespacePrefix
*/
public function __construct(
string $namespacePrefix = self::NAMESPACE_PREFIX
) {
$this->namespacePrefix = $namespacePrefix;
}
/**
* @param string $identifier
* @param bool $validate
*
* @return $this
* @throws AlreadyExistsException
*/
public function set(string $identifier, $validate = true): self
{
if ($validate && $identifier === self::DEFAULT_IDENTIFIER) {
throw new AlreadyExistsException(
__('Namespace can not be the same as default')
);
}
$this->identifier = $identifier;
return $this;
}
/**
* Get selected event namespace
*
* @param bool $includePrefix
*
* @return string
*/
public function get($includePrefix = true): string
{
$identifier = $this->identifier ?? self::DEFAULT_IDENTIFIER;
if ($includePrefix) {
$identifier = $this->getNamespacePrefix() . $identifier;
}
return $identifier;
}
/**
* @return bool
*/
public function has(): bool
{
return $this->get(false) !== self::DEFAULT_IDENTIFIER;
}
/**
* @return string
*/
public function getNamespacePrefix(): string
{
return $this->namespacePrefix;
}
/**
* @param bool $includePrefix
*
* @return string
*/
public function getDefault($includePrefix = false): string
{
$current = $this->get(false);
try {
$this->set(self::DEFAULT_IDENTIFIER, false);
$default = $this->get($includePrefix);
$this->set($current);
} catch (AlreadyExistsException $exception) {
return self::NAMESPACE_PREFIX . self::DEFAULT_IDENTIFIER;
}
return $default;
}
}
This enabled you to create your own custom UI-component XML files within any custom module you like. The above example registers a selected 'event'. This can happen for example within a Controller Action where you set it on the Registry.
public function execute()
{
//...
try {
$this->actionHookEventSelectRegistry->set($event);
} catch (AlreadyExistsException $exception) {
$this->messageManager->addErrorMessage('Could not instantiate the event options');
}
//...
}
Hope this helps others! I'm really happy with the final result despite it required a different approach.
Best Answer
Currently Fieldset Ui Component doesn't suport such ability: click on fieldset -> load content via AJAX .
1) You can implement it by yourself
Take a look on fieldset.js. It can be easily extended with custom behavior. In same maner as fieldset.js extends collapsible.js.
2) You can use InsertListing Ui Component
As was pointed above its js part is a insert-listing.js file.
Sample can be used like this:
Where to put this?
In any Form Ui Component under
<fieldset>
's or<container>
's.What does component do?
It uses namespace
ns
as a locator for some Ui Component, loads it, and instantiate in place.cms_page_listing
is a name of configured Listing Ui Component.