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

0
20781
How to Add Custom Field to Advanced Product Options | Mageworx Blog
Reading Time: 7 minutes

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.

Without further ado, let’s proceed to the step-by-step guidelines.

Table of Contents

Step #1. Create New Module

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:

1.composer.json

{
    "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\\": ""
        }
    }
}

2.etc/module.xml

<?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>

3.registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'VendorName_OptionGtin',
    __DIR__
);

Step #2. Add Our New Field to Database

After we’ve built an empty module, it’s time to create the new “GTIN” field and add it to the database within the corresponding table. As we add a field for option values, we’ll need the “catalog_product_option” table.

Let’s create the following file:

app/code/VendorName/OptionGtin/Setup/InstallSchema.php

<?php
namespace VendorName\OptionGtin\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;

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

We’ll use the pool-modifier mechanism to add our new field.

Now, add the following file:

app/code/VendorName/OptionGtin/etc/adminhtml/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">
    <virtualType name="MageWorx\OptionBase\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="mageworx-option-gtin" xsi:type="array">
                    <item name="class" xsi:type="string">VendorName\OptionGtin\Ui\DataProvider\Product\Form\Modifier\OptionGtin</item>
                    <item name="sortOrder" xsi:type="number">72</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

Here, let’s add our modifier to the shared pool of the Advanced Product Options extension―”MageWorx\OptionBase\Ui\DataProvider\Product\Form\Modifier\Pool”. “VendorName\OptionGtin\Ui\DataProvider\Product\Form\Modifier\OptionGtin” is our class modifier. 

The code that allows adding our field to the app/code/VendorName/OptionGtin/Ui/DataProvider/Product/Form/Modifier/OptionGtin.php form is provided below:

<?php
namespace VendorName\OptionGtin\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\DataType\Number;
use Magento\Ui\Component\Form\Field;
use MageWorx\OptionBase\Ui\DataProvider\Product\Form\Modifier\ModifierInterface;

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;
    }
}

Now, let’s try to install the extension and check that everything gets displayed:

  • php bin/magento module:enable VendorName_OptionGtin
  • php bin/magento setup:upgrade
  • php bin/magento cache:flush

Our new field has been added successfully:

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

Step #4. Display our Field on Product Page Front-End

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.

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.  

Let’s start with creating a model with our attribute:

app/code/VendorName/OptionGtin/Model/Attriburte/Option/Gtin.php

<?php

namespace VendorName\OptionGtin\Model\Attribute\Option;


use MageWorx\OptionBase\Model\Product\Option\AbstractAttribute;

class Gtin extends AbstractAttribute
{
    /**
     * @return string
     */
    public function getName()
    {
        return 'gtin';
    }

}

Now, use the “dependency injection” mechanism and add our attribute to the shared attributes dataset of the Advanced Product Options extension.

app/code/VendorName/OptionGtin/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">
    <!-- Data -->
    <type name="MageWorx\OptionBase\Model\Product\Option\Attributes">
        <arguments>
            <argument name="data" xsi:type="array">
                <item name="gtin" xsi:type="object">VendorName\OptionGtin\Model\Attribute\Option\Gtin</item>
            </argument>
        </arguments>
    </type>
</config>

In other words, by opening the MageWorx\OptionBase\Model\Product\Option\Attributes class, you will see that it simply collects all attribute objects to the shared dataset.

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.

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

var config = {
    config: {
        mixins: {
            'MageWorx_OptionFeatures/js/catalog/product/features': {
                'VendorName_OptionGtin/js/catalog/product/features-gtin-mixin' : true
            }
        }
    }
};

Here, MageWorx_OptionFeatures/js/catalog/product/features is the root to our file, which method we need to rewrite. VendorName_OptionGtin/js/catalog/product/features-gtin-mixin is the file, where we will rewrite the method.

So, let’s create it: app/code/VendorName/OptionGtin/view/frontend/web/js/catalog/product/features-gtin-mixin.js

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);

                // Option\Value 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;
    };

});

Generally, we can run the following commands now:

  • php bin/magento cache:flush
  • php bin/magento setup:static-content:deploy (only for production mode)

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

Create a layout and define our new styles file there: app/code/VendorName/OptionGtin/view/frontend/layout/catalog_product_view.xml

<?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>

It’s time to create a styles file: app/code/VendorName/OptionGtin/view/frontend/web/css/gtin.css

.option-gtin-text span {
    color: #6cc308;
    font-weight: 700;
}

Now, let’s run the previously described commands and check the results:

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

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

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.

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.

Let’s define the event by creating: app/code/VendorName/OptionGtin/etc/events.xml

<?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="VendorName\OptionGtin\Observer\AddGtinToOrder"
        />
    </event>
</config>

Then, create our observer: app/code/VendorName/OptionGtin/Observer/AddGtinToOrder.php

<?php
namespace VendorName\OptionGtin\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Catalog\Model\ProductRepository as ProductRepository;
use MageWorx\OptionBase\Helper\Data 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 \Magento\Quote\Model\Quote\Item $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);

        }
    }
}

Here, with the help of the observer, we get the list of all items in the order and add the data of our “GTIN” attribute to the so-called $infoBuyRequest.

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:

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

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

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! 

Change the previously created app/code/VendorName/OptionGtin/etc/adminhtml/di.xml by adding the plugin there:

<?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="MageWorx\OptionBase\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="mageworx-option-gtin" xsi:type="array">
                    <item name="class" xsi:type="string">VendorName\OptionGtin\Ui\DataProvider\Product\Form\Modifier\OptionGtin</item>
                    <item name="sortOrder" xsi:type="number">72</item>
                </item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Plugins-->
    <type name="Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn">
        <plugin name="mageworx-optiongtin-add-default-column"
                type="VendorName\OptionGtin\Plugin\AddDefaultColumn"
                sortOrder="5"
                disabled="false"
        />
    </type>
</config>

Create the plugin itself:

app/code/VendorName/OptionGtin/Plugin/AddDefaultColumn.php

<?php
namespace VendorName\OptionGtin\Plugin;

class AddDefaultColumn
{

    /**
     * @param \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn $subject
     * @param $result
     * @return array
     */
    public function afterGetOrderOptions(\Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn $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;
    }
}

This plugin adds information about our new attribute for order options, for which these data exist.

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.

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: 

app/code/VendorName/OptionGtin/view/adminhtml/layout/sales_order_view.xml

<?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>

app/code/VendorName/OptionGtin/view/adminhtml/templates/items/column/name.phtml

<?php
/* @var $block \Magento\Sales\Block\Adminhtml\Items\Column\Name */
?>
<?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(\Magento\Catalog\Helper\Data::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:

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

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.

Book a Live Demo with Mageworx

LEAVE A REPLY

Please enter your comment!
Please enter your name here