Magento – Magento2 module : Custom option type fields not displaying

backendmagento2module

I am working on a module to add a product custom option input type (Derivated from file type).

In the backend under catalog / product / custom options, my new option is present in the "input type" list but when I select it, the associated fields are not displayed (Price, price type, sky, compatible file extension, etc).

I do not manage to find what is going on…

Thank you for your help,

My files :

app/code/A/Custoptiontype/etc/module.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="A_Custoptiontype" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>

app/code/A/Custoptiontype/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\Block\Adminhtml\Product\Edit\Tab\Options\Option" type="A\Custoptiontype\Block\Adminhtml\Product\Edit\Tab\Options\Option"/>
    <preference for="Magento\Catalog\Model\Product\Option" type="A\Custoptiontype\Model\Catalog\Product\Option"/>
</config>

app/code/A/Custoptiontype/etc/product_options.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_options.xsd">
    <option name="xfile" label="X File" renderer="A\Custoptiontype\Block\Adminhtml\Product\Edit\Tab\Options\Type\Xfile">
        <inputType name="xfile" label="X File" />
    </option>
</config>

app/code/A/Custoptiontype/Block/Adminhtml/Product/Edit/Tab/Options/Option.php

<?php
namespace A\Custoptiontype\Block\Adminhtml\Product\Edit\Tab\Options;

class Option extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option
{
    protected $_template = 'A_Custoptiontype::catalog/product/edit/options/option.phtml';

    /**
    * Class constructor
    */  
  public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Config\Model\Config\Source\Yesno $configYesNo,
        \Magento\Catalog\Model\Config\Source\Product\Options\Type $optionType,
        \Magento\Catalog\Model\Product $product,
        \Magento\Framework\Registry $registry,
        \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig,
        array $data = []
    )
    {
        parent::__construct(
            $context,
            $configYesNo,
            $optionType,
            $product,
            $registry,
            $productOptionConfig,
            $data
        );
    }

    /**
     * Retrieve html templates for different types of product custom options
     *
     * @return string
     */
    public function getTemplatesHtml()
    {
        $canEditPrice = $this->getCanEditPrice();
        $canReadPrice = $this->getCanReadPrice();

        $this->getChildBlock('xfile_option_type')
            ->setCanReadPrice($canReadPrice)
            ->setCanEditPrice($canEditPrice);
        $templates = parent::getTemplatesHtml() . "\n" .
            $this->getChildHtml('xfile_option_type');

        return $templates;    
    }          
}

app/code/A/Custoptiontype/Block/Adminhtml/Product/Edit/Tab/Options/Type/Xfile.php

<?php
namespace A\Custoptiontype\Block\Adminhtml\Product\Edit\Tab\Options\Type;

class Xfile extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\AbstractType
{
    /**
     * @var string
     */
    protected $_template = 'A_Custoptiontype::catalog/product/edit/options/type/xfile.phtml';


    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Catalog\Model\Config\Source\Product\Options\Price $optionPrice,
        array $data = []
    ) {
        $this->_optionPrice = $optionPrice;
        parent::__construct($context, $optionPrice, $data);
    }

}

app/code/A/Custoptiontype/view/adminhtml/templates/catalog/product/edit/options/option.phtml

<?php
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
// @codingStandardsIgnoreFile
?>
<?php /** @var $block \A\Custoptiontype\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ ?>
<?php echo $block->getTemplatesHtml() ?>
<script id="custom-option-base-template" type="text/x-magento-template">
    <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="option_<%- data.id %>">
        <div class="fieldset-wrapper-title">
            <strong
                class="admin__collapsible-title"
                data-toggle="collapse"
                data-target="#<%- data.id %>-content">
                <span id="option_<%- data.id %>_header_title"><%- data.title %></span>
            </strong>
            <div class="actions">
                <button type="button" title="<?php /* @escapeNotVerified */ echo __('Delete Custom Option'); ?>" class="action-delete" id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_delete">
                    <span><?php /* @escapeNotVerified */ echo __('Delete Custom Option'); ?></span>
                </button>
            </div>
            <div id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_move" data-role="draggable-handle" class="draggable-handle"
                 title="<?php /* @escapeNotVerified */ echo __('Sort Custom Options'); ?>"></div>
        </div>
        <div class="fieldset-wrapper-content in collapse" id="<%- data.id %>-content">
            <fieldset class="fieldset">
                <fieldset class="fieldset-alt" id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>">
                    <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_is_delete" name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][is_delete]" type="hidden" value=""/>
                    <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_previous_type" name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][previous_type]" type="hidden" value="<%- data.type %>"/>
                    <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_previous_group" name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][previous_group]" type="hidden" value=""/>
                    <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_id" name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][id]" type="hidden" value="<%- data.id %>"/>
                    <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_option_id" name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][option_id]" type="hidden" value="<%- data.option_id %>"/>
                    <input name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][sort_order]" type="hidden" value="<%- data.sort_order %>"/>

                    <div class="field field-option-title required">
                        <label class="label" for="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_title">
                            <?php /* @escapeNotVerified */ echo __('Option Title') ?>
                        </label>
                        <div class="control">
                            <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_title"
                                   name="<?php /* @escapeNotVerified */ echo $block->getFieldName() ?>[<%- data.id %>][title]"
                                   class="required-entry input-text"
                                   type="text"
                                   value="<%- data.title %>"
                                   data-store-label="<%- data.title %>"
                                   <% if (typeof data.scopeTitleDisabled != 'undefined' && data.scopeTitleDisabled != null) { %> disabled="disabled" <% } %>
                                   >
                            <%- data.checkboxScopeTitle %>
                        </div>
                    </div>

                    <div class="field field-option-input-type required">
                        <label class="label" for="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_title">
                            <?php /* @escapeNotVerified */ echo __('Input Type') ?>
                        </label>
                        <div class="control opt-type">
                            <?php echo $block->getTypeSelectHtml() ?>
                        </div>
                    </div>

                    <div class="field field-option-req">
                        <div class="control">
                            <input id="<?php /* @escapeNotVerified */ echo $block->getFieldId() ?>_<%- data.id %>_required" class="is-required" type="checkbox" checked="checked"/>
                            <label for="field-option-req">
                                <?php /* @escapeNotVerified */ echo __('Required')?>
                            </label>
                            <span style="display:none"><?php echo $block->getRequireSelectHtml() ?></span>
                        </div>
                    </div>
                </fieldset>
            </fieldset>
        </div>
    </div>
