How to create a modal window in a custom field with a code in Magento 2

2
28421
Reading Time: 6 minutes

In this article, we will show how to create a modal window with custom fields and add it to the existing UI form. Moreover, we will show you the example of such a modal in Magento 2. Let’s begin.

First, we need to create a Vendor_Module module:

1. Create a directory app/code/Vendor/Module
2. Create a registration file app/code/Vendor/Module/registration.php with the following content:

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_Module',
    __DIR__
);
?>

3. Create a composer file (if you plan to transfer the module) app/code/Vendor/Module/composer.json :

<script type="application/json">
    {
        "name": "vendor/module-module",
        "description": "N/A",
        "require": {
            "php": "~5.6.0|~7.0.0"
        },
        "type": "magento2-module",
        "version": "2.0.0",
        "license": [
            "OSL-3.0",
            "AFL-3.0"
        ],
        "autoload": {
            "files": [
                "registration.php"
            ],
            "psr-4": {
                "Vendor\\Module\\": ""
            }
        }
    }
</script>

4. Now, create the module’s main XML-file app/code/Vendor/Module/etc/module.xml with the dependency from the Magento_Catalog module because our modal window will be added to its form:

<?xml version="1.0"?>
<config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_Module" setup_version="2.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>

The preparation is done. Now, some more details.

You can see the new module in the Magento modules list by entering the following: bin/magento module:status

Then, you need to enable it with the following command: bin/magento module:enable Vendor_Module

Next, execute bin/magento setup:upgrade. After that, our module should be displayed in the system. However, it contains nothing at the moment. For the code starts working add the following:

Create a file app/code/Vendor/Module/etc/adminhtml/di.xml. We are going to place a modifier inside:

<?xml version="1.0"?>
<config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="custom-options-custom-modal" xsi:type="array">
                    <item name="class" xsi:type="string">Vendor\Module\Ui\DataProvider\Product\Form\Modifier\CustomModal</item>
                    <item name="sortOrder" xsi:type="number">71</item>
                    <!-- Because 70 is sort order of the regular custom options -->
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

The modifier is responsible for data addition and some manipulations with elements and UI-form components. There are 2 main methods that came from the modifier’s interface (they should always present):

<?php
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Ui\DataProvider\Modifier;

/**
 * Class ModifierInterface
 */
interface ModifierInterface
{
    /**
     * @param array $data
     * @return array
     */
    public function modifyData(array $data);

    /**
     * @param array $meta
     * @return array
     */
    public function modifyMeta(array $meta);
}
?>

We can add any data to our UI-form in the modifyData method or we can delete/modify the existing product data during the form creation. All the existing UI-form data will be displayed in the input value. Pay attention to the Sort Order of your modifier. A lot of data can be added to the following modifier.

We can add our UI components and elements in the modifyMeta method. In the example below we are going to add a simple modal window to the custom options fieldset with multiple elements.

Our modifier will be inherited from an abstract modifier for the Catalog_Product (vendor/magento/module-catalog/Ui/DataProvider/Product/Form/Modifier/AbstractModifier.php) module and will be located here: app/code/Vendor/Module/Ui/DataProvider/Product/Form/Modifier/CustomModal.php

Note! You can find the available UI components here: vendor/magento/module-ui/Component. It will be useful to look through the subfolders and see what the default Magento_UI module provides, before start utilizing it.

Let’s fill in our modifier’s file with the base content (namespace, class name, etc):

<?php
namespace Vendor\Module\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\UrlInterface;

class CustomModal extends AbstractModifier
{

    /**
     * @var \Magento\Catalog\Model\Locator\LocatorInterface
     */
    protected $locator;

    /**
     * @var ArrayManager
     */
    protected $arrayManager;

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

    /**
     * @var array
     */
    protected $meta = [];

    /**
     * @param LocatorInterface $locator
     * @param ArrayManager $arrayManager
     * @param UrlInterface $urlBuilder
     */
    public function __construct(
        LocatorInterface $locator,
        ArrayManager $arrayManager,
        UrlInterface $urlBuilder
    ) {
        $this->locator = $locator;
        $this->arrayManager = $arrayManager;
        $this->urlBuilder = $urlBuilder;
    }

    public function modifyData(array $data)
    {
        return $data;
    }

    public function modifyMeta(array $meta)
    {
        $this->meta = $meta;

        return $this->meta;
    }
}
?>

Now, it should be processed without any changes. Then, add the first UI-form element, a link, a modal window that should be called after hitting the link, and multiple fields to the modal window’s container.

