we will create a custom tab with custom table allow serialization grid and custom column get/post value like selected product, qty in grid.
Options 1
1. Add “htmlContent” in your form ui. You need to add tabs and template.
2. In block Lights\OrderCombine\Block\Adminhtml\Edit\Tab\Missing . Implement tabs interface.
– Function getProductsJson() will specify selected product show on grid. It return array ids with json_encode.
– Function getBlockGrid() will call grid show in your tabs.
3. Implement block include of grid data: \Lights\OrderCombine\Block\Adminhtml\Edit\Tab\MissingProductgrid.php . I will use product grid in here(You can use your custom table)
– Function getGridUrl() will use url callback when you have action on grid.
– Function _getSelectedProducts() will return an array key to filter and selected value. – Function _prepareColumns() will allow add your column in grid.
4. File templates:Lights/OrderCombine/view/adminhtml/templates/tabs/missingproducts.phtml
– Input will post data name= “ordercombine[missing_tab_data]”. When you select or add qty it will add to this field.
– data-form-part=”ordercombine_index_edit”: It allow your input can post to ui_form.
5. File js process grid and data when adjustment.
6. Finally, Create controller of this grid.

2. In block Lights\OrderCombine\Block\Adminhtml\Edit\Tab\Missing . Implement tabs interface.
namespace Lights\OrderCombine\Block\Adminhtml\Edit\Tab; use Magento\Ui\Component\Layout\Tabs\TabInterface; class Missing extends \Magento\Backend\Block\Template implements TabInterface { protected $blockGrid; /** * Core registry * * @var \Magento\Framework\Registry */ protected $_coreRegistry; /** * @var \Lights\OrderCombine\Model\CombineDetailsFactory */ protected $detailsFactory; /** * Orders constructor. * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry * @param \Lights\OrderCombine\Model\CombineDetailsFactory $detailsFactory * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, \Lights\OrderCombine\Model\CombineDetailsFactory $detailsFactory, array $data = [] ) { $this->_coreRegistry = $registry; $this->detailsFactory = $detailsFactory; parent::__construct($context, $data); } /** * @return \Magento\Framework\Phrase */ public function getTabLabel() { return __('Missing Products'); } /** * @return \Magento\Framework\Phrase */ public function getTabTitle() { return __('Missing Products'); } /** * @return bool */ public function canShowTab() { if ($this->canShow()) { return true; } return false; } /** * @return bool */ public function isHidden() { if ($this->canShow()) { return false; } return true; } /** * Tab class getter * * @return string */ public function getTabClass() { return ''; } /** * Return URL link to Tab content * * @return string */ public function getTabUrl() { return ''; } /** * Tab should be loaded trough Ajax call * * @return bool */ public function isAjaxLoaded() { return false; } /** * @return bool */ public function canShow() { $id = $this->getRequest()->getParam('id'); if($id != null && $id !='') return true; return false; } /** * Retrieve instance of grid block * * @return \Magento\Framework\View\Element\BlockInterface * @throws \Magento\Framework\Exception\LocalizedException */ public function getBlockGrid() { if (null === $this->blockGrid) { $this->blockGrid = $this->getLayout()->createBlock( \Lights\OrderCombine\Block\Adminhtml\Edit\Tab\MissingProductgrid::class, 'product.missing.grid' ); } return $this->blockGrid; } /** * @return string * @throws \Magento\Framework\Exception\LocalizedException */ public function getGridHtml() { return $this->getBlockGrid()->toHtml(); } /** * product ids and qtys * @return string */ public function getProductsJson() { $selected = []; $orderCombineDetail = $this->detailsFactory->create()->load($this->getRequest()->getParam('id')); if($orderCombineDetail){ if($orderCombineDetail->getData('missing_tab_data') != null){ $selected = $orderCombineDetail->getData('missing_tab_data'); return $selected; } } return \Zend_Json::encode($selected); } }– Model \Lights\OrderCombine\Model\CombineDetailsFactory to get your data selected on grid.
– Function getProductsJson() will specify selected product show on grid. It return array ids with json_encode.
– Function getBlockGrid() will call grid show in your tabs.
3. Implement block include of grid data: \Lights\OrderCombine\Block\Adminhtml\Edit\Tab\MissingProductgrid.php . I will use product grid in here(You can use your custom table)
namespace Lights\OrderCombine\Block\Adminhtml\Edit\Tab; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Visibility; use Magento\Framework\App\ObjectManager; class MissingProductgrid extends \Magento\Backend\Block\Widget\Grid\Extended { protected $blockGrid; /** * Core registry * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; /** * @var \Magento\Catalog\Model\ProductFactory */ protected $_productFactory; /** * @var Status */ private $status; /** * @var Visibility */ private $visibility; /** * @var \Lights\OrderCombine\Model\CombineDetailsFactory */ protected $combineDetailsFactory; /** * LiquorProductgrid constructor. * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper * @param \Magento\Catalog\Model\ProductFactory $productFactory * @param \Magento\Framework\Registry $coreRegistry * @param array $data * @param Visibility|null $visibility * @param Status|null $status * @param \Lights\OrderCombine\Model\CombineDetailsFactory $combineDetailsFactory */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, \Magento\Catalog\Model\ProductFactory $productFactory, \Magento\Framework\Registry $coreRegistry, array $data = [], Visibility $visibility = null, Status $status = null, \Lights\OrderCombine\Model\CombineDetailsFactory $combineDetailsFactory ) { $this->combineDetailsFactory = $combineDetailsFactory; $this->_productFactory = $productFactory; $this->_coreRegistry = $coreRegistry; $this->visibility = $visibility ?: ObjectManager::getInstance()->get(Visibility::class); $this->status = $status ?: ObjectManager::getInstance()->get(Status::class); parent::__construct($context, $backendHelper, $data); } /** * @return void */ protected function _construct() { parent::_construct(); $this->setId('catalog_missing_products'); $this->setDefaultSort('entity_id'); $this->setUseAjax(true); } /** * @param Column $column * @return $this|\Magento\Backend\Block\Widget\Grid\Extended * @throws \Magento\Framework\Exception\LocalizedException * @throws \Zend_Json_Exception */ protected function _addColumnFilterToCollection($column) { // Set custom filter for in category flag if ($column->getId() == 'in_category') { $productIds = $this->_getSelectedProducts(); if (empty($productIds)) { $productIds = 0; } if ($column->getFilter()->getValue()) { $this->getCollection()->addFieldToFilter('entity_id', ['in' => $productIds]); } elseif (!empty($productIds)) { $this->getCollection()->addFieldToFilter('entity_id', ['nin' => $productIds]); } } else { parent::_addColumnFilterToCollection($column); } return $this; } /** * @return \Magento\Backend\Block\Widget\Grid\Extended */ protected function _prepareCollection() { $collection = $this->_productFactory->create()->getCollection()->addAttributeToSelect( 'name' )->addAttributeToSelect( 'sku' )->addAttributeToSelect( 'visibility' )->addAttributeToSelect( 'status' )->addAttributeToSelect( 'price' ); $storeId = (int)$this->getRequest()->getParam('store', 0); if ($storeId > 0) { $collection->addStoreFilter($storeId); } $this->setCollection($collection); return parent::_prepareCollection(); } /** * @return \Magento\Backend\Block\Widget\Grid\Extended * @throws \Exception */ protected function _prepareColumns() { $this->addColumn( 'in_category', [ 'type' => 'checkbox', 'name' => 'in_category', 'values' => $this->_getSelectedProducts(), 'index' => 'entity_id', 'header_css_class' => 'col-select col-massaction', 'column_css_class' => 'col-select col-massaction' ] ); $this->addColumn( 'entity_id', [ 'header' => __('ID'), 'sortable' => true, 'index' => 'entity_id', 'header_css_class' => 'col-id', 'column_css_class' => 'col-id' ] ); $this->addColumn('name', ['header' => __('Name'), 'index' => 'name']); $this->addColumn('sku', ['header' => __('SKU'), 'index' => 'sku']); $this->addColumn( 'visibility', [ 'header' => __('Visibility'), 'index' => 'visibility', 'type' => 'options', 'options' => $this->visibility->getOptionArray(), 'header_css_class' => 'col-visibility', 'column_css_class' => 'col-visibility' ] ); $this->addColumn( 'status', [ 'header' => __('Status'), 'index' => 'status', 'type' => 'options', 'options' => $this->status->getOptionArray() ] ); $this->addColumn( 'price', [ 'header' => __('Price'), 'type' => 'currency', 'currency_code' => (string)$this->_scopeConfig->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ), 'index' => 'price' ] ); $this->addColumn( 'position', [ 'header' => __('Qty'), 'name' => 'position', 'type' => 'number', 'validate_class' => 'validate-number', 'index' => 'position', 'editable' => true, 'edit_only' => true, 'header_css_class' => 'col-position', 'column_css_class' => 'col-position' ] ); return parent::_prepareColumns(); } /** * url grid when processing filter * @return string */ public function getGridUrl() { return $this->getUrl('ordercombine/*/missing', ['_current' => true]); } /** * @return array|mixed * @throws \Zend_Json_Exception */ protected function _getSelectedProducts() { $selected = []; $orderCombineDetail = $this->combineDetailsFactory->create()->load($this->getRequest()->getParam('id')); if($orderCombineDetail){ if($orderCombineDetail->getData('missing_tab_data') != null){ $selected = \Zend_Json::decode($orderCombineDetail->getData('missing_tab_data')); } } return array_keys($selected); } }
– Function getGridUrl() will use url callback when you have action on grid.
– Function _getSelectedProducts() will return an array key to filter and selected value. – Function _prepareColumns() will allow add your column in grid.
4. File templates:Lights/OrderCombine/view/adminhtml/templates/tabs/missingproducts.phtml

5. File js process grid and data when adjustment.
/** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ /* global $, $H */ define([ 'mage/adminhtml/grid' ], function () { 'use strict'; return function (config) { var selectedProducts = config.selectedProducts, categoryProducts = $H(selectedProducts), gridJsObject = window[config.gridJsObjectName], tab_post_id = config.tab_post_id, tabIndex = 1000; $(tab_post_id).value = Object.toJSON(categoryProducts); /** * Register Category Product * * @param {Object} grid * @param {Object} element * @param {Boolean} checked */ function registerCategoryProduct(grid, element, checked) { if (checked) { if (element.positionElement) { element.positionElement.disabled = false; categoryProducts.set(element.value, element.positionElement.value); } } else { if (element.positionElement) { element.positionElement.disabled = true; } categoryProducts.unset(element.value); } $(tab_post_id).value = Object.toJSON(categoryProducts); grid.reloadParams = { 'selected_products[]': categoryProducts.keys() }; } /** * Click on product row * * @param {Object} grid * @param {String} event */ function categoryProductRowClick(grid, event) { var trElement = Event.findElement(event, 'tr'), isInput = Event.element(event).tagName === 'INPUT', checked = false, checkbox = null; if (trElement) { checkbox = Element.getElementsBySelector(trElement, 'input'); if (checkbox[0]) { checked = isInput ? checkbox[0].checked : !checkbox[0].checked; gridJsObject.setCheckboxChecked(checkbox[0], checked); } } } /** * Change product position * * @param {String} event */ function positionChange(event) { var element = Event.element(event); if (element && element.checkboxElement && element.checkboxElement.checked) { categoryProducts.set(element.checkboxElement.value, element.value); $(tab_post_id).value = Object.toJSON(categoryProducts); } } /** * Initialize category product row * * @param {Object} grid * @param {String} row */ function categoryProductRowInit(grid, row) { var checkbox = $(row).getElementsByClassName('checkbox')[0], position = $(row).getElementsByClassName('input-text')[0], positionValue = checkbox.value; if (checkbox && position) { if(selectedProducts[positionValue] != undefined){ position.value = selectedProducts[positionValue]; } checkbox.positionElement = position; position.checkboxElement = checkbox; position.disabled = !checkbox.checked; position.tabIndex = tabIndex++; Event.observe(position, 'keyup', positionChange); } } gridJsObject.rowClickCallback = categoryProductRowClick; gridJsObject.initRowCallback = categoryProductRowInit; gridJsObject.checkboxCheckCallback = registerCategoryProduct; if (gridJsObject.rows) { gridJsObject.rows.each(function (row) { categoryProductRowInit(gridJsObject, row); }); } }; });
6. Finally, Create controller of this grid.
namespace Lights\OrderCombine\Controller\Adminhtml\Index; use Magento\Backend\App\Action; class Missing extends Action { /** * @var \Magento\Framework\Controller\Result\RawFactory */ protected $resultRawFactory; /** * @var \Magento\Framework\View\LayoutFactory */ protected $layoutFactory; /** * @param Action\Context $context * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory */ public function __construct( Action\Context $context, \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, \Magento\Framework\View\LayoutFactory $layoutFactory ) { parent::__construct($context); $this->resultRawFactory = $resultRawFactory; $this->layoutFactory = $layoutFactory; } /** * Grid Action * Display list of products related to current category * * @return \Magento\Framework\Controller\Result\Raw */ public function execute() { /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); return $resultRaw->setContents( $this->layoutFactory->create()->createBlock( \Lights\OrderCombine\Block\Adminhtml\Edit\Tab\LiquorProductgrid::class, 'product.missing.grid' )->toHtml() ); } }Now, you can use grid sezialiler on your tabs and post data on your form.
Options 2
1. Add “htmlContent” in your form ui. You need to add tabs and template.
2. Create controller file:
3. Block file:

