How to Add Custom Field to Options in Advanced Product Options Extension

How to Add Custom Field to Advanced Product Options | Mageworx Blog

Reading Time: 7 minutes
Change the previously created app/code/VendorName/OptionGtin/etc/adminhtml/di.xml by adding the plugin there:
<?php
namespace VendorNameOptionGtinObserver; use MagentoFrameworkEventObserver;
use MagentoFrameworkEventObserverInterface;
use MagentoCatalogModelProductRepository as ProductRepository;
use MageWorxOptionBaseHelperData as BaseHelper; class AddGtinToOrder implements ObserverInterface
{ /** * @var BaseHelper */ protected $baseHelper; protected $productRepository; /** * AddGtinToOrder constructor. * @param BaseHelper $baseHelper * @param ProductRepository $productRepository */ public function __construct( BaseHelper $baseHelper, ProductRepository $productRepository ) { $this->baseHelper = $baseHelper; $this->productRepository = $productRepository; } /** * Add product to quote action * Processing: gtin * * @param Observer $observer * @return $this */ public function execute(Observer $observer) { $quoteItems = $observer->getQuote()->getAllItems(); /** @var MagentoQuoteModelQuoteItem $quoteItem */ foreach ($quoteItems as $quoteItem) { $buyRequest = $quoteItem->getBuyRequest(); $optionIds = array_keys($buyRequest->getOptions()); $productOptions = $this->productRepository->getById($buyRequest->getProduct())->getOptions(); $quoteItemOptionGtins = []; $optionGtins = []; foreach ($productOptions as $option) { if ($option->getGtin()) { $quoteItemOptionGtins[$option->getOptionId()] = $option->getGtin(); } } foreach ($optionIds as $optionId) { $optionGtins[$optionId] = $optionId; } $optionGtins = array_intersect_key($quoteItemOptionGtins, $optionGtins); $infoBuyRequest = $quoteItem->getOptionByCode('info_buyRequest'); $buyRequest->setData('gtin', $optionGtins); $infoBuyRequest->setValue($this->baseHelper->encodeBuyRequestValue($buyRequest->getData())); $quoteItem->addOption($infoBuyRequest); } }
}

and see what we’ve got. But first, add some styles to our new attribute and make it look nice on the front-end.

Step #1. Create New Module

The code that allows adding our field to the app/code/VendorName/OptionGtin/Ui/DataProvider/Product/Form/Modifier/OptionGtin.php form is provided below:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="VendorName_OptionGtin" setup_version="1.0.0"> <sequence> <module name="Magento_Catalog"/> <module name="MageWorx_OptionBase"/> </sequence> </module>
</config>

<?php
MagentoFrameworkComponentComponentRegistrar::register( MagentoFrameworkComponentComponentRegistrar::MODULE, 'VendorName_OptionGtin', __DIR__
);

Step #2. Add Our New Field to Database

Let’s start with creating a model with our attribute:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <virtualType name="MageWorxOptionBaseUiDataProviderProductFormModifierPool"> <arguments> <argument name="modifiers" xsi:type="array"> <item name="mageworx-option-gtin" xsi:type="array"> <item name="class" xsi:type="string">VendorNameOptionGtinUiDataProviderProductFormModifierOptionGtin</item> <item name="sortOrder" xsi:type="number">72</item> </item> </argument> </arguments> </virtualType>
</config>