<?php
namespace Vendor\Module\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\UrlInterface;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Fieldset;
use Magento\Ui\Component\Modal;
use Magento\Ui\Component\Form\Element\DataType\Number;
use Magento\Ui\Component\Form\Element\DataType\Text;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Element\MultiSelect;
use Magento\Ui\Component\Form\Field;

class CustomModal extends AbstractModifier
{

    const CUSTOM_MODAL_LINK = 'custom_modal_link';
    const CUSTOM_MODAL_INDEX = 'custom_modal';
    const CUSTOM_MODAL_CONTENT = 'content';
    const CUSTOM_MODAL_FIELDSET = 'fieldset';
    const CONTAINER_HEADER_NAME = 'header';

    const FIELD_NAME_1 = 'field1';
    const FIELD_NAME_2 = 'field2';
    const FIELD_NAME_3 = 'field3';

    /**
     * @var \Magento\Catalog\Model\Locator\LocatorInterface
     */
    protected $locator;

    /**
     * @var ArrayManager
     */
    protected $arrayManager;

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

    /**
     * @var array
     */
    protected $meta = [];

    /**
     * @param LocatorInterface $locator
     * @param ArrayManager $arrayManager
     * @param UrlInterface $urlBuilder
     */
    public function __construct(
        LocatorInterface $locator,
        ArrayManager $arrayManager,
        UrlInterface $urlBuilder
    ) {
        $this->locator = $locator;
        $this->arrayManager = $arrayManager;
        $this->urlBuilder = $urlBuilder;
    }

    public function modifyData(array $data)
    {
        return $data;
    }

    public function modifyMeta(array $meta)
    {
        $this->meta = $meta;
        $this->addCustomModal();
        $this->addCustomModalLink(10);

        return $this->meta;
    }

    protected function addCustomModal()
    {
        $this->meta = array_merge_recursive(
            $this->meta,
            [
                static::CUSTOM_MODAL_INDEX => $this->getModalConfig(),
            ]
        );
    }

