Magento have excellent feature which allow a developer to create the admin forms without coding any HTML. It is easy to create any form with any data. This article will guide you how to create such form.

New extension creation

First of all let's create new extension. In our case it will be 'Turnkeye_Adminform'.

Note: You can upload extension which described in this article here.

So we create file: 'app/etc/modules/Turnkeye_Adminform.xml' with extension declaration.
Also we need root directory for our extension, it will be: 'app/code/community/Turnkeye/Adminform'.

Next step is to create config.xml file for our extension: 'app/code/community/Turnkeye/Adminform/etc/config.xml'.
We will declare block, model, helper aliases, adminhtml router and layout file update in this config.xml. There will be also resources and translate file.

We will use 'turnkeye_adminform' as prefix for our block/model/helper. Finally we will create 'sql', 'Model', 'Block' and 'controllers' folders in root directory of our extension.

Menu item creation

Preparation stage is finished, and we can move forward.
We need a menu item in admin area with a link to our form. Lets create file 'app/code/community/Turnkeye/Adminform/etc/adminhtml.xml' and put the menu declaration in this file:

<?xml version="1.0"?>
<config>
    <menu>
        <turnkeye translate="title" module="turnkeye_adminform">
            <title>Turnkeye</title>
            <sort_order>90</sort_order>
            <children>
                <form translate="title" module="turnkeye_adminform">
                    <title>Form</title>
                    <sort_order>10</sort_order>
                    <action>adminhtml/adminform</action>
                </form>
            </children>
        </turnkeye>
    </menu>
</config>

Since we will use translation in our extension (module="turnkeye_adminform"), we need to create Helper class.
It is an easy step, just create a file with Helper class.

class Turnkeye_Adminform_Helper_Data extends Mage_Core_Helper_Abstract
{
}

We use 'adminform' controller alias. In our main config we need to declare admin router for it:

<admin>
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <turnkeye_adminform after="Mage_Adminhtml">Turnkeye_Adminform_Adminhtml</turnkeye_adminform>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>

We need to create controller file 'app/code/community/Turnkeye/Adminform/controllers/Adminhtml/AdminformController.php' with index action:

[cc lang='php' ]
class Turnkeye_Adminform_Adminhtml_AdminformController extends Mage_Adminhtml_Controller_Action
{

/**
* View form action
*/
public function indexAction()
{
$this->loadLayout();
$this->_setActiveMenu('turnkeye/form');
$this->_addBreadcrumb(Mage::helper('turnkeye_adminform')->__('Form'), Mage::helper('turnkeye_adminform')->__('Form'));
$this->renderLayout();
}

}

Use ACL for controller and menu

Let's be a good Magento developers and use ACL for our form. It can be done in 2 steps.

1. We need to add the following XML code to 'app/code/community/Turnkeye/Adminform/etc/adminhtml.xml' file:

    <acl>
        <resources>
            <all>
                <title>Allow Everything</title>
            </all>
            <admin>
                <children>
                    <turnkeye>
                        <title>Turnkeye</title>
                        <sort_order>90</sort_order>
                        <children>
                            <form>
                                <title>Form</title>
                                <sort_order>10</sort_order>
                            </form>
                        </children>
                    </turnkeye>
                </children>
            </admin>
        </resources>
    </acl>

2. Also we need to rewrite _isAllowed() method in our controller class:

[cc lang='php' ]
/**
* Check allow or not access to ths page
*
* @return bool - is allowed to access this menu
*/
protected function _isAllowed()
{
return Mage::getSingleton('admin/session')->isAllowed('turnkeye/form');
}

You should use the path which you declare in the ACL config (turnkeye/form), and this one should be the same as you declared in menu config.

Layouts and Blocks

Now you should have new menu item with a link in your Magento admin panel.
If you click on it you will see admin area interface with menu and footer, but with empty content section.

So we need to create layout file. In our case it will be 'app/design/adminhtml/default/default/layout/turnkeye_adminform.xml' file with the following content:

<?xml version="1.0"?>
<layout version="1.0.0">
    <adminhtml_adminform_index>
        <update handle="editor"/>
        <reference name="content">
            <block type="turnkeye_adminform/adminhtml_form_edit" name="adminform"/>
        </reference>
    </adminhtml_adminform_index>
</layout>

Now we need Magento blocks for our form.
First one is a container for a form. The container allow you to have header and buttons, which will be used for form submitting.

Of course you can create form block directly without a container, but it is standard Magento practice to use container on pages with a form.

According to layout, our container block is 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit.php':

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit extends Mage_Adminhtml_Block_Widget_Form_Container
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->_blockGroup = 'turnkeye_adminform';
$this->_controller = 'adminhtml_form';
$this->_headerText = Mage::helper('turnkeye_adminform')->__('Edit Form');
}
}

