2

I've build a regular adminhtml form via UI component(s) including multiple fieldsets, input and select fields. All working like they should.

Now I've created a so called 'Event' select field within fieldset #1, who is responsible for hiding and displaying containers within fieldset #2. These containers correspond with the options within the select. This way they can be selected based on the option(s) value. Using this construction enables me to bundle specific fields who belong to the selected 'Event' select option.

This mechanism is required but create two problems;

  1. Hidden 'required' fields are still required when you remove it's parent container from the fieldset/form
  2. When the Events, containers and fields, including there own Source Models, start to grow... this form will become unnecessary heavy to load

Now I'm planning to load the Event container when selected. This way the UI component only loads the Source Model when required. If you switch, the previous container will be destroyed and a new one injected into the form.

This creates two questions;

  1. How to generate a UI component on the fly from within a Controller Action
  2. How to inject these component after a ajax-load

Just to be clear... everything is in place to make this possible. The only thing I need is an example who can answer those two questions.

define([
  'jquery',
  'uiComponent'
], function ($, Component) {
  'use strict';
  return Component.extend({
    defaults: {
      imports: {
        toggleEventContainer: '{event option}:value'
      }
    },

    /*
     * @param event
     * @return {exports}
     */
    toggleEventContainer: function (event)
    {
      // Ajax load here...
      return this;
    }
  });
});

Thanks in advance!

1

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.

|improve this answer|||||
0

This is Ui file

<field name="auto_lat_lang">
            <argument name="data" xsi:type="array">
                <item name="options" xsi:type="object">[vendor_name]\[module_name]\Model\LatLongStatus</item>
                <item name="config" xsi:type="array">
                    <item name="dataType" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Latitude and Longitude</item>
                    <item name="component" xsi:type="string">[vendor_name]_[module_name]/js/form/element/options</item>
                    <item name="formElement" xsi:type="string">select</item>
                    <item name="source" xsi:type="string">modulename</item>
                    <item name="dataScope" xsi:type="string">auto_lat_lang</item>
                    <item name="validation" xsi:type="array">
                        <item name="required-entry" xsi:type="boolean">true</item>
                    </item>
                </item>
            </argument>
        </field>
        <field name="lat">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="dataType" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Latitude</item>
                    <item name="formElement" xsi:type="string">input</item>
                    <item name="source" xsi:type="string">lat</item>
                    <item name="dataScope" xsi:type="string">lat</item>
                    <item name="validation" xsi:type="array">
                        <item name="required-entry" xsi:type="boolean">true</item>
                        <item name="validate-number" xsi:type="boolean">true</item>
                    </item>
                </item>
            </argument>
        </field>
        <field name="lng">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="dataType" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Longitude</item>
                    <item name="formElement" xsi:type="string">input</item>
                    <item name="source" xsi:type="string">lng</item>
                    <item name="dataScope" xsi:type="string">lng</item>
                    <item name="validation" xsi:type="array">
                        <item name="required-entry" xsi:type="boolean">true</item>
                        <item name="validate-number" xsi:type="boolean">true</item>
                    </item>
                </item>
            </argument>
        </field>

now create an Modal for get Custom option

<?php

namespace [module_name]\[Module_name]\Model;

use Magento\Framework\Data\OptionSourceInterface;

class LatLongStatus implements OptionSourceInterface
{
    public function getOptionArray()
    {
        $options = ['0' => __('Automatically'),'1' => __('Manually')];
        return $options;
    }

    public function getAllOptions()
    {
        $res = $this->getOptions();
        array_unshift($res, ['value' => '', 'label' => '']);
        return $res;
    }

    public function getOptions()
    {
        $res = [];
        foreach ($this->getOptionArray() as $index => $value) {
            $res[] = ['value' => $index, 'label' => $value];
        }
        return $res;
    }

    public function toOptionArray()
    {
        return $this->getOptions();
    }
}

now create an Js file to show and hide fields

[vendor_name][module_name]\view\adminhtml\web\js\form\element

options.js

define([
    'underscore',
    'uiRegistry',
    'Magento_Ui/js/form/element/select',
    'Magento_Ui/js/modal/modal'
], function (_, uiRegistry, select, modal) {
    'use strict';
    return select.extend({

        initialize: function (){

            var field1 = uiRegistry.get('index = lat');
            var field2 = uiRegistry.get('index = lng');
            var status = this._super().initialValue;
            if (status == 1) {
                field1.show();
                field2.show();
            } else{
               field2.hide();
               field1.hide();
            }
            return this;
        },
        onUpdate: function (value) {
            var field1 = uiRegistry.get('index = lat');
            var field2 = uiRegistry.get('index = lng');
            if (value == 1) {
                field1.show();
                field2.show();
            } else {
                field1.hide();
                field2.hide();
            }
            return this._super();
        },
    });
});

I Hope This Helps You.

|improve this answer|||||
  • @WillemPoortman what to do with Controller . Please explain in more Details – Msquare Nov 11 '19 at 17:45
  • I think you misunderstood my question... you answer the easy part which is already in place. I need to generate a UI component HTML within a controller loaded via ajax and assign it to the form – Willem Poortman Nov 12 '19 at 13:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.