    protected function getModalConfig()
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'componentType' => Modal::NAME,
                        'dataScope' => '',
                        'provider' => static::FORM_NAME . '.product_form_data_source',
                        'ns' => static::FORM_NAME,
                        'options' => [
                            'title' => __('Modal Title'),
                            'buttons' => [
                                [
                                    'text' => __('Save'),
                                    'class' => 'action-primary', // additional class
                                    'actions' => [
                                        [
                                            'targetName' => 'index = product_form', // Element selector
                                            'actionName' => 'save', // Save parent form (product)
                                        ],
                                        'closeModal', // method name
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
            'children' => [
                static::CUSTOM_MODAL_CONTENT => [
                    'arguments' => [
                        'data' => [
                            'config' => [
                                'autoRender' => false,
                                'componentType' => 'container',
                                'dataScope' => 'data.product', // save data in the product data
                                'externalProvider' => 'data.product_data_source',
                                'ns' => static::FORM_NAME,
                                'render_url' => $this->urlBuilder->getUrl('mui/index/render'),
                                'realTimeLink' => true,
                                'behaviourType' => 'edit',
                                'externalFilterMode' => true,
                                'currentProductId' => $this->locator->getProduct()->getId(),
                            ],
                        ],
                    ],
                    'children' => [
                        static::CUSTOM_MODAL_FIELDSET => [
                            'arguments' => [
                                'data' => [
                                    'config' => [
                                        'label' => __('Fieldset'),
                                        'componentType' => Fieldset::NAME,
                                        'dataScope' => 'custom_data',
                                        'collapsible' => true,
                                        'sortOrder' => 10,
                                        'opened' => true,
                                    ],
                                ],
                            ],
                            'children' => [
                                static::CONTAINER_HEADER_NAME => $this->getHeaderContainerConfig(10),
                                static::FIELD_NAME_1 => $this->getFirstFieldConfig(20),
                                static::FIELD_NAME_2 => $this->getSecondFieldConfig(30),
                                static::FIELD_NAME_3 => $this->getThirdFieldConfig(40),
                            ],
                        ],
                    ],
                ],
            ],
        ];
    }

    /**
     * Get config for header container
     *
     * @param int $sortOrder
     * @return array
     */
    protected function getHeaderContainerConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => null,
                        'formElement' => Container::NAME,
                        'componentType' => Container::NAME,
                        'template' => 'ui/form/components/complex',
                        'sortOrder' => $sortOrder,
                        'content' => __('You can write any text here'),
                    ],
                ],
            ],
            'children' => [],
        ];
    }

    protected function getFirstFieldConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Example Text Field'),
                        'formElement' => Field::NAME,
                        'componentType' => Input::NAME,
                        'dataScope' => static::FIELD_NAME_1,
                        'dataType' => Number::NAME,
                        'sortOrder' => $sortOrder,
                    ],
                ],
            ],
        ];
    }

    protected function getSecondFieldConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Product Options Select'),
                        'componentType' => Field::NAME,
                        'formElement' => Select::NAME,
                        'dataScope' => static::FIELD_NAME_2,
                        'dataType' => Text::NAME,
                        'sortOrder' => $sortOrder,
                        'options' => $this->_getOptions(),
                        'visible' => true,
                        'disabled' => false,
                    ],
                ],
            ],
        ];
    }

    protected function getThirdFieldConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Product Options Multiselect'),
                        'componentType' => Field::NAME,
                        'formElement' => MultiSelect::NAME,
                        'dataScope' => static::FIELD_NAME_3,
                        'dataType' => Text::NAME,
                        'sortOrder' => $sortOrder,
                        'options' => $this->_getOptions(),
                        'visible' => true,
                        'disabled' => false,
                    ],
                ],
            ],
        ];
    }

    /**
     * Get all product options as an option array:
     *      option_id => [
     *          label => string,
     *          value => option_id
     *      ]
     *
     * @return array
     */
    protected function _getOptions()
    {
        $options = [];
        $productOptions = $this->locator->getProduct()->getOptions() ?: [];

        /** @var \Magento\Catalog\Model\Product\Option $option */
        foreach ($productOptions as $index => $option) {
            $options[$index]['label'] = $option->getTitle();
            $options[$index]['value'] = $option->getId();
        }

        return $options;
    }

    protected function addCustomModalLink($sortOrder)
    {
        $this->meta = array_replace_recursive(
            $this->meta,
            [
                CustomOptions::GROUP_CUSTOM_OPTIONS_NAME => [
                    'children' => [
                        CustomOptions::CONTAINER_HEADER_NAME => [
                            'children' => [
                                static::CUSTOM_MODAL_LINK => [
                                    'arguments' => [
                                        'data' => [
                                            'config' => [
                                                'title' => __('Open Custom Modal'),
                                                'formElement' => Container::NAME,
                                                'componentType' => Container::NAME,
                                                'component' => 'Magento_Ui/js/form/components/button',
                                                'actions' => [
                                                    [
                                                        'targetName' => 'ns=' . static::FORM_NAME . ', index='
                                                            . static::CUSTOM_MODAL_INDEX, // selector
                                                        'actionName' => 'openModal', // method name
                                                    ],
                                                ],
                                                'displayAsLink' => false,
                                                'sortOrder' => $sortOrder,
                                            ],
                                        ],
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ]
        );
    }
}
?>

When the product form is downloaded, Magento will collect all the modifiers and sort them in the chosen order. Then, the modifyData and modifyMeta methods will be called for each modifier. Our link (button) and a modal window will be added at the same moment. The only problem is to save this data because the default saving is used and duplicated in our window:

'buttons' => [
    [
        'text' => __('Save'),
        'class' => 'action-primary', // additional class
        'actions' => [
            [
                'targetName' => 'index = product_form', // Element selector
                'actionName' => 'save', // Save parent form (product)
            ],
        'closeModal', // method name
        ],
    ],
],

and if the selected fields are not in the product, they won’t be saved. You should use an observer or create your own model for saving and sending data using ajax to override the default saving.

The modal window will look like that:

A button to open the window:

You can change its look from the link to the default button by modifying the line from:

‘displayAsLink’ => true,

to:

‘displayAsLink’ => false,

Then, the display will change in the following way:

The data from our modal window will be send in the following way during the product saving:

in the product saving controller (vendor/magento/module-catalog/Controller/Adminhtml/Product/Save.php):

If you still got questions, feel free to ask them in the comments below.

I am a huge coffee fan. If I’m not drinking it, I’m likely to be busy with getting MageWorx projects done. Fond of reading sci-fi books (especially those about dwarfs, ogres and the post-apocalyptical world). My biggest dream is to find a huge chest of gold and buy my own uninhabited island. Happy husband. Proud father. Ah... and also I'm a certified Magento developer. ;)

2 COMMENTS

  1. This is great post and really helpful. I would like to ask one thing that I want to show the cart steps as a wizard in a modal window. Can i do that?

    Can i just extend the existing Checkout module or i have to implement a brand new module?

    • Yes, I guess you can try to do that at the theme (design) level. However, if you’ve got the necessity to do that, most likely, you’ll need an separate extension for that, as a lot of checkout processes will require the usage of Observer.

      Unfortunately, I haven’t had any experience with implementing such a task. But as soon as I do, I’ll share my thoughts and observations in a new blog post.

Leave a Reply to Sergey Uchuhlebov Cancel reply

Please enter your comment!
Please enter your name here