Custom Field Types

Sometimes default selection of field types bundled with WebForms is not enough for specific tasks and you will need to code your own field type.

Let's create new field type called Box.

Box consist of Width, Height and Depth properties. Customer should be able to add multiple boxes to the submission.

custom field

ONLINE DEMO: CLICK HERE.

We will be creating new extension called CustomField.

CustomField file structure

It contains 8 files in total.

file structure

app / etc / modules / Mageme_CustomField.xml

This file registeres and enables extension in Magento

<?xml version="1.0"?>
<config>
<modules>
<Mageme_CustomField>
<active>true</active>
<codePool>local</codePool>
</Mageme_CustomField >
</modules>
</config>

app / code / local / Mageme / CustomField

This folder contains main extension files.

etc / config.xml

In this file we are registering model for our extension and hook it to WebForms events:

webforms_fields_types - register new field type in the field type list.

webforms_fields_tohtml_html - print field frontend html code.

webforms_block_adminhtml_results_grid_prepare_columns_config - add field renderer to backend result grid column.

webforms_block_adminhtml_results_edit_form_prepare_layout_field - print field backend html code.

webforms_results_tohtml_value - print field submission value in html format.

<?xml version="1.0"?>
<config>
<modules>
<Mageme_CustomField>
<version>0.1.0</version>
</Mageme_CustomField>
</modules>
<global>
<models>
<customfield>
<class>Mageme_CustomField_Model</class>
<resourceModel>mageme_customfield_resource</resourceModel>
</customfield>
</models>
<helpers>
<customfield>
<class>Mageme_CustomField_Helper</class>
</customfield>
</helpers>
<blocks>
<customfield>
<class>Mageme_CustomField_Block</class>
</customfield>
</blocks>
<events>
<webforms_fields_types>
<observers>
<customfield>
<class>Mageme_CustomField_Model_Observer</class>
<method>webformsFieldsTypes</method>
<type>singleton</type>
</customfield>
</observers>
</webforms_fields_types>
<webforms_fields_tohtml_html>
<observers>
<customfield>
<class>Mageme_CustomField_Model_Observer</class>
<method>webformsFieldsTohtmlHtml</method>
<type>singleton</type>
</customfield>
</observers>
</webforms_fields_tohtml_html>
<webforms_block_adminhtml_results_grid_prepare_columns_config>
<observers>
<customfield>
<class>Mageme_CustomField_Model_Observer</class>
<method>webformsBlockAdminhtmlResultsGridPrepareColumnsConfig</method>
<type>singleton</type>
</customfield>
</observers>
</webforms_block_adminhtml_results_grid_prepare_columns_config>
<webforms_block_adminhtml_results_edit_form_prepare_layout_field>
<observers>
<customfield>
<class>Mageme_CustomField_Model_Observer</class>
<method>webformsBlockAdminhtmlResultsEditFormPrepareLayoutField</method>
<type>singleton</type>
</customfield>
</observers>
</webforms_block_adminhtml_results_edit_form_prepare_layout_field>
<webforms_results_tohtml_value>
<observers>
<customfield>
<class>Mageme_CustomField_Model_Observer</class>
<method>webformsResultsTohtmlValue</method>
<type>singleton</type>
</customfield>
</observers>
</webforms_results_tohtml_value>
</events>
</global>
</config>

Model / Observer.php

Observer registeres new field type and adds field presentation.

<?php
class Mageme_CustomField_Model_Observer
{
public function webformsFieldsTypes(Varien_Object $observer)
{
$types = $observer->getTypes();
// add new custom field Box
$types->setData('custom/box',Mage::helper('customfield')->__('Custom / Box'));
}
public function webformsFieldsTohtmlHtml(Varien_Object $observer)
{
$field = $observer->getField();
// create frontend html code for the Box
if($field->getType() == 'custom/box'){
$field_name = "field[{$field->getId()}]";
$config = array(
'field_name' => $field_name,
'field' => $field,
'template' => 'customfield/box.phtml'
);
$html = Mage::app()->getLayout()->createBlock('core/template', $field_name, $config)->toHtml();
$observer->getHtmlObject()->setHtml($html);
}
}
public function webformsBlockAdminhtmlResultsGridPrepareColumnsConfig(Varien_Object $observer)
{
$field = $observer->getField();
$config = $observer->getConfig();
// add grid column renderer for our custom field
switch($field->getType()){
case 'custom/box':
$config->setRenderer('Mageme_CustomField_Block_Adminhtml_Renderer_BoxColumn');
break;
}
}
public function webformsBlockAdminhtmlResultsEditFormPrepareLayoutField(Varien_Object $observer)
{
/** @var Varien_Data_Form $form */
$fieldset = $observer->getFieldset();
$field = $observer->getField();
$config = $observer->getConfig();
// add new field type to admin form
$fieldset->addType('box', Mage::getConfig()->getBlockClassName('customfield/adminhtml_element_box'));
switch($field->getType()){
case 'custom/box':
$config->setType('box');
break;
}
}
public function webformsResultsTohtmlValue(Varien_Object $observer)
{
/** @var Varien_Object $value */
$value = $observer->getValue();
/** @var VladimirPopov_WebForms_Model_Fields $field */
$field = $observer->getField();
// prepare custom field for result visualisation
switch($field->getType()){
case 'custom/box':
$json_data = $value->getValue();
$box_array = json_decode($json_data, true);
$str = array();
for ($i = 0; $i < count($box_array); $i++) {
$str[] = $box_array[$i]["width"] . "cm x " . $box_array[$i]["height"] . "cm x " . $box_array[$i]["depth"] . "cm";
}
$value->setHtml(implode('<br>',$str));
break;
}
}
}