use Magento\Backend\App\Action\Context; use Magento\Catalog\Controller\Adminhtml\Product\Builder; use Magento\Framework\View\Result\LayoutFactory; use Magento\Catalog\Controller\Adminhtml\Product; class Productgrid extends Product { protected $resultLayoutFactory; public function __construct( Context$context, Builder $productBuilder, LayoutFactory$resultLayoutFactory ) { parent::__construct($context, $productBuilder); $this->resultLayoutFactory= $resultLayoutFactory; } public function execute() { $this->productBuilder->build($this->getRequest()); $resultLayout= $this->resultLayoutFactory->create(); $resultLayout->getLayout()->getBlock('company.module.edit.tab.productgrid') ->setProducts($this->getRequest()->getPost('products', null)); return $resultLayout; } }
3. Block file:
use Magento\Backend\Block\Template\Context; use Magento\Backend\Helper\Data; use Magento\Catalog\Model\ProductFactory; use Vendor\Extension\Model\OptionFactory; use Magento\Framework\Registry; class Missing extends \Magento\Backend\Block\Widget\Grid\Extended { protected $coreRegistry= null; protected $productFactory; public function __construct( Context $context, Data $backendHelper, ProductFactory$productFactory, OptionFactory$optionFactory, Registry $coreRegistry, array $data = [] ) { $this->productFactory= $productFactory; $this->optionFactory= $optionFactory; $this->coreRegistry= $coreRegistry; parent::__construct($context, $backendHelper, $data); } protected function _construct() { parent::_construct(); $this->setId('product_grid'); $this->setDefaultSort('entity_id'); $this->setUseAjax(true); } protected function _addColumnFilterToCollection($column) { if ($column->getId() == 'products') { $productIds= $this->_getSelectedProducts(); if (empty($productIds)) { $productIds= 0; } if ($column->getFilter()->getValue()) { $this->getCollection()->addFieldToFilter('entity_id', ['in' =>$productIds]); } else { if ($productIds) { $this->getCollection()->addFieldToFilter('entity_id', ['nin' =>$productIds]); } } } else { parent::_addColumnFilterToCollection($column); } return $this; } protected function _prepareCollection() { $collection = $this->productFactory->create()->getCollection()->addAttributeToSelect( '*' ); $this->setCollection($collection); return parent::_prepareCollection(); } protected function _prepareColumns() { $this->addColumn( 'products', [ 'type' =>'checkbox', 'field_name' =>'products_id[]', 'required' =>true, 'values' =>$this->_getSelectedProducts(), 'align' =>'center', 'index' =>'entity_id', 'header_css_class' =>'col-select', 'column_css_class' =>'col-select' ] ); $this->addColumn( 'name', [ 'header' => __('Name'), 'index' =>'name', 'header_css_class' =>'col-name', 'column_css_class' =>'col-name' ] ); $this->addColumn( 'sku', [ 'header' => __('SKU'), 'index' =>'sku', 'header_css_class' =>'col-sku', 'column_css_class' =>'col-sku' ] ); return parent::_prepareColumns(); } public function getGridUrl() { return $this->_getData( 'grid_url' ) ? $this->_getData( 'grid_url' ) : $this->getUrl( '*/*/productgrid', ['_current' =>true] ); } protected function _getSelectedProducts() { $products = array_keys($this->getSelectedProducts()); return $products; } public function getSelectedProducts() { $tm_id= $this->getRequest()->getParam('id'); if (!isset($tm_id)) { $tm_id= 0; } $collection = $this->optionFactory->create()->load($tm_id); $data = $collection->getProducts(); $products = explode(',', $data); $proIds= array(); foreach($products as $product) { $proIds[$product] = array('id' =>$product); } return $proIds; } }