It is rather compact PHP class, but it is very important to declare correct _blockGroup and _controller properties.

The _blockGroup is a prefix to your extension blocks (part before the slash symbol, which you declared in config.xml).
The _controller is a path to group of blocks which will use for form creation.

If you look at the _prepareLayout() method of Mage_Adminhtml_Block_Widget_Form_Container, you will find the PHP logic that will create your form block (not block container which we define):

$this->setChild('form', $this->getLayout()->createBlock($this->_blockGroup . '/' . $this->_controller . '_' . $this->_mode . '_form'));

The _mode property is 'edit' by default, and that is why we use 'turnkeye_adminform/adminhtml_form_edit' for our form container block.

We just place all blocks, which are linked to one form in one place (in our case it is: app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/). It is possible to change _mode just like _blockGroup and _controller properties, also it is possible to rewrite _prepareLayout() method and create form block from another path.

So currently we need to create a form block and path to it will be 'turnkeye_adminform/adminhtml_form_edit_form'.
Also we need to create 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Form.php':

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
{
/**
* Preparing form
*
* @return Mage_Adminhtml_Block_Widget_Form
*/
protected function _prepareForm()
{
$form = new Varien_Data_Form(
array(
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save'),
'method' => 'post',
)
);

$form->setUseContainer(true);
$this->setForm($form);

$helper = Mage::helper('turnkeye_adminform');
$fieldset = $form->addFieldset('display', array(
'legend' => $helper->__('Display Settings'),
'class' => 'fieldset-wide'
));

$fieldset->addField('label', 'text', array(
'name' => 'label',
'label' => $helper->__('Label'),
));

if (Mage::registry('turnkeye_adminform')) {
$form->setValues(Mage::registry('turnkeye_adminform')->getData());
}

return parent::_prepareForm();
}
}

In this block you need redeclare _prepareForm() method, where you should create Varien_Data_Form object and assign it to your form block.

Also for form object you can create fieldsets which can contain different fields of your form.
When you create a field, the first parameter should be ID of a field (unique in the form) and second one is a type of field.

You can find the full list of all available field types in 'lib/Varien/Data/Form/Element/' folder.
Each file name in lower case and without '.php' postfix is a form element type that you can use.

The third parameter is an array with option for you field. Some types have their own parameters (like 'select' element with parameter 'values').

At the end of the method you need to set values for form elements from your entity object.

Your own field

If Magento predefined types are not suitable for your project, you can make your own field.

By default you can make this in the following ways:

1. Set specific renderer for your field.
2. Create you own field type.

If you prefer first variant you need to add following code in your form block:

$form->getElement('label')->setRenderer(Mage::app()->getLayout()->createBlock(
'turnkeye_adminform/adminhtml_form_edit_renderer_label'
));

Also, you will need to create a renderer class in file 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Renderer/Label.php':

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Renderer_Label
extends Mage_Adminhtml_Block_Widget
implements Varien_Data_Form_Element_Renderer_Interface
{

/**
* renderer
*
* @param Varien_Data_Form_Element_Abstract $element
*/
public function render(Varien_Data_Form_Element_Abstract $element)
{
$element->setDisabled(true);
$disabled = true;
$htmlId = 'use_config_' . $element->getHtmlId();
$html = '
' . $element->getLabelHtml() . '';
$html .= 'getId() . '"'. ($disabled ? ' checked="checked"' : '');
$html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />';
$html .= ' <label for="' . $htmlId . '">' . Mage::helper('turnkeye_adminform')->__('Do not change value') . '</label>';
$html .= $element->getElementHtml();
$html .= 'toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . '\').parentNode);';

return $html;
}
}

Please note that your renderer class should implement Varien_Data_Form_Element_Renderer_Interface interface. Method 'render' should return HTML source of whole form element (with label) not only value field. This variant is very useful when you need to change some existing form element. For example when you rewrite some existing form.

The second variant - is creating your own field type. In this case you need to register your type in fieldset first and element(s) of this type after that:

$fieldset->addType('multiselect_enabled', Mage::getConfig()->getBlockClassName('turnkeye_adminform/adminhtml_form_edit_renderer_multienabled'));
$fieldset->addField('available_sortby', 'multiselect_enabled', array(
'name' => 'available_sortby',
'label' => $helper->__('Available Product Listing Sort By'),
'values' => Mage::getModel('catalog/category_attribute_source_sortby')->getAllOptions(),
'checkbox_label' => $helper->__('Use All Available Attributes'),
'required' => true,
));

Also, you will need to create element type class in this file 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Renderer/Multienabled.php':

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Renderer_Multienabled extends Varien_Data_Form_Element_Multiselect
{

/**
* Retrieve Element HTML fragment
*
* @return string
*/
public function getElementHtml()
{
$disabled = false;
if (!$this->getValue()) {
$this->setData('disabled', 'disabled');
$disabled = true;
}

$html = parent::getElementHtml();
$htmlId = 'use_config_' . $this->getHtmlId();
$html .= 'getId() . '"';
$html .= ($disabled ? ' checked="checked"' : '') . ($this->getReadonly()? ' disabled="disabled"':'');
$html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />';
$html .= ' <label for="' . $htmlId . '">' . $this->getCheckboxLabel() . '</label>';
$html .= 'toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . '\').parentNode);';
return $html;
}

}

As you see, you can use any parameters in your new type (in our example it is 'checkbox label') and make your own fields output correspond this parameter.

Tabs in the form

Lets create tabs in our form.

Lets put our current form elements to 'General' tab and create new tab 'Products', that will list all products in the system with possibility to assign position. We need 3 new Blocks: Block with list of tabs, block for 'General' tab and block for 'Products' tab. Also we need to create our own action for products grid view, it will be used when we will make search in Products grid.

So, firstly we need to modify 'app/design/adminhtml/default/default/layout/turnkeye_adminform.xml' layout file. In our 'adminhtml_adminform_index' handler we need to add:

        <reference name="left">
            <block type="turnkeye_adminform/adminhtml_form_edit_tabs" name="adminform_tabs"/>
        </reference>

After that we need to create new tabs block in file 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Tabs.php'.
Lets start from one tab - 'General':

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs
{

/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->setId('edit_home_tabs');
$this->setDestElementId('edit_form');
$this->setTitle(Mage::helper('turnkeye_adminform')->__('Form Tabs'));
}

/**
* add tabs before output
*
* @return Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tabs
*/
protected function _beforeToHtml()
{
$this->addTab('general', array(
'label' => Mage::helper('turnkeye_adminform')->__('General'),
'title' => Mage::helper('turnkeye_adminform')->__('General'),
'content' => $this->getLayout()->createBlock('turnkeye_adminform/adminhtml_form_edit_tab_general')->toHtml(),
));
return parent::_beforeToHtml();
}

}

You can add tab using layout XML file, in this case your tab block should implement 'Mage_Adminhtml_Block_Widget_Tab_Interface' interface. We use another variant because when we will add tab with a grid, we will need to add a serializer, and to my opinion it is easier to make it this way.

After that we need to create file with 'General' tab, it will be 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Tab/General.php'. You need to move all functionality with fieldset and fields to this tab. The difference only in this code:

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tab_General extends Mage_Adminhtml_Block_Widget_Form
{

/**
* prepare form in tab
*/
protected function _prepareForm()
{
$form = new Varien_Data_Form();
$form->setHtmlIdPrefix('general_');
$form->setFieldNameSuffix('general');
...
}
}

It is not necessary to assign parameters to the form. This definition (only this one) should stay in form block. The 'html_id_prefix' will assign to all of your fields in HTML id's starting from 'general_'. The 'field_name_suffix' will set name for form elements like 'general[field_name_here]'. Of course this features can be used in any form, not only tabbed form.

Grid in tab

Finally lets add grid to new tab 'Products' with editable field 'Position' on it.

We need to add new tab to our Tabs block 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Tabs.php':

$product_content = $this->getLayout()->createBlock('turnkeye_adminform/adminhtml_form_edit_tab_product', 'adminform_products.grid')->toHtml();
$serialize_block = $this->getLayout()->createBlock('adminhtml/widget_grid_serializer');
$serialize_block->initSerializerBlock('adminform_products.grid', 'getSelectedProducts', 'products', 'selected_products');
$serialize_block->addColumnInputName('position');
$product_content .= $serialize_block->toHtml();
$this->addTab('associated_products', array(
'label' => Mage::helper('turnkeye_adminform')->__('Products'),
'title' => Mage::helper('turnkeye_adminform')->__('Products'),
'content' => $product_content
));

The serializer is most important feature which allow you to define fields which will be editable in the grid, and define a name parameter which you should see in Save action of the form.

So, we need to know the name of our grid block, that is why we created this block with second (not required) parameter 'adminform_products.grid'.

Next we create serialize block. In this block we need to run method initSerializerBlock with the following parameters:

1. Grid block name
2. Method name in our Grid Block, which return already selected products with values. This method should return data from the database where you store this information.
3. Name of parameters, which will send Save action to your form. You are able to get this value from $this->getRequest()->getParam('products');. This value will be encoded, but it is easy to decode it by the method 'Mage::helper('adminhtml/js')->decodeGridSerializedInput()'.
4. Name of parameters which will send to your reload grid action (when you will make search in grid).