Block / Adminhtml / Element / Box.php

Here we create backend representation of the field in case we need to edit submission data. Our element renderer displays block that utilizes adminhtml template customfield/box.phtml.

backend

?php
class Mageme_CustomField_Block_Adminhtml_Element_Box extends Varien_Data_Form_Element_Abstract
{
public function getElementHtml()
{
$config = array(
'field_name' => $this->getName(),
'required' => $this->getRequired(),
'value' => $this->getValue(),
'template' => 'customfield/box.phtml'
);
$html = Mage::app()->getLayout()->createBlock('core/template', $this->getName(), $config)->toHtml();
$html .= $this->getAfterElementHtml();
return $html;
}
}

Block / Adminhtml / Renderer / BoxColumn.php

We need to display the submission data in readable format.

column

<?php
class Mageme_CustomField_Block_Adminhtml_Renderer_BoxColumn extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract
{
public function render(Varien_Object $row)
{
$json_data = $row->getData($this->getColumn()->getIndex());
$box_array = json_decode($json_data, true);
$str = array();
for ($i = 0; $i < count($box_array); $i++) {
$str[] = $box_array[$i]["width"] . "cm x " . $box_array[$i]["height"] . "cm x " . $box_array[$i]["depth"] . "cm";
}
return implode("<br>", $str);
}
}

app / design / frontend / base / default / template / customfield / box.html

Our template contains some CSS styles, JavaScript and HTML code to create table like input field with the ability to add more records.

Since we need to store 2 dimensional data in the text field in the database we will need to serialize values.

The easiest way to achieve it is to serialize data with JSON. We will attach events on each field change event and update hidden field with the JSON string of our input array so that it submits already serialized.

<style>
.webforms-fields-<?php echo $this->getField()->getId()?> {
width: 100% !important;
}
.box-input {
float: left;
margin-right: 5px;
}
.box-dimension {
width: 100px;
}
.box-dimension .input-text {
width: 95px !important;
}
.box-row {
clear: both;
float: left;
padding-bottom: 5px;
}
.button .box-button span span {
line-height: 20px;
}
</style>
<script>
function removeBox(el) {
var boxRow = el.up().up();
boxRow.remove();
}
// duplicate box on button click
function cloneBox(el) {
var boxRow = el.up().up();
var boxContainer = boxRow.up();
newBox = boxRow.clone(true);
// change add button
var button = newBox.getElementsByClassName('box-button')[0];
button.setAttribute('onclick', 'removeBox(this)');
button.childElements()[0].childElements()[0].update('X');
// append to the main container
boxContainer.appendChild(newBox);
storeBoxData(boxContainer.getAttribute('data-id'));
}
// store JSON array in hidden field
function storeBoxData(fieldId) {
var boxArray = $$('div[data-id="' + fieldId + '"] .box-row');
var boxData = [];
for (var i = 0; i < boxArray.length; i++) {
boxData.push({
width: $$('input[data-name="' + fieldId + '_width"]')[i].value,
height: $$('input[data-name="' + fieldId + '_height"]')[i].value,
depth: $$('input[data-name="' + fieldId + '_depth"]')[i].value
});
}
$$('input[name="' + fieldId + '"]')[0].setValue(JSON.stringify(boxData));
}
// initialize box with the value
function initBox(fieldName, boxData) {
var container = $$('div[data-id="' + fieldName + '"]')[0];
for (var i = 0; i < boxData.length - 1; i++) {
cloneBox(container.getElementsByClassName('add-select-row')[0]);
}
// map values
var boxArray = $$('div[data-id="' + fieldName + '"] .box-row');
for (i = 0; i < boxArray.length; i++) {
$$('input[data-name="' + fieldName + '_width"]')[i].setValue(boxData[i].width);
$$('input[data-name="' + fieldName + '_height"]')[i].setValue(boxData[i].height);
$$('input[data-name="' + fieldName + '_depth"]')[i].setValue(boxData[i].depth);
}
storeBoxData(fieldName);
}
</script>
<input type="hidden" name="<?php echo $this->getFieldName() ?>"/>
<div data-id="<?php echo $this->getFieldName() ?>" class="customfield-box">
<div class="box-row">
<div class="box-input">
<div class="box-dimension">
<input data-name="<?php echo $this->getFieldName() ?>_width"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getField()->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Width (cm)"
onchange="storeBoxData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="box-input">
<div class="box-dimension">
<input data-name="<?php echo $this->getFieldName() ?>_height"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getField()->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Height (cm)"
onchange="storeBoxData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="box-input">
<div class="box-dimension">
<input data-name="<?php echo $this->getFieldName() ?>_depth"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getField()->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Depth (cm)"
onchange="storeBoxData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="box-input">
<button type="button" class="button box-button add-select-row" onclick="cloneBox(this)">
<span><span>Add box</span></span>
</button>
</div>
</div>
</div>
<?php if ($this->getField()->getCustomerValue()) { ?>
<script>
// initialize field with previously submitted data
initBox('<?php echo $this->getFieldName() ?>', <?php echo $this->getField()->getCustomerValue()?>);
</script>
<?php }

