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:
1 2 3 4 5 6 7 |
<?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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<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:
1 2 3 4 5 6 7 8 |
<?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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<?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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
<?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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
'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.
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.