Now we need to create grid block for tab in the file: 'app/code/community/Turnkeye/Adminform/Block/Adminhtml/Form/Edit/Tab/Products.php':

class Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tab_Product extends Mage_Adminhtml_Block_Widget_Grid
{

/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->setId('adminform_products');
$this->setDefaultSort('position');
$this->setDefaultDir('ASC');
$this->setDefaultFilter(array('in_category'=> 1));
$this->setUseAjax(true);
}

...

}

Here we will set 'use_ajax' to true for our grid. It mean that when user will use filter for searching, we will update our grid without full page reload.

Also by default we use filter 'in_category'. This filter will allow users to see only selected products when we go to form for values editing. This filter key is not a product attribute, so we need to make changes in _addColumnFilterToCollection method:

/**
* adding filter by column
*
* @param Varien_Object $column - colum data
* @return Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tab_Product
*/
protected function _addColumnFilterToCollection($column)
{
// Set custom filter for in category flag
if ($column->getId() == 'in_category') {
$productIds = $this->_getSelectedProducts();
if (empty($productIds)) {
$productIds = array(0);
}
if ($column->getFilter()->getValue()) {
$this->getCollection()->addFieldToFilter('entity_id', array('in'=> $productIds));
}
elseif (!empty($productIds)) {
$this->getCollection()->addFieldToFilter('entity_id', array('nin'=> $productIds));
}
}
else {
parent::_addColumnFilterToCollection($column);
}
return $this;
}

As you can see, we just added filter by entity_id to our collection. The array of this IDs returned _getSelectedProducts() method:


/**
* get selected products
*
* @return array|mixed
*/
protected function _getSelectedProducts()
{
$products = $this->getRequest()->getPost('selected_products');
if (is_null($products) && Mage::registry('turnkeye_adminform')) {
return array_keys($this->getSelectedProducts());
}

return $products;
}

Here we try to use POST value 'selected_products' for IDs at first. If you remember we assign this value for our serializer, so if user will make some changes in product list and start products search by any parameters, he will not lose any changes. If we do not enter this parameter in request it will use values from database, and these values will return getSelectedProducts() method. If you remember we define this method in our serializer too:


/**
* get selected products
*
* @return array
*/
public function getSelectedProducts()
{
$products = array();
if (Mage::registry('turnkeye_adminform')) {
foreach (Mage::registry('turnkeye_adminform')->getProductsPosition() as $id => $pos) {
$products[$id] = array('position' => $pos);
}
}

return $products;
}

Here we use our form entity object to get information about element position. Don't forget to create method 'getProductsPosition()' in your model.

As any other grid block, we need to define '_prepareCollection()' and '_prepareColumns()' methods. For collection we will use standard catalog collection with joined table with position value:


/**
* Prepare grid collection object
*
* @return Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tab_Product
*/
protected function _prepareCollection()
{
$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('name')
->addAttributeToSelect('sku')
->addAttributeToSelect('price')
// example of how to join your table with values
/* ->joinField('position',
'turnkeye_adminform/form_product',
'position',
'product_id=entity_id',
'category_id=' . 0,
'left')
*/
;
$this->setCollection($collection);
return parent::_prepareCollection();
}

The '_prepareColumns()' method should have editable column and checkbox column, you can add any other information you want to the grid:

/**
* prepare columns
*
* @return Turnkeye_Adminform_Block_Adminhtml_Form_Edit_Tab_Product
*/
protected function _prepareColumns()
{
$this->addColumn('in_category', array(
'header_css_class' => 'a-center',
'type' => 'checkbox',
'name' => 'in_category',
'values' => $this->_getSelectedProducts(),
'align' => 'center',
'index' => 'entity_id'
));
...
$this->addColumn('position', array(
'header' => Mage::helper('turnkeye_adminform')->__('Position'),
'width' => '1',
'type' => 'number',
'index' => 'position',
'editable' => true
));

As you see we added parameter 'editable' for column position, which make it editable.

Except this we should have 'getGridUrl()' method in our block class which should return URL for the grid view action. In out extension we use 'grid' action, so we need to add this action to our controller 'app/code/community/Turnkeye/Adminform/controllers/Adminhtml/AdminformController.php':

/**
* Grid Action
* Display list of products related to current category
*
* @return void
*/
public function gridAction()
{
$this->getResponse()->setBody(
$this->getLayout()->createBlock('turnkeye_facebookstore/adminhtml_home_edit_tab_product', 'facebookhome.product.grid')
->toHtml()
);
}

Finally I wish to note that we do not create any templates for forms, we use some HTML only in renderer action.

I hope this article will help you to understand how to create any form you need in Magento admin panel.