The Mageworx Advanced Product Options extension already has it all to display and work with attributes that our module adds. All we need to do is add the new attribute to the shared dataset.
We hope you find this article helpful. Should you have any difficulties or issues, feel free to let us know in the comments field below.
From this article, you will learn how to create a “GTIN” field for product custom options, show it on the product page front-end, and display it in the order.
We described in detail how to create a module in this article. Thus, let’s skip this part, and move straight to the code you’ll need to create an add-on:
Table of Contents
Book a Live Demo with Mageworx
Our new field has been added successfully:
{ "name": "mageworx/module-optiongtin", "description": "N/A", "require": { "magento/framework" : ">=100.1.0 <101", "magento/module-catalog": ">=101.0.0 <104" }, "type": "magento2-module", "version": "1.0.0", "license": [ "OSL-3.0", "AFL-3.0" ], "autoload": { "files": [ "registration.php" ], "psr-4": { "VendorName\OptionGtin\": "" } }
}

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <head> <css src="VendorName_OptionGtin::css/gtin.css"/> </head>
</page>

app/code/VendorName/OptionGtin/Model/Attriburte/Option/Gtin.php
Now, add the following file:
Without further ado, let’s proceed to the step-by-step guidelines.
So, let’s create it: app/code/VendorName/OptionGtin/view/frontend/web/js/catalog/product/features-gtin-mixin.js
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <!-- Data --> <type name="MageWorxOptionBaseModelProductOptionAttributes"> <arguments> <argument name="data" xsi:type="array"> <item name="gtin" xsi:type="object">VendorNameOptionGtinModelAttributeOptionGtin</item> </argument> </arguments> </type>
</config>

There are different means to display the required information in the ready template. For example, using “js”. We worked with “js” in this article. Let’s work with the templates themselves for a change, and try to rewrite them! 
app/code/VendorName/OptionGtin/Setup/InstallSchema.php
app/code/VendorName/OptionGtin/etc/adminhtml/di.xml
<?php
namespace VendorNameOptionGtinSetup; use MagentoFrameworkSetupInstallSchemaInterface;
use MagentoFrameworkSetupModuleContextInterface;
use MagentoFrameworkSetupSchemaSetupInterface;
use MagentoFrameworkDBDdlTable; class InstallSchema implements InstallSchemaInterface
{ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); $setup->getConnection()->addColumn( $setup->getTable('catalog_product_option'), 'gtin', [ 'type' => Table::TYPE_TEXT, 'nullable' => true, 'default' => null, 'comment' => 'Gtin (added by VendorName Option Gtin)', ] ); $setup->endSetup(); }
}

Step #3. Add Logic to Work with Backend

app/code/VendorName/OptionGtin/Plugin/AddDefaultColumn.php
As an order gets created, the sales_quote_address_collect_totals_before event gets triggered. We will use it to add our data to product options.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="sales_quote_address_collect_totals_before"> <observer name="mageworx_optiongtin_add_gtin_to_order" instance="VendorNameOptionGtinObserverAddGtinToOrder" /> </event>
</config>

app/code/VendorName/OptionGtin/view/adminhtml/layout/sales_order_view.xml
.option-gtin-text span { color: #6cc308; font-weight: 700;
}

