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.