Custom Field Type

In certain cases you will need to create a custom field type to achieve specific input abilities.

Let's create grid input field type for custom set of items.

Each custom item in this case has following properties: id number, quantity and description.

custom field frontend

Click here for online demo.

We will be creating new extension called Mageme_Items.

File structure

app/code/Mageme/Items
├── Block
│ └── Adminhtml
│ └── Renderer
│ └── Items.php
├── etc
│ ├── events.xml
│ └── module.xml
├── Observer
│ ├── Adminhtml
│ │ └── Result
│ │ └── Grid
│ │ └── ColumnsConfig.php
│ ├── Result
│ │ └── Value.php
│ ├── Renderer.php
│ ├── ResultSave.php
│ └── Types.php
├── view
│ └── frontend
│ └── templates
│ └── webforms
│ └── fields
│ └── items.phtml
├── composer.json
└── registration.php

composer.json

This file holds information for composer script

{
"name": "mageme/items",
"require": {
"php": "~5.5.0|~5.6.0|~7.0.0",
"vladimirpopov/webforms": "2.8.4",
"magento/magento-composer-installer": "*"
},
"type": "magento2-module",
"version": "0.1.0",
"license": [
],
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Mageme\\Items\\": ""
}
}
}

registration.php

This file registers module in Magento system. Without this file the module is not visible.

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

etc/module.xml

In this file we holds basic description of our module. We need to set the sequence so the module is loaded after the main WebForms extension.

<?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="Mageme_Items" setup_version="0.1.0">
<sequence>
<module name="VladimirPopov_WebForms"/>
</sequence>
</module>
</config>

etc/events.xml

This file registers our event handler function.

Following events are being used:

webforms_fields_types - this event is used to register new field type in the select list

webforms_fields_tohtml_html - this is used to insert our custom field html frontend template

webforms_result_save - this event is used to process raw field submission data and can be used for extra actions

webforms_block_adminhtml_results_grid_prepare_columns_config - this event is used to render field value in result grid

webforms_results_tohtml_value - this event is used to render field value in result grid

<?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="webforms_fields_types">
<observer name="mageme_items_types" instance="Mageme\Items\Observer\Types" shared="true"/>
</event>
<event name="webforms_fields_tohtml_html">
<observer name="mageme_items_renderer" instance="Mageme\Items\Observer\Renderer" shared="true"/>
</event>
<event name="webforms_result_save">
<observer name="mageme_items_resultsave" instance="Mageme\Items\Observer\ResultSave" shared="true"/>
</event>
<event name="webforms_block_adminhtml_results_grid_prepare_columns_config">
<observer name="mageme_items_adminhtml_results_grid_prepare_columns_config" instance="Mageme\Items\Observer\Adminhtml\Result\Grid\ColumnsConfig"/>
</event>
<event name="webforms_results_tohtml_value">
<observer name="mageme_results_tohtml_value" instance="Mageme\Items\Observer\Result\Value"/>
</event>
</config>

Block/Adminhtml/Renderer/Items.php

Here we define our backend grid column renderer. We are decoding data from JSON format and displaying it line by line as text

custom field frontend

<?php
namespace Mageme\Items\Block\Adminhtml\Renderer;
class Items extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer
{
public function render(\Magento\Framework\DataObject $row)
{
$json_data = $row->getData($this->getColumn()->getIndex());
$items = json_decode($json_data, true);
$str = array();
for ($i = 0; $i < count($items); $i++) {
$str[] = "#".$items[$i]["number"] . " | Quantity: " . $items[$i]["quantity"] . " | Description: " . $items[$i]["description"];
}
return "<nobr>".implode("<br>", $str)."</nobr>";
}
}

Observer/Adminhtml/Result/Grid/ColumnsConfig.php

Here we register our backend grid column renderer.

<?php
namespace Mageme\Items\Observer\Adminhtml\Result\Grid;
use Magento\Framework\Event\ObserverInterface;
class ColumnsConfig implements ObserverInterface
{
protected $_request;
protected $_layout;
public function __construct(
\Magento\Framework\View\Element\Context $context
)
{
$this->_layout = $context->getLayout();
$this->_request = $context->getRequest();
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
$field = $observer->getField();
$config = $observer->getConfig();
// add grid column renderer for our custom field
switch($field->getType()){
case 'custom/items':
$config->setRenderer('\Mageme\Items\Block\Adminhtml\Renderer\Items');
break;
}
}
}

Observer/Result/Value.php

This file formats field value for email notifications and result view pages.

<?php
namespace Mageme\Items\Observer\Result;
use Magento\Framework\Event\ObserverInterface;
class Value implements ObserverInterface
{
protected $_request;
protected $_layout;
public function __construct(
\Magento\Framework\View\Element\Context $context
)
{
$this->_layout = $context->getLayout();
$this->_request = $context->getRequest();
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
$value = $observer->getValue();
$field = $observer->getField();
switch($field->getType()){
case 'custom/items':
$json_data = $value->getValue();
$items = json_decode($json_data, true);
$str = array();
for ($i = 0; $i < count($items); $i++) {
$str[] = "#".$items[$i]["number"] . " | Quantity: " . $items[$i]["quantity"] . " | Description: " . $items[$i]["description"];
}
$value->setHtml("<nobr>".implode('<br>',$str)."</nobr>");
break;
}
}
}

Observer/Renderer.php

Here we insert the field frontend template.

<?php
namespace Mageme\Items\Observer;
use Magento\Framework\Event\ObserverInterface;
class Renderer implements ObserverInterface
{
protected $_request;
protected $_layout;
public function __construct(
\Magento\Framework\View\Element\Context $context
)
{
$this->_layout = $context->getLayout();
$this->_request = $context->getRequest();
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
$field = $observer->getField();
if ($field->getType() == 'custom/items') {
$field_name = "field[{$field->getId()}]";
$config = array(
'field_name' => $field_name,
'field_id' => "field" . $field->getUid() . $field->getId(),
'template' => 'Mageme_Items::webforms/fields/items.phtml'
);
$block = $this->_layout->createBlock('VladimirPopov\WebForms\Block\Field\AbstractField', '', ['data' => $config]);
$block->setField($field);
$html = $block->toHtml();
$observer->getHtmlObject()->setHtml($html);
}
}
}

Observer/ResultSave.php

This file is used for custom result processing.

<?php
namespace Mageme\Items\Observer;
use Magento\Framework\Event\ObserverInterface;
class ResultSave implements ObserverInterface
{
protected $_request;
protected $_layout;
protected $_fieldFactory;
public function __construct(
\Magento\Framework\View\Element\Context $context,
\VladimirPopov\WebForms\Model\FieldFactory $fieldFactory
) {
$this->_layout = $context->getLayout();
$this->_request = $context->getRequest();
$this->_fieldFactory = $fieldFactory;
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
$result = $observer->getResult();
$fields = $this->_fieldFactory->create()
->setStoreId($result->getStoreId())
->getCollection()
->addFilter('webform_id', $result->getWebformId());
foreach($fields as $field) {
if ($field->getType() == 'custom/items') {
if($result->getData('field',$field->getId())){
// place your custom code here
}
}
}
}
}

Observer/Types.php

This file is used to register our field type.

<?php
namespace Mageme\Items\Observer;
use Magento\Framework\Event\ObserverInterface;
class Types implements ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer)
{
$types = $observer->getTypes();
$types->setData('custom/items', __('Custom / Items'));
}
}

view/frontend/templates/webforms/fields/items.phtml

This file holds our frontend field template. It consist of CSS style part, JavaScript and html code.

<style>
.quote-input {
float: left;
margin-right: 5px;
line-height:1.6rem;
}
.quote-input input {width:100%;}
.quote-row {
clear: both;
float: left;
padding-bottom: 5px;
width:100%;
}
.quote-number {width:10%}
.quote-quantity {width:15%}
.quote-description { width:50%}
</style>
<script>
function removeItem(el) {
var quoteRow = el.up().up();
quoteRow.remove();
}
// duplicate item on button click
function cloneItem(el) {
var quoteRow = el.up().up();
var quoteContainer = quoteRow.up();
newItem = quoteRow.clone(true);
newItem.select('input').invoke('setValue','');
// change add button
var button = newItem.getElementsByClassName('quote-button')[0];
button.setAttribute('onclick', 'removeItem(this)');
button.childElements()[0].childElements()[0].update('X');
// append to the main container
quoteContainer.appendChild(newItem);
storeItemData(quoteContainer.getAttribute('data-id'));
}
// store JSON array in hidden field
function storeItemData(fieldId) {
var quoteArray = $$('div[data-id="' + fieldId + '"] .quote-row');
var quoteData = [];
for (var i = 0; i < quoteArray.length; i++) {
quoteData.push({
number: $$('input[data-name="' + fieldId + '_number"]')[i].value,
quantity: $$('input[data-name="' + fieldId + '_quantity"]')[i].value,
description: $$('input[data-name="' + fieldId + '_description"]')[i].value
});
}
$$('input[name="' + fieldId + '"]')[0].setValue(JSON.stringify(quoteData));
}
// initialize quote with the value
function initItem(fieldName, quoteData) {
var container = $$('div[data-id="' + fieldName + '"]')[0];
for (var i = 0; i < quoteData.length - 1; i++) {
cloneItem(container.getElementsByClassName('add-select-row')[0]);
}
// map values
var quoteArray = $$('div[data-id="' + fieldName + '"] .quote-row');
for (i = 0; i < quoteArray.length; i++) {
$$('input[data-name="' + fieldName + '_number"]')[i].setValue(quoteData[i].number);
$$('input[data-name="' + fieldName + '_quantity"]')[i].setValue(quoteData[i].quantity);
$$('input[data-name="' + fieldName + '_description"]')[i].setValue(quoteData[i].description);
}
storeItemData(fieldName);
}
</script>
<input type="hidden" name="<?php echo $this->getFieldName() ?>"/>
<div data-id="<?php echo $this->getFieldName() ?>" class="customfield-quote">
<div class="quote-row">
<div class="quote-input quote-number">
<div class="quote-item">
<input data-name="<?php echo $this->getFieldName() ?>_number"
type="number"
class="input-text <?php if ($this->getField()->getRequired()) { ?>required-entry<?php } ?> validate-number"
placeholder=""
onchange="storeItemData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="quote-input quote-quantity">
<div class="quote-item">
<input data-name="<?php echo $this->getFieldName() ?>_quantity"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getField()->getRequired()) { ?>required-entry<?php } ?> validate-number"
placeholder="Quantity"
onchange="storeItemData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="quote-input quote-description">
<div class="quote-item">
<input data-name="<?php echo $this->getFieldName() ?>_description"
type="text"
class="input-text <?php if ($this->getField()->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Description/Size/Color:"
onchange="storeItemData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="quote-input">
<button type="button" class="quote-button add-select-row" onclick="cloneItem(this)">
<span><span>Add Item</span></span>
</button>
</div>
</div>
</div>
<?php if ($this->getField()->getCustomerValue()) { ?>
<script>
// initialize field with previously submitted data
initItem('<?php echo $this->getFieldName() ?>', <?php echo $this->getField()->getCustomerValue()?>);
</script>
<?php }

Thats it! Feel free to modify this extension, experiment and add required post processing functionality to WebForms.

You can download sources here.