app / design / adminhtml / default / default / template / customfield / box.html

Admin template is similar to frontend except it has some little nuances.

<style>
.box-input {
float: left;
margin-right: 5px;
}
.box-dimension {
width: 100px !important;
}
.box-dimension input {
width: 95px !important;
}
.box-row {
clear: both;
padding-bottom: 5px;
float: left;
}
</style>
<script>
function removeBox(el) {
var boxRow = el.up().up();
var boxContainer = boxRow.up();
boxRow.remove();
storeBoxData(boxContainer.getAttribute('data-id'));
}
function cloneBox(el) {
var boxRow = el.up().up();
var boxContainer = boxRow.up();
newBox = boxRow.clone(true);
// change add button
var button = newBox.getElementsByClassName('add-select-row')[0];
button.setAttribute('onclick', 'removeBox(this)');
button.setAttribute('class', 'scalable delete delete-select-row icon-btn');
button.childElements()[0].childElements()[0].update('Remove');
// append to the main container
boxContainer.appendChild(newBox);
storeBoxData(boxContainer.getAttribute('data-id'));
}
// store JSON array in hidden field
function storeBoxData(fieldName) {
var boxArray = $$('div[data-id="' + fieldName + '"] .box-row');
var boxData = [];
for (var i = 0; i < boxArray.length; i++) {
boxData.push({
width: $$('input[data-name="' + fieldName + '_width"]')[i].value,
height: $$('input[data-name="' + fieldName + '_height"]')[i].value,
depth: $$('input[data-name="' + fieldName + '_depth"]')[i].value
});
}
$$('input[name="' + fieldName + '"]')[0].setValue(JSON.stringify(boxData));
}
function initBox(fieldName, boxData) {
var container = $$('div[data-id="' + fieldName + '"]')[0];
for (var i = 0; i < boxData.length - 1; i++) {
cloneBox(container.getElementsByClassName('add-select-row')[0]);
}
// map values
var boxArray = $$('div[data-id="' + fieldName + '"] .box-row');
for (i = 0; i < boxArray.length; i++) {
$$('input[data-name="' + fieldName + '_width"]')[i].setValue(boxData[i].width);
$$('input[data-name="' + fieldName + '_height"]')[i].setValue(boxData[i].height);
$$('input[data-name="' + fieldName + '_depth"]')[i].setValue(boxData[i].depth);
}
storeBoxData(fieldName);
}
</script>
<input type="hidden" name="<?php echo $this->getFieldName() ?>"/>
<div data-id="<?php echo $this->getFieldName() ?>" class="customfield-box">
<div class="box-row">
<div class="box-input">
<div class="box-dimension">
<input data-name="<?php echo $this->getFieldName() ?>_width"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Width (cm)"
onchange="storeBoxData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="box-input">
<div class="box-dimension">
<input data-name="<?php echo $this->getFieldName() ?>_height"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Height (cm)"
onchange="storeBoxData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="box-input">
<div class="box-dimension">
<input data-name="<?php echo $this->getFieldName() ?>_depth"
type="number"
min="0"
max="100"
class="input-text <?php if ($this->getRequired()) { ?>required-entry<?php } ?>"
placeholder="Depth (cm)"
onchange="storeBoxData('<?php echo $this->getFieldName() ?>')"/>
</div>
</div>
<div class="box-input">
<button type="button" class="scalable add add-select-row" onclick="cloneBox(this)">
<span><span>Add box</span></span>
</button>
</div>
</div>
</div>
<script>
initBox('<?php echo $this->getFieldName() ?>', <?php echo $this->getValue()?>);
</script>

Summary

We have created a basic field type which can be re-used to create new field types according to your needs.

You can download the sources here.

If you have created a field type you wish to share with others you can contact me and I will add it below.