define([ 'jquery', 'jquery/ui', 'mage/utils/wrapper'
], function ($, wrapper) { 'use strict'; return function (widget) { $.widget('mageworx.optionFeatures', widget, { /** * Triggers one time at first run (from base.js) * @param optionConfig * @param productConfig * @param base * @param self */ firstRun: function firstRun(optionConfig, productConfig, base, self) { //shareable link $('#mageworx_shareable_hint_icon').qtip({ content: { text: this.options.shareable_link_hint_text }, style: { classes: 'qtip-light' }, position: { target: false } }); $('#mageworx_shareable_link').on('click', function () { try { self.copyTextToClipboard(self.getShareableLink(base)); $('.mageworx-shareable-link-container').hide(); $('.mageworx-shareable-link-success-container').show(); setTimeout(function () { $('.mageworx-shareable-link-container').show(); $('.mageworx-shareable-link-success-container').hide(); }, 2000); } catch (error) { console.log('Something goes wrong. Unable to copy'); } }); setTimeout(function () { // Qty input $('.mageworx-option-qty').each(function () { $(this).on('change', function () { var optionInput = $("[data-selector='" + $(this).attr('data-parent-selector') + "']"); optionInput.trigger('change'); }); }); }, 500); // OptionValue Description & tooltip var extendedOptionsConfig = typeof base.options.extendedOptionsConfig != 'undefined' ? base.options.extendedOptionsConfig : {}; for (var option_id in optionConfig) { if (!optionConfig.hasOwnProperty(option_id)) { continue; } var description = extendedOptionsConfig[option_id]['description'], gtin = extendedOptionsConfig[option_id]['gtin'], gtinTitle = "Global Trade Item Number: ", $option = base.getOptionHtmlById(option_id); if (1 > $option.length) { console.log('Empty option container for option with id: ' + option_id); continue; } var $label = $option.find('label'); if(gtin != null && gtin.length > 0) { if ($label.length > 0) { $label .first() .after($('<p class="option-gtin-text"><span>' + gtinTitle + '</span>' + gtin + '</p>')); } else { $label = $option.find('span'); $label .first() .parent() .after($('<p class="option-gtin-text"><span>' + gtinTitle + '</span>' + gtin + '</p>')); } } if (this.options.option_description_enabled && !_.isEmpty(extendedOptionsConfig[option_id]['description'])) { if (this.options.option_description_mode == this.options.option_description_modes.tooltip) { var $element = $option.find('label span') .first(); if ($element.length == 0) { $element = $option.find('fieldset legend span') .first(); } $element.css('border-bottom', '1px dotted black'); $element.qtip({ content: { text: description }, style: { classes: 'qtip-light' }, position: { target: false } }); } else if (this.options.option_description_mode == this.options.option_description_modes.text) { if ($label.length > 0) { $label .first() .after($('<p class="option-description-text">' + description + '</p>')); } else { $label = $option.find('span'); $label .first() .parent() .after($('<p class="option-description-text">' + description + '</p>')); } } else { console.log('Unknown option mode'); } } if (this.options.value_description_enabled) { this._addValueDescription($option, optionConfig, extendedOptionsConfig); } } } }); return $.mageworx.optionFeatures; }; });

Create the following file, and define our mixin there: app/code/VendorName/OptionGtin/view/frontend/requirejs-config.js

How to Add Custom Field to Advanced Product Options | Mageworx Blog

Step #5. Add our Attribute Data to Order Details in Database

<?php
namespace VendorNameOptionGtinPlugin; class AddDefaultColumn
{ /** * @param MagentoSalesBlockAdminhtmlItemsColumnDefaultColumn $subject * @param $result * @return array */ public function afterGetOrderOptions(MagentoSalesBlockAdminhtmlItemsColumnDefaultColumn $subject, $result) { if ($options = $subject->getItem()->getProductOptions()) { if (isset($result)) { foreach ($result as &$option) { if (array_key_exists($option['option_id'], $options['info_buyRequest']['gtin'])) { $option['gtin'] = $options['info_buyRequest']['gtin'][$option['option_id']]; } } } } return $result; }
}

1.composer.json
<?php namespace VendorNameOptionGtinModelAttributeOption; use MageWorxOptionBaseModelProductOptionAbstractAttribute; class Gtin extends AbstractAttribute
{ /** * @return string */ public function getName() { return 'gtin'; } }

When a customer makes a purchase, an order gets created. Details about the added items get included in the sales_order_item table. This table has the product_options field that contains information about the selected parameters of an added item. That’s where we should add our new attribute’s data.
Let’s create the following file:
To display data of our new “GTIN” attribute, we’ve decided to use the firstrun() function from app/code/MageWorx/OptionFeatures/view/base/web/js/catalog/product/features.js. It already has all the required implementation that fits our example the best. To avoid overwriting the whole file, we will apply the “JavaScript mixins” mechanism, which will help us change the necessary function only.

How to Add Custom Field to Advanced Product Options | Mageworx Blog

Step #6. Display Data on Orders Page in Admin Panel

vendor/magento/module-sales/view/adminhtml/templates/items/column/name.phtml is responsible for displaying information about product options on the order page in the admin panel.
app/code/VendorName/OptionGtin/etc/di.xml
Let’s rewrite it to display our “GTIN”. For that, we need to rewrite the “column_name” block, or rather its template. Create a layout and a template: 
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="column_name"> <action method="setTemplate"> <argument name="template" xsi:type="string">VendorName_OptionGtin::items/column/name.phtml</argument> </action> </referenceBlock> </body>
</page>

Let’s define the event by creating: app/code/VendorName/OptionGtin/etc/events.xml
To check that everything has been performed correctly, create an order with the product, which options have “GTIN” data. You can check that the data has been added in the Database sales_order_item table -> product_options field:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <virtualType name="MageWorxOptionBaseUiDataProviderProductFormModifierPool"> <arguments> <argument name="modifiers" xsi:type="array"> <item name="mageworx-option-gtin" xsi:type="array"> <item name="class" xsi:type="string">VendorNameOptionGtinUiDataProviderProductFormModifierOptionGtin</item> <item name="sortOrder" xsi:type="number">72</item> </item> </argument> </arguments> </virtualType> <!-- Plugins--> <type name="MagentoSalesBlockAdminhtmlItemsColumnDefaultColumn"> <plugin name="mageworx-optiongtin-add-default-column" type="VendorNameOptionGtinPluginAddDefaultColumn" sortOrder="5" disabled="false" /> </type>
</config>

<?php
namespace VendorNameOptionGtinUiDataProviderProductFormModifier; use MagentoCatalogUiDataProviderProductFormModifierAbstractModifier;
use MagentoCatalogUiDataProviderProductFormModifierCustomOptions;
use MagentoUiComponentFormElementInput;
use MagentoUiComponentFormElementDataTypeNumber;
use MagentoUiComponentFormField;
use MageWorxOptionBaseUiDataProviderProductFormModifierModifierInterface; class OptionGtin extends AbstractModifier implements ModifierInterface
{ /** * @var array */ protected $meta = []; /** * {@inheritdoc} */ public function modifyData(array $data) { return $data; } /** * {@inheritdoc} */ public function modifyMeta(array $meta) { $this->meta = $meta; $this->addFields(); return $this->meta; } /** * Adds fields to the meta-data */ protected function addFields() { $groupCustomOptionsName = CustomOptions::GROUP_CUSTOM_OPTIONS_NAME; $optionContainerName = CustomOptions::CONTAINER_OPTION; $commonOptionContainerName = CustomOptions::CONTAINER_COMMON_NAME; // Add fields to the option $optionFeaturesFields = $this->getOptionGtinFieldsConfig(); $this->meta[$groupCustomOptionsName]['children']['options']['children']['record']['children'] [$optionContainerName]['children'][$commonOptionContainerName]['children'] = array_replace_recursive( $this->meta[$groupCustomOptionsName]['children']['options']['children']['record']['children'] [$optionContainerName]['children'][$commonOptionContainerName]['children'], $optionFeaturesFields ); } /** * The custom option fields config * * @return array */ protected function getOptionGtinFieldsConfig() { $fields['gtin'] = $this->getGtinFieldConfig(); return $fields; } /** * Get gtin field config * * @return array */ protected function getGtinFieldConfig() { return [ 'arguments' => [ 'data' => [ 'config' => [ 'label' => __('GTIN'), 'componentType' => Field::NAME, 'formElement' => Input::NAME, 'dataType' => Number::NAME, 'dataScope' => 'gtin', 'sortOrder' => 65 ], ], ], ]; } /** * Check is current modifier for the product only * * @return bool */ public function isProductScopeOnly() { return false; } /** * Get sort order of modifier to load modifiers in the right order * * @return int */ public function getSortOrder() { return 32; }
}

Our MageWorx_OptionBase module already uses the getExtendedOptionsConfig()method. It collects and displays all the custom attributes in a block on the front-end. Open the app/code/MageWorx/OptionBase/Block/Product/View/Options.php class to see how it gets implemented.  
var config = { config: { mixins: { 'MageWorx_OptionFeatures/js/catalog/product/features': { 'VendorName_OptionGtin/js/catalog/product/features-gtin-mixin' : true } } }
};

Create a layout and define our new styles file there: app/code/VendorName/OptionGtin/view/frontend/layout/catalog_product_view.xml
<?php
/* @var $block MagentoSalesBlockAdminhtmlItemsColumnName */
?>
<?php if ($_item = $block->getItem()) : ?> <div id="order_item_<?= (int) $_item->getId() ?>_title" class="product-title"> <?= $block->escapeHtml($_item->getName()) ?> </div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU'))?>:</span> <?= /* @noEscape */ implode('<br />', $this->helper(MagentoCatalogHelperData::class)->splitSku($block->escapeHtml($block->getSku()))) ?> </div> <?php if ($block->getOrderOptions()) : ?> <dl class="item-options"> <?php foreach ($block->getOrderOptions() as $_option) : ?> <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> <?= /* @noEscape */ $block->getCustomizedOptionValue($_option) ?> <?php else : ?> <?php $optionValue = $block->getFormattedOption($_option['value']); ?> <?php $dots = 'dots' . uniqid(); ?> <?php $id = 'id' . uniqid(); ?> <?= $block->escapeHtml($optionValue['value'], ['a', 'br']) ?><?php if (isset($optionValue['remainder']) && $optionValue['remainder']) : ?> <span id="<?= /* @noEscape */ $dots; ?>"> ...</span> <span id="<?= /* @noEscape */ $id; ?>"><?= $block->escapeHtml($optionValue['remainder'], ['a']) ?></span> <script> require(['prototype'], function() { $('<?= /* @noEscape */ $id; ?>').hide(); $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $id; ?>').show();}); $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $dots; ?>').hide();}); $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $id; ?>').hide();}); $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $dots; ?>').show();}); }); </script> <?php endif; ?> <?php endif; ?> </dd> <dt> <?php if (isset($_option['gtin']) && $_option['gtin']) : ?> <span>GTIN:</span> <?php endif; ?> </dt> <dd> <?php if (isset($_option['gtin']) && $_option['gtin']) : ?> <span> <?= $block->escapeHtml($_option['gtin']) ?></span> <?php endif; ?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?>
<?php endif; ?>

If everything has been performed correctly, cleared, and compiled, then you will see the following result: