Skip to main content

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.

custom field frontend

File structure

app/code/MageMe/WebFormsCustomField
├── Block
│ └── Form
│ └── Element
│ └── Field
│ └── Type
│ └── CustomField.php
├── etc
│ ├── di.xml
│ ├── module.xml
│ └── webforms.xml
├── Model
│ └── Field
│ └── Type
│ └── CustomField.php
├── Ui
│ └── Field
│ └── Type
│ └── CustomField.php
├── view
│ └── frontend
│ └── templates
│ │ └── form
│ │ └── element
│ │ └── field
│ │ └── type
│ │ └── custom-field.phtml
│ └── web
│ └── js
│ │ └── custom-field.js
│ └── template
│ └── custom-field.html
└── registration.php

registration.php

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

<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'MageMe_WebFormsCustomField',
__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_WebFormsCustomField" setup_version="0.1.0">
<sequence>
<module name="MageMe_WebForms"/>
</sequence>
</module>
</config>

etc/webforms.xml

This is used to register new field type.

<?xml version="1.0"?>
<webforms xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:MageMe_WebForms:etc/webforms.xsd">
<field_types>
<type id="customField" order="1000">
<label>Custom Field</label>
<model>MageMe\WebFormsCustomField\Model\Field\Type\CustomField</model>
<category>Custom</category>
</type>
</field_types>
</webforms>

etc/di.xml

We define custom block class for our field model.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="MageMe\WebFormsCustomField\Model\Field\Type\CustomField">
<arguments>
<argument name="fieldUi" xsi:type="object">MageMe\WebFormsCustomField\Ui\Field\Type\CustomField</argument>
<argument name="fieldBlock" xsi:type="object">MageMe\WebFormsCustomField\Block\Form\Element\Field\Type\CustomField</argument>
</arguments>
</type>
</config>

Block/Form/Element/Field/Type/CustomField.php

Here we define our frontend block.

<?php
namespace MageMe\WebFormsCustomField\Block\Form\Element\Field\Type;

use MageMe\WebForms\Block\Form\Element\Field\AbstractField;

class CustomField extends AbstractField
{
/**
* Block's template
* @var string
*/
protected $_template = self::TEMPLATE_PATH . 'custom-field.phtml';

}

Model/Field/Type/CustomField.php

Each field type depends on the main model which should extend the AbstractField class.

<?php

namespace MageMe\WebFormsCustomField\Model\Field\Type;

use MageMe\WebForms\Model\Field\AbstractField;use function htmlentities;

class CustomField extends AbstractField
{
/**
* Get input columns
*
* @return array[]
*/
public function getColumns(): array
{
return [
['code' => 'part_number', 'name' => __('Part Number')],
['code' => 'quantity', 'name' => __('Quantity')],
['code' => 'budget', 'name' => __('Budget / Last Buy Price')]
];
}

/**
* Get number of empty rows
*
* @return int
*/
public function getEmptyRows(): int
{
return 3;
}

/**
* Result value html representation
*
* @param mixed $value
* @param array $options
* @return string
*/
public function getValueForResultHtml($value, array $options = [])
{
if ($value) {
$value = json_decode($value, true);
$html = '<table class="data-grid"><thead><tr>';
foreach ($this->getColumns() as $column) {
$html .= '<th class="data-grid-th">' . $column['name'] . '</th>';
};
$html .= '</tr></thead><tbody>';
foreach ($value as $val) {
$html .= '<tr>';
foreach ($this->getColumns() as $column) {
$html .= '<td>' . htmlentities($val[$column['code']]) . '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table>';

return $html;
}
return '';
}
}

Ui/Field/Type/CustomField.php

Ui classes hold admin ui configurations like result column and extra parameters inputs. Each field type depends on the main model which should extend the AbstractField class.

<?php
namespace MageMe\WebFormsCustomField\Ui\Field\Type;

use MageMe\WebForms\Api\Ui\FieldResultListingColumnInterface;
use MageMe\WebForms\Ui\Component\Common\Listing\Constants\BodyTmpl;
use MageMe\WebForms\Ui\Field\AbstractField;

class CustomField extends AbstractField implements FieldResultListingColumnInterface
{
/**
* Get Adminhtml column configuration
*
* @param int $sortOrder
* @return array
*/
public function getResultListingColumnConfig(int $sortOrder):array
{
$config = $this->getDefaultUIResultColumnConfig($sortOrder);
$config['bodyTmpl'] = BodyTmpl::HTML;
return $config;
}

}

frontend/temlpates/form/element/field/type/custom-field.phtml

The frontend template referenced in the CustomField block. The template is calling the frontend JavaScript component.

<?php
/** @var MageMe\WebFormsCustomField\Block\Form\Element\Field\Type\CustomField $block */
/** @var MageMe\WebFormsCustomField\Model\Field\Type\CustomField $field */
$field = $block->getField();
?>

<div data-bind="scope: '<?= $this->getFieldId() ?>'">
<!-- ko template: getTemplate() --><!-- /ko -->
</div>

<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"<?= $this->getFieldId() ?>": {
"required": <?= (int)$field->getIsRequired() ?>,
"fieldName":"<?= $block->getFieldName() ?>",
"removeLabel": "<?= __("Remove")?>",
"addItemLabel": "<?= __("Add Item")?>",
"columns": <?= json_encode($field->getColumns()) ?>,
"emptyRows": <?= (int)$field->getEmptyRows() ?>,
"component": "MageMe_WebFormsCustomField/js/custom-field",
"template": "MageMe_WebFormsCustomField/custom-field"
}
}
}
}
}
</script>

frontend/web/js/custom-field.js

The frontend JavaScript component which was referenced in the custom-field.phtml template.

define(['uiComponent', 'ko'], function (Component, ko) {

return Component.extend({
initialize: function () {
this._super();
var self = this;

function DataRecord(data, columns) {
var self = this;
for (var i = 0; i < columns.length; i++) {
self[columns[i].code] = ko.observable(data[columns[i].code]);
}
}

self.data = ko.observableArray([]);

self.fieldClass = ko.computed(function () {
if (self.required) return 'required-entry';
return '';
}, this);

self.addItem = function () {
var record = {};
var columns = self.columns;
for (var i = 0; i < columns.length; i++) {
record[columns[i].code] = '';
}
self.data.push(new DataRecord(record, columns));
};

self.removeItem = function (item) {
self.data.remove(item)
};

for (var i = 0; i < self.emptyRows; i++) {
self.addItem();
}

self.fieldValue = ko.computed(function () {
if (self.data().length)
return ko.toJSON(self.data);
});
}
});
});

frontend/web/template/custom-field.html

The template for the JavaScript component which was referenced in the custom-field.phtml template.

<table>
<thead>
<tr data-bind="foreach: columns">
<th><span data-bind="text: name"></span></th>
</tr>
</thead>

<tbody data-bind="foreach: data">
<tr>
<!-- ko foreach: $parent.columns -->
<td>
<input type="text" data-bind="value: $parent[code]"/>
</td>
<!-- /ko -->
<td>
<button type="button" data-bind="click: $parent.removeItem, text: $parent.removeLabel"></button>
</td>
</tr>
</tbody>

<tfoot>
<tr>
<td colspan="*">
<button type="button" class="button" data-bind="click: addItem, text: addItemLabel"></button>
</td>
</tr>
</tfoot>
</table>

<input type="hidden" data-bind="attr: {name: fieldName}, css: fieldClass, value: fieldValue()"/>

After the module is ready, please run following console commands:

php bin/magento module:enable MageMe_WebFormsCustomField
php bin/magento setup:upgrade

Feel free to modify this extension, experiment and add required customization to WebForms.

You can download sources here.