In my humble opinion, creating new
sections in the Admin Panel are a tad bit more complicated than creating new
features on the frontend. Hopefully, this post will help get you a few steps
closer to being able to understand and create adminhtml grids and forms.
The first thing you need to do is create a
menu item to get to your new grid, then you begin the exciting journey into
some Adminhtml action.
The Config
How about I just throw the whole stinkin' config.xml file out here right off the bat:
How about I just throw the whole stinkin' config.xml file out here right off the bat:
|
<config>
<modules>
<Super_Awesome>
<version>..</version>
</Super_Awesome>
</modules>
<adminhtml>
<!--
The <layout> updates allow us to define our block layouts in a seperate
file so are aren't messin' with the magento layout files. -->
<layout>
<updates>
<awesome>
<file>awesome.xml</file>
</awesome>
</updates>
</layout>
<!--
The <acl> section is for access control. Here we define the pieces
where access can be controlled within a role. -->
<acl>
<resources>
<admin>
<children>
<awesome>
<title>Awesome
Menu Item</title>
<children>
<example translate="title" module="awesome">
<title>Example
Menu Item</title>
</example>
</children>
</awesome>
</children>
</admin>
</resources>
</acl>
</adminhtml>
<admin>
<!--
Here
we are telling the Magento router to look for the controllers in the
Super_Awesome_controllers_Adminhtml before we look in the
Mage_Adminhtml
module for all urls that begin with /admin/controller_name
-->
<routers>
<adminhtml>
<args>
<modules>
<awesome before="Mage_Adminhtml">Super_Awesome_Adminhtml</awesome>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<global>
<models>
<awesome>
<class>Super_Awesome_Model</class>
<resourceModel>awesome_mysql</resourceModel>
</awesome>
<awesome_mysql>
<class>Super_Awesome_Model_Mysql</class>
<entities>
<example>
<table>Super_Awesome_example</table>
</example>
</entities>
</awesome_mysql>
</models>
<resources>
<awesome_setup>
<setup>
<module>Super_Awesome</module>
</setup>
<connection>
<use>core_setup</use>
</connection>
</awesome_setup>
<awesome_write>
<connection>
<use>core_write</use>
</connection>
</awesome_write>
<awesome_read>
<connection>
<use>core_read</use>
</connection>
</awesome_read>
</resources>
<blocks>
<awesome>
<class>Super_Awesome_Block</class>
</awesome>
</blocks>
<helpers>
<awesome>
<class>Super_Awesome_Helper</class>
</awesome>
</helpers>
</global>
</config>
|
Some things of note about the
config.xml:
- There is a layout file defined (awesome.xml)
- There is a change to the adminhtml router that tells the route to look in our module before looking into Mage_Adminhtml
- Everything else is pretty straight-forward (models, resource models, collections, setup, blocks, helpers...)
The Layout
Before I forget, here is the contents of the layout file (design/adminhtml/default/default/layout/awesome.xml):
Before I forget, here is the contents of the layout file (design/adminhtml/default/default/layout/awesome.xml):
|
<?xml version=“1.0”?>
<layout>
<adminhtml_example_index>
<reference name="content">
<block type="awesome/adminhtml_example" name="example" />
</reference>
</adminhtml_example_index>
<adminhtml_example_edit>
<reference name="content">
<block type="awesome/adminhtml_example_edit" name="example_edit" />
</reference>
</adminhtml_example_edit>
</layout>
|
We don't need to define much in
here. The reason I think that Admin Panel coding is a little more complicated
is because there is a lot of things that happen behind the scenes that are not
driven by the layout as we normally see it.
Install Script
For testing/example purposes, I also created an install script to create a table and load up some data. In the sql/awesome_setup/mysql4-install-0.1.0.php file, I have:
For testing/example purposes, I also created an install script to create a table and load up some data. In the sql/awesome_setup/mysql4-install-0.1.0.php file, I have:
|
<?php
$installer =
$this;
$installer->startSetup();
$installer->run("
-- DROP TABLE IF
EXISTS {$this->getTable('super_awesome_example')};
CREATE TABLE
{$this->getTable('super_awesome_example')} (
`id`
int() unsigned NOT NULL auto_increment,
`name`
varchar() NOT NULL,
`description`
varchar() NOT NULL,
`other` varchar()
NOT NULL,
PRIMARY
KEY (`id`)
)
ENGINE=InnoDB DEFAULT CHARSET=latin AUTO_INCREMENT= ;
INSERT INTO
{$this->getTable('super_awesome_example')} (name, description, other)
values ('Example ', 'Example One Description', 'This first example is reall
awesome.');
INSERT INTO
{$this->getTable('super_awesome_example')} (name, description, other)
values ('Example ', 'Example Two Description', 'This second example is reall
awesome.');
INSERT INTO
{$this->getTable('super_awesome_example')} (name, description, other)
values ('Example ', 'Example Three Description', 'This third example is reall
awesome.');
INSERT INTO
{$this->getTable('super_awesome_example')} (name, description, other)
values ('Example ', 'Example Four Description', 'This fourth example is reall
awesome.');
");
$installer->endSetup();
|
The Models
In this example, I am going to assume you know how to create a model, its resource model, and its collection model. I created the following classes:
Super_Awesome_Model_Example
Super_Awesome_Model_Mysql_Example
Super_Awesome_Model_Mysql_Example_Collection
In this example, I am going to assume you know how to create a model, its resource model, and its collection model. I created the following classes:
Super_Awesome_Model_Example
Super_Awesome_Model_Mysql_Example
Super_Awesome_Model_Mysql_Example_Collection
The Controller
Hopefully, we all know what a controller does, so I won't explain that part of the MVC pattern. The adminhtml controllers generally provide actions to do basic CRUD operations on the model. In ours, you will find the following actions:
Hopefully, we all know what a controller does, so I won't explain that part of the MVC pattern. The adminhtml controllers generally provide actions to do basic CRUD operations on the model. In ours, you will find the following actions:
- index - Shows the grid.
- edit - Shows the edit/new form.
- save - Saves the form data.
- delete - Deletes the model.
- new - Forwards on to the edit action
There really isn't anything crazy
going on here, so I would just take a few minutes to read through the code and
get an understanding of what each function does (and does not do):
|
<?php
class Super_Awesome_Adminhtml_ExampleController
extends Mage_Adminhtml_Controller_Action
{
public function indexAction()
{
$this->loadLayout();
$this->renderLayout();
}
public function newAction()
{
$this->_forward('edit');
}
public function editAction()
{
$id =
$this->getRequest()->getParam('id', null);
$model =
Mage::getModel('awesome/example');
if ($id)
{
$model->load((int)
$id);
if ($model->getId())
{
$data =
Mage::getSingleton('adminhtml/session')->getFormData(true);
if ($data)
{
$model->setData($data)->setId($id);
}
}
else {
Mage::getSingleton('adminhtml/session')->addError(Mage::helper('awesome')->__('Example
does not exist'));
$this->_redirect('*/*/');
}
}
Mage::register('example_data',
$model);
$this->loadLayout();
$this->getLayout()->getBlock('head')->setCanLoadExtJs(true);
$this->renderLayout();
}
public function saveAction()
{
if ($data =
$this->getRequest()->getPost())
{
$model =
Mage::getModel('awesome/example');
$id =
$this->getRequest()->getParam('id');
if ($id)
{
$model->load($id);
}
$model->setData($data);
Mage::getSingleton('adminhtml/session')->setFormData($data);
try {
if ($id)
{
$model->setId($id);
}
$model->save();
if (!$model->getId())
{
Mage::throwException(Mage::helper('awesome')->__('Error
saving example'));
}
Mage::getSingleton('adminhtml/session')->addSuccess(
Mage::helper('awesome')->__('Example
was successfully saved.'));
Mage::getSingleton('adminhtml/session')->setFormData(false);
//
The following line decides if it is a "save" or "save and
continue"
if ($this->getRequest()->getParam('back'))
{
$this->_redirect('*/*/edit',
array('id' => $model->getId()));
}
else {
$this->_redirect('*/*/');
}
}
catch (Exception $e) {
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
if ($model &&
$model->getId()) {
$this->_redirect('*/*/edit',
array('id' => $model->getId()));
}
else {
$this->_redirect('*/*/');
}
}
return;
}
Mage::getSingleton('adminhtml/session')->addError(Mage::helper('awesome')->__('No
data found to save'));
$this->_redirect('*/*/');
}
public function deleteAction()
{
if ($id =
$this->getRequest()->getParam('id')) {
try {
$model =
Mage::getModel('awesome/example');
$model->setId($id);
$model->delete();
Mage::getSingleton('adminhtml/session')->addSuccess(
Mage::helper('awesome')->__('The
example has been deleted.'));
$this->_redirect('*/*/');
return;
}
catch (Exception
$e) {
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
$this->_redirect('*/*/edit',
array('id' => $this->getRequest()->getParam('id')));
return;
}
}
Mage::getSingleton('adminhtml/session')->addError(Mage::helper('adminhtml')->__('Unable
to find the example to delete.'));
$this->_redirect('*/*/');
}
}
|
The Grid Block
Here is where it starts getting a touch tricky. When you originally click on the menu item to see the grid of examples, you are going to the "indexAction" in the controller and simply loading and rendering the layout. This means that you will probably have something halfway useful to see in the awesome.xml layout file. We see that there is only one block defined for that handle, and that block is: 'awesome/adminhtml_example'. This block extends Mage_Adminhtml_Block_Widget_Grid_Container which which tells us that our block (Super_Awesome_Block_Adminhtml_Example) will be be a container for a grid. What does that mean? This container will provide a few buttons at the top and automagically define the grid as a child block of itself. Below I will show you the entire contents of our container, and the piece that builds the name of the grid block (which is in the parent Mage_Adminhtml_Block_Widget_Grid_Container.
Here is where it starts getting a touch tricky. When you originally click on the menu item to see the grid of examples, you are going to the "indexAction" in the controller and simply loading and rendering the layout. This means that you will probably have something halfway useful to see in the awesome.xml layout file. We see that there is only one block defined for that handle, and that block is: 'awesome/adminhtml_example'. This block extends Mage_Adminhtml_Block_Widget_Grid_Container which which tells us that our block (Super_Awesome_Block_Adminhtml_Example) will be be a container for a grid. What does that mean? This container will provide a few buttons at the top and automagically define the grid as a child block of itself. Below I will show you the entire contents of our container, and the piece that builds the name of the grid block (which is in the parent Mage_Adminhtml_Block_Widget_Grid_Container.
Super_Awesome_Block_Adminhtml_Example:
|
<?php
class Super_Awesome_Block_Adminhtml_Example
extends Mage_Adminhtml_Block_Widget_Grid_Container
{
protected $_addButtonLabel =
'Add New Example';
public function __construct()
{
parent::__construct();
$this->_controller
= 'adminhtml_example';
$this->_blockGroup
= 'awesome';
$this->_headerText
= Mage::helper('awesome')->__('Examples');
}
}
|
|
protected function _prepareLayout()
{
$this->setChild(
'grid',
$this->getLayout()->createBlock(
$this->_blockGroup.'/' . $this->_controller . '_grid',
$this->_controller
. '.grid')->setSaveParametersInSession(true) );
return parent::_prepareLayout();
}
|
Now that we have the container we
need to build our grid (Super_Awesome_Block_Adminhtml_Example_Grid):
|
<?php
class Super_Awesome_Block_Adminhtml_Example_Grid
extends Mage_Adminhtml_Block_Widget_Grid
{
public function __construct()
{
parent::__construct();
$this->setId('example_grid');
$this->setDefaultSort('id');
$this->setDefaultDir('desc');
$this->setSaveParametersInSession(true);
}
protected function _prepareCollection()
{
$collection =
Mage::getModel('awesome/example')->getCollection();
$this->setCollection($collection);
return parent::_prepareCollection();
}
protected function _prepareColumns()
{
$this->addColumn('id',
array(
'header' => Mage::helper('awesome')->__('ID'),
'align' =>'right',
'width' => 'px',
'index' => 'id',
));
$this->addColumn('name',
array(
'header' => Mage::helper('awesome')->__('Name'),
'align' =>'left',
'index' => 'name',
));
$this->addColumn('description',
array(
'header' => Mage::helper('awesome')->__('Description'),
'align' =>'left',
'index' => 'description',
));
$this->addColumn('other',
array(
'header' => Mage::helper('awesome')->__('Other'),
'align' => 'left',
'index' => 'other',
));
return parent::_prepareColumns();
}
public function getRowUrl($row)
{
return $this->getUrl('*/*/edit',
array('id' => $row->getId()));
}
}
|
The _prepareCollection() function
gets the collection of data that will populate our grid, and the
_prepareColumns() function maps that data into the specific columns. Keep in
mind that the _prepareCollection() and the _prepareColumns() can be much more
detailed/complicated than my example here, so don't be afraid to try crazy
things.
If you stop here, you should have a
working grid.
The Forms
If you notice, the getRowUrl() on the grid returns back a url that maps to the editAction() in our controller. That is where were start the "form fun". The editAction() handles both the "edit" scenario and the "new" scenario for the model. It makes no difference to us since it is the same form for both.
If you notice, the getRowUrl() on the grid returns back a url that maps to the editAction() in our controller. That is where were start the "form fun". The editAction() handles both the "edit" scenario and the "new" scenario for the model. It makes no difference to us since it is the same form for both.
The edit action maps to a handle in
the awesome.xml layout file which simply defines the block:
awesome/adminhtml_example_edit. If we take a look at that block we will see the
following code:
|
<?php
class Super_Awesome_Block_Adminhtml_Example_Edit
extends Mage_Adminhtml_Block_Widget_Form_Container
{
public function __construct()
{
parent::__construct();
$this->_objectId
= 'id';
$this->_blockGroup
= 'awesome';
$this->_controller
= 'adminhtml_example';
$this->_mode
= 'edit';
$this->_addButton('save_and_continue',
array(
'label' =>
Mage::helper('adminhtml')->__('Save And Continue Edit'),
'onclick' =>
'saveAndContinueEdit()',
'class' =>
'save',
),
-);
$this->_updateButton('save',
'label', Mage::helper('awesome')->__('Save Example'));
$this->_formScripts[]
= "
function toggleEditor()
{
if (tinyMCE.getInstanceById('form_content')
== null) {
tinyMCE.execCommand('mceAddControl',
false, 'edit_form');
}
else {
tinyMCE.execCommand('mceRemoveControl',
false, 'edit_form');
}
}
function saveAndContinueEdit(){
editForm.submit($('edit_form').action+'back/edit/');
}
";
}
public function getHeaderText()
{
if (Mage::registry('example_data')
&& Mage::registry('example_data')->getId())
{
return Mage::helper('awesome')->__('Edit
Example "%s"',
$this->htmlEscape(Mage::registry('example_data')->getName()));
}
else {
return Mage::helper('awesome')->__('New
Example');
}
}
}
|
Just like the grid had a container,
so does the form. We are just changing some labels on buttons and creating some
javascript to handle the save scenarios. Below is the snippet of code in the
parent container that builds the name of the block that will be rendered
containing the actual form:
|
protected function _prepareLayout()
{
if ($this->_blockGroup
&& $this->_controller && $this->_mode) {
$this->setChild('form',
$this->getLayout()->createBlock($this->_blockGroup . '/' .
$this->_controller . '_' . $this->_mode . '_form'));
}
return parent::_prepareLayout();
}
|
Next/finally, we look at the actual
form class; It's so awesome, you might faint when you see it:
|
<?php
class Super_Awesome_Block_Adminhtml_Example_Edit_Form
extends Mage_Adminhtml_Block_Widget_Form
{
protected function _prepareForm()
{
if (Mage::getSingleton('adminhtml/session')->getExampleData())
{
$data =
Mage::getSingleton('adminhtml/session')->getExamplelData();
Mage::getSingleton('adminhtml/session')->getExampleData(null);
}
elseif (Mage::registry('example_data'))
{
$data =
Mage::registry('example_data')->getData();
}
else
{
$data =
array();
}
$form =
new Varien_Data_Form(array(
'id' =>
'edit_form',
'action' =>
$this->getUrl('*/*/save', array('id'
=>
$this->getRequest()->getParam('id'))),
'method' =>
'post',
'enctype' =>
'multipart/form-data',
));
$form->setUseContainer(true);
$this->setForm($form);
$fieldset =
$form->addFieldset('example_form', array(
'legend' =>Mage::helper('awesome')->__('Example
Information')
));
$fieldset->addField('name',
'text', array(
'label' => Mage::helper('awesome')->__('Name'),
'class' => 'required-entry',
'required' =>
true,
'name' => 'name',
'note' => Mage::helper('awesome')->__('The name of the
example.'),
));
$fieldset->addField('description',
'text', array(
'label' => Mage::helper('awesome')->__('Description'),
'class' => 'required-entry',
'required' =>
true,
'name' => 'description',
));
$fieldset->addField('other',
'text', array(
'label' => Mage::helper('awesome')->__('Other'),
'class' => 'required-entry',
'required' =>
true,
'name' => 'other',
));
$form->setValues($data);
return parent::_prepareForm();
}
}
|
Did you faint?
The only thing not straight-forward
here is the line: $form->setUseContainer(true);. This line is important
because it is the line that actually causes the form renderer to output the
surrounding <form> tags.