This is what I did to create a custom option type in Magento 2. For the new option type, I retained using the sku field but renamed it.
app/code/Testvendor/CustomOptions/etc/di.xml
<?xml version="1.0"?><config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions" type="Testvendor\CustomOptions\Ui\DataProvider\Catalog\Product\Form\Modifier\CustomOptions" /></config>
app/code/Testvendor/CustomOptions/etc/product_options.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_options.xsd">
<option name="testvendor" label="Test Vendor" renderer="Testvendor\CustomOptions\Block\Adminhtml\Catalog\Product\Edit\Tab\Options\Type\Testvendor">
<inputType name="testoption" label="Test Option" />
</option>
app/code/Testvendor/CustomOptions/Ui/DataProvider/Catalog/Product/Form/Modifier/CustomOptions.php
<?php
namespace Testvendor\CustomOptions\Ui\DataProvider\Catalog\Product\Form\Modifier;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Catalog\Model\ProductOptions\ConfigInterface;
use Magento\Catalog\Model\Config\Source\Product\Options\Price as ProductOptionsPrice;
use Magento\Framework\UrlInterface;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Ui\Component\Modal;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\DynamicRows;
use Magento\Ui\Component\Form\Fieldset;
use Magento\Ui\Component\Form\Field;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Element\Checkbox;
use Magento\Ui\Component\Form\Element\ActionDelete;
use Magento\Ui\Component\Form\Element\DataType\Text;
use Magento\Ui\Component\Form\Element\DataType\Number;
use Magento\Framework\Locale\CurrencyInterface;
class CustomOptions extends \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions
{
const FIELD_TESTOPTION_NAME = 'testoption';
protected function getStaticTypeContainerConfig($sortOrder)
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => Container::NAME,
'formElement' => Container::NAME,
'component' => 'Magento_Ui/js/form/components/group',
'breakLine' => false,
'showLabel' => false,
'additionalClasses' => 'admin__field-group-columns admin__control-group-equal',
'sortOrder' => $sortOrder,
],
],
],
'children' => [
static::FIELD_PRICE_NAME => $this->getPriceFieldConfig(10),
static::FIELD_PRICE_TYPE_NAME => $this->getPriceTypeFieldConfig(20),
static::FIELD_SKU_NAME => $this->getSkuFieldConfig(30),
static::FIELD_TESTOPTION_NAME => $this->getTestoptionFieldConfig(30),
static::FIELD_MAX_CHARACTERS_NAME => $this->getMaxCharactersFieldConfig(40),
static::FIELD_FILE_EXTENSION_NAME => $this->getFileExtensionFieldConfig(50),
static::FIELD_IMAGE_SIZE_X_NAME => $this->getImageSizeXFieldConfig(60),
static::FIELD_IMAGE_SIZE_Y_NAME => $this->getImageSizeYFieldConfig(70)
]
];
}
/**
* Get config for grid for "select" types
*
* @param int $sortOrder
* @return array
*/
protected function getSelectTypeGridConfig($sortOrder)
{
return [
'arguments' => [
'data' => [
'config' => [
'addButtonLabel' => __('Add Value'),
'componentType' => DynamicRows::NAME,
'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows',
'additionalClasses' => 'admin__field-wide',
'deleteProperty' => static::FIELD_IS_DELETE,
'deleteValue' => '1',
'renderDefaultRecord' => false,
'sortOrder' => $sortOrder,
],
],
],
'children' => [
'record' => [
'arguments' => [
'data' => [
'config' => [
'componentType' => Container::NAME,
'component' => 'Magento_Ui/js/dynamic-rows/record',
'positionProvider' => static::FIELD_SORT_ORDER_NAME,
'isTemplate' => true,
'is_collection' => true,
],
],
],
'children' => [
static::FIELD_TITLE_NAME => $this->getTitleFieldConfig(10),
static::FIELD_PRICE_NAME => $this->getPriceFieldConfig(20),
static::FIELD_PRICE_TYPE_NAME => $this->getPriceTypeFieldConfig(30, ['fit' => true]),
static::FIELD_SKU_NAME => $this->getSkuFieldConfig(40),
static::FIELD_SORT_ORDER_NAME => $this->getPositionFieldConfig(50),
static::FIELD_IS_DELETE => $this->getIsDeleteFieldConfig(60)
]
]
]
];
}
protected function getTypeFieldConfig($sortOrder)
{
return [
'arguments' => [
'data' => [
'config' => [
'label' => __('Option Type'),
'componentType' => Field::NAME,
'formElement' => Select::NAME,
'component' => 'Magento_Catalog/js/custom-options-type',
'elementTmpl' => 'ui/grid/filters/elements/ui-select',
'selectType' => 'optgroup',
'dataScope' => static::FIELD_TYPE_NAME,
'dataType' => Text::NAME,
'sortOrder' => $sortOrder,
'options' => $this->getProductOptionTypes(),
'disableLabel' => true,
'multiple' => false,
'selectedPlaceholders' => [
'defaultPlaceholder' => __('-- Please select --'),
],
'validation' => [
'required-entry' => true
],
'groupsConfig' => [
'text' => [
'values' => ['field', 'area'],
'indexes' => [
static::CONTAINER_TYPE_STATIC_NAME,
static::FIELD_PRICE_NAME,
static::FIELD_PRICE_TYPE_NAME,
static::FIELD_SKU_NAME,
static::FIELD_MAX_CHARACTERS_NAME
]
],
'Testvendor' => [
'values' => ['testoption'],
'indexes' => [
static::CONTAINER_TYPE_STATIC_NAME,
#static::FIELD_SKU_NAME,
static::FIELD_TESTOPTION_NAME,
]
],
'file' => [
'values' => ['file'],
'indexes' => [
static::CONTAINER_TYPE_STATIC_NAME,
static::FIELD_PRICE_NAME,
static::FIELD_PRICE_TYPE_NAME,
static::FIELD_SKU_NAME,
static::FIELD_FILE_EXTENSION_NAME,
static::FIELD_IMAGE_SIZE_X_NAME,
static::FIELD_IMAGE_SIZE_Y_NAME
]
],
'select' => [
'values' => ['drop_down', 'radio', 'checkbox', 'multiple'],
'indexes' => [
static::GRID_TYPE_SELECT_NAME
]
],
'data' => [
'values' => ['date', 'date_time', 'time'],
'indexes' => [
static::CONTAINER_TYPE_STATIC_NAME,
static::FIELD_PRICE_NAME,
static::FIELD_PRICE_TYPE_NAME,
static::FIELD_SKU_NAME
]
]
],
],
],
],
];
}
protected function getTestoptionFieldConfig($sortOrder)
{
return [
'arguments' => [
'data' => [
'config' => [
'label' => __('Test Option'),
'componentType' => Field::NAME,
'formElement' => Input::NAME,
'dataScope' => static::FIELD_SKU_NAME,
'dataType' => Text::NAME,
'sortOrder' => $sortOrder,
],
],
],
];
}
}
After this... I went in the bin directory and ran
./magento setup:upgrade
./magento setup:di:compile
./magento cache:flush
./magento cache:clean
Our team just recently went through this. In fact, one of our Ace member solved this. So this one is tricky because it nests so close to Magento's core. The best way to go about this is to start with living in vendor/magento/module-catalog
A few sub directories that come to mind would be:
- Block/Adminhtml/Category/Tab/Product.php
- Model/Product/Option/Value.php
- Model/Product/Option/Type/*
Now for the fun part!
In your modules etc, create a product_options.xml and add in something like this:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_options.xsd">
<option name="CategoryName" label="CategoryName" renderer="Namespace\Module\Block\Adminhtml\Product\Edit\Tab\Options\Type">
<inputType name="your_custom_type" label="WhatWillBeDisplayed" />
<inputType name="another_custom_type" label="WhatWillBeDisplayedForThisOne" />
</option>
</config>
This is the building blocks and mayhap all you'll need moving forward.
With that said, in our build, we required more advanced functionality. This functionality being a custom key to prevent collisions with default key values. We also needed to save them onto a separate table to prevent any undesirables if someone ever removes the module.
If you are looking to add more customization to your implementation, you'll want to take advantage of plugins and you'll want to add something like we have below:
<type name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions">
<plugin name="Somename_Product_CustomOptions_Modifier"
type="Namespace\Module\Ui\DataProvider\Product\Form\Modifier\CustomOptionsPlugin"
disabled="false" />
</type>
<type name="Magento\Catalog\Model\Product\Option">
<plugin name="Somename_Product_Option"
type="Namespace\Module\Controller\Adminhtml\Product\Option"
disabled="false" />
</type>
<type name="Magento\Catalog\Model\Product\Option\Value">
<plugin name="Somename_Product_Option_Value"
type="Namespace\Module\Controller\Adminhtml\Product\Option\Value"
disabled="false" />
</type>
- The first plugin is to take control over the meta data to inject our structure in preventing any undesired malformed data with Magento's default values.
- The second plugin is to capture the after save to move the keys around to store the data that Magento will consume as desired.
- Finally the third plugin is to extract our custom values (not to be confused with Magento's custom option select type values) and save them into our custom table.
Here is a sample of the First plugin CustomOptionPlugin.php. In some cases you may not need to make any changes here, but if you are looking to build custom option inputs you may have to inject them. You'll want the inner file to be something like the following:
<?php
namespace Namespace\Module\Ui\DataProvider\Product\Form\Modifier;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Field;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Element\ActionDelete;
use Magento\Ui\Component\Form\Element\DataType\Text;
use Magento\Ui\Component\Form\Element\DataType\Number;
class CustomOptionsPlugin
{
/**
* @var \Magento\Store\Model\StoreManagerInterface
*/
private $storeManager;
/**
* @var \Magento\Catalog\Model\Config\Source\Product\Options\Price
*/
private $productOptionsPrice;
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Catalog\Model\Config\Source\Product\Options\Price $productOptionsPrice,
\Magento\Ui\Component\Container $container,
\Namespace\Module\Model\ProductOptionTypeSupportFactory $productOptionTypeSupportFactory
)
{
$this->productOptionTypeSupportFactory = $productOptionTypeSupportFactory->create();
$this->storeManager = $storeManager;
$this->productOptionsPrice = $productOptionsPrice;
}
/*
* Method that modifies core Magento edit page for the product
* Mainly adds additional Custom Option Type
*/
public function afterModifyMeta($subject, $result)
{
// This is where you'll want to add all your values into the $results array
...
return $result;
}
/*
* Method adds data from Custom Table that holds support period dates
*/
public function afterModifyData($subject, $result)
{
/*
* This here also has a lot of of logic to parse through the $result values to add in the correct values as needed.
* I have faith in you.
*/
...
return $result;
}
}
Depending on your approach and end goal, you may need to use the second and third plugin as well. Feel free to ask any other questions you have and I will do my best to answer them.
Edit w/ addition info for the afterModifyMeta method listed above. Here you'll see the result parameter, this has the values needed to render the code. If you take advantage of var_dump within afterModifyMeta on results parameter using a Magento default option type, you can see how they structure it as well. Here is an example below:
$result['custom_options']['children']['options']['children']['record']['children']['container_option']['children']['values'] = [
'arguments' => [
'data' => [
'config' => [
'addButtonLabel' => __('Add Value'),
'componentType' => 'dynamicRows',
'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows',
'additionalClasses' => 'admin__field-wide',
'deleteProperty' => 'is_delete',
'deleteValue' => 1,
'renderDefaultRecord' => '',
'sortOrder' => '40'
]
]
],
'children' => [
'record' => [
'arguments' => [
'data' => [
'config' => [
'componentType' => Container::NAME,
'component' => 'Magento_Ui/js/dynamic-rows/record',
'positionProvider' => $subject::FIELD_SORT_ORDER_NAME,
'isTemplate' => true,
'is_collection' => true
]
]
],
'children' => [
$subject::FIELD_TITLE_NAME => [
'arguments' => [
'data' => [
'config' => [
'label' => __('Title'),
'componentType' => Field::NAME,
'formElement' => Input::NAME,
'dataScope' => 'title',
'dataType' => 'text',
'sortOrder' => '10',
'validation' => [
'required-entry' => '1'
]
]
]
]
],
$subject::FIELD_PRICE_NAME => [
'arguments' => [
'data' => [
'config' => [
'label' => __('Price'),
'componentType' => Field::NAME,
'formElement' => Input::NAME,
'dataScope' => 'price',
'dataType' => Number::NAME,
'addbefore' => $this->storeManager->getStore()->getBaseCurrency()->getCurrencySymbol(),
'sortOrder' => '20',
'validation' => [
'validate-zero-or-greater' => true
]
]
]
]
],
'paper' => [
'arguments' => [
'data' => [
'config' => [
'label' => __('Paper'),
... Rest of your code here ...
return $result;
Best Answer
For first method override use below code,
For second method,
For form prepareform override use below methods,
Remove var folder and try again.