</script>

<div id="import-container" style="display: none;"></div>
<?php if (!$block->isReadonly()): ?>
<div><input type="hidden" name="affect_product_custom_options" value="1"/></div>
<?php endif; ?>
<script>
require([
    "jquery",
    "Magento_Catalog/js/custom-options"
], function(jQuery){

jQuery(function ($) {
    var fieldSet = $('[data-block=product-custom-options]');
    fieldSet.customOptions(<?php /* @escapeNotVerified */ echo $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode(
        [
            'fieldId' => $block->getFieldId(),
            'productGridUrl' => $block->getProductGridUrl(),
            'formKey' => $block->getFormKey(),
            'customOptionsUrl' => $block->getCustomOptionsUrl(),
            'isReadonly' => $block->isReadonly(),
            'itemCount' => $block->getItemCount(),
            'currentProductId' => $block->getCurrentProductId(),
        ]
    )?>);
    //adding data to templates
    <?php /** @var $_value \Magento\Framework\DataObject */ ?>
    <?php foreach ($block->getOptionValues() as $_value): ?>
        fieldSet.customOptions('addOption', <?php /* @escapeNotVerified */ echo $_value->toJson() ?>);
    <?php endforeach; ?>
});

});
</script>

app/code/A/Custoptiontype/view/adminhtml/templates/catalog/product/edit/options/type/xfile.phtml

<?php
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
// @codingStandardsIgnoreFile
?>
<?php /** @var $block \A\Custoptiontype\Block\Adminhtml\Product\Edit\Tab\Options\Type\Xfile */ ?>
<h1>TOTOTOTOTO</h1>
<script id="custom-option-xfile-type-template" type="text/x-magento-template">
    <div id="product_option_<%- data.option_id %>_type_<%- data.group %>" class="fieldset">
        <table class="data-table">
            <thead>
            <tr>
                <?php if ($block->getCanReadPrice() !== false) : ?>
                <th><?php /* @escapeNotVerified */ echo __('Price'); ?></th>
                <th><?php /* @escapeNotVerified */ echo __('Price Type'); ?></th>
                <?php endif; ?>
                <th><?php /* @escapeNotVerified */ echo __('SKU'); ?></th>
                <th><?php /* @escapeNotVerified */ echo __('Compatible File Extensions'); ?></th>
                <th><?php /* @escapeNotVerified */ echo __('Maximum Image Size'); ?></th>
            </tr>
            </thead>
            <tr>
                <?php if ($block->getCanReadPrice() !== false) : ?>
                <td class="opt-price">
                    <input name="product[options][<%- data.option_id %>][price]" data-store-label="<%- data.price %>"
                           class="input-text validate-zero-or-greater" type="text" value="<%- data.price %>"
                        <?php if ($block->getCanEditPrice() === false) : ?>
                           disabled="disabled"
                        <?php endif; ?>>
                </td>
                <td class="opt-price-type"><?php echo $block->getPriceTypeSelectHtml('data-attr="price-type"') ?><%- data.checkboxScopePrice %></td>
                <?php else : ?>
                <input name="product[options][<%- data.option_id %>][price]" type="hidden">
                <input id="product_option_<%- data.option_id %>_price_type" name="product[options][<%- data.option_id %>][price_type]" type="hidden">
                <?php endif; ?>
                <td>
                    <input name="product[options][<%- data.option_id %>][sku]" class="input-text" type="text" value="<%- data.sku %>">
                </td>
                <td>
                    <input name="product[options][<%- data.option_id %>][file_extension]" class="input-text" type="text" value="<%- data.file_extension %>">
                </td>
                <td class="col-file"><?php /* @escapeNotVerified */ echo __('%1 <span>x</span> %2 <span>px.</span>',
                    '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_x]" value="<%- data.image_size_x %>">',
                    '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_y]" value="<%- data.image_size_y %>">') ?>
                    <div class="note"><?php /* @escapeNotVerified */ echo __('Please leave blank if it is not an image.') ?></div>
                </td>
            </tr>
        </table>
    </div>
</script>

Best Answer

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