Limitations of multi-select boxes
There are many cases in an ecommerce platform where some entities are related to each other with one-to-many relationship. Familiar examples are Upsell, Cross-sell and Related Products. One product may have many upsell, cross-sell or related products. Let’s take Upsell Products for example. Each product can have more than one Upsell product. To accept data for managing and storing this kind of relationship through a form or admin interface, multi-select boxes are normally sufficient, but this approach has a few limitations:
- It can be used to display only one property of the entity. In our example, we can display product names in a multi-select box, but we can’t easily display other attributes like SKU, price etc.
- The user can only select multiple entities but cannot supply additional data for each selection. In our example, the form can accept a selection of products but cannot accept data for the position of each product.
- When there is huge number of options, multi-select boxes can become bulky and difficult to manage.
- It cannot provide sorting or filtering so that user can easily pick choices from a subset of results.
The Grid Component as a Form Field
To overcome the limitations of a simple multi-select box, we should have an advanced element that has features like:
- Ability to select/unselect multiple entities
- Ability to display more information about entities
- Ability to accept additional user input data
- Should support pagination when there is a large number of entities
- Should support sorting and filtering by multiple parameters
- Should be unified so that it can be used for different entities and different data
Fortunately, Magento provides some very interesting components, which are very useful in the development of extensions involving the admin interface. These components are so powerful that developers have to write very little to almost ‘no’ template code to create an intuitive user interface.
One of these components is the ‘Grid’. It provides built-in pagination, sorting, and filtering. The Grid is widely used in the admin interface for listing entities like products, orders, invoices, customers etc. But my most favorite use of the Grid is as a form element. Magento provides an excellent solution to overcome the above described limitations of multi-select boxes by using the grid as a form element for accepting data for creating one-to-many relationships.
$this->addColumn('in_products', array(
'header_css_class' => 'a-center',
'type' => 'checkbox',
'name' => 'in_products',
'values' => $this->_getSelectedProducts(),
'align' => 'center',
'index' => 'entity_id'
));
When a Grid is used as a form element as a replacement for multi-select boxes, normally, the first column type is defined as ‘checkbox’. So each row in the grid displays a checkbox. Users can tick the checkboxes to select particular rows. This is more intuitive than selecting multiple options in a multi-select box.
Data preservation problem between AJAX reloads
At first glance, this mechanism seems perfect. But, the grid also has pagination, sorting and filtering. Each time these operations are performed, the grid is reloaded through an AJAX call, and each time the grid is reloaded, the selection made previously is lost. If, for example, we are selecting upsell products, we could select 3 products from the grid by ticking the corresponding checkboxes. Now, we click on the ‘Next’ link to load the next page of products and perhaps, select 3 products here too. Then, we click on the ‘Previous’ link to load the previous page again. We may find that our 3 previously chosen products are no longer selected. The same would happen for sorting and filtering. So while Grid component overcomes the limitation of multi-select boxes, the features of pagination, filtering and sorting add another issue in the sense that our selected data is not preserved across these actions.
How the Serializer Block solves data preservation problem
Magento provides another block called the ‘Serializer Block’ for preserving selection data. The serializer block is normally appended to the grid block. This block adds a hidden field and creates a Serializer JavaScript object. The Serializer JS Object uses the Grid JS Object to retrieve any selected rows. Then it serializes the data i.e. key1=value1&key2=value2&… and stores this serialized string in the hidden field. As this block is always outside of the grid block, even after the grid block is reloaded via AJAX, the value of it is preserved. This serialized string is passed as part of the form submission data which can easily be decoded using the PHP function parse_str().
However, a form element doesn’t only store user input data but also displays what has been stored in it. For example, when we enter some text in a textbox, it displays whatever we have typed. When we select any option from a select box, it displays which options have been selected. Similarly, this new user input element should also display which rows a user has selected. In other words, it should mark checkboxes checked for each selected row.
For this, Serializer JS object keeps its interaction with the Grid JS object by observing events from the grid. So when a grid block is reloaded, the Serializer JS object automatically selects checkboxes of rows, which we may have already selected.
So, with this theory out of the way, we can focus on how to apply this knowledge to create our own custom admin grid interfaces.
Using the Grid Serializer Block
Magento provides a built-in library for creating custom grid and serializer blocks. Here, we can take Upsell Products grid as an example to understand how this works.
On the product edit page, there is a tab – Upsell Products. The tab content is loaded via AJAX. The action called for this AJAX tab is: Mage_Adminhtml_Catalog_ProductController::upsellAction().
So the layout update loaded for this action is defined as below in the layout XML file app/design/adminhtml/default/default/catalog.xml
<adminhtml_catalog_product_upsell>
<block type="core/text_list" name="root">
<block type="adminhtml/catalog_product_edit_tab_upsell" name="catalog.product.edit.tab.upsell"/>
<block type="adminhtml/widget_grid_serializer" name="upsell_grid_serializer">
<reference name="upsell_grid_serializer">
<action method="initSerializerBlock">
<grid_block_name>catalog.product.edit.tab.upsell</grid_block_name>
<data_callback>getSelectedUpsellProducts</data_callback>
<hidden_input_name>links[upsell]</hidden_input_name>
<reload_param_name>products_upsell</reload_param_name>
</action>
<action method="addColumnInputName">
<input_name>position</input_name>
</action>
</reference>
</block>
</block>
</adminhtml_catalog_product_upsell>
Here, the root block is core/text_list type. So all children blocks are automatically rendered in sequence. See Demystifying Magento’s Layout XML Part 1.
First child block catalog.product.edit.tab.upsell is type of adminhtml/catalog_product_edit_tab_upsell, which is inherited class of Mage_Adminhtml_Block_Widget_Grid. So it creates a grid. The second child block adminhtml/widget_grid_serializer is of type adminhtml/widget_grid_serializer, which is the serializer block. So as we discussed above, the serializer block is appended to the grid block.
In layout XML, for the serializer block, an action initSerializerBlock is called with the following parameters:
- grid_block_name: It defines the grid’s block name in the layout. In our case it is
catalog.product.edit.tab.upsell. - data_callback: This defines a method to be called on the grid block instance to retrieve already selected values. In our case it is
getSelectedUpsellProducts(). - hidden_input_name: This is a hidden input element name by which the data can be retrieved from the POST object. In our case it is
links[upsell]. - reload_param_name: This is the parameter name by which the selection data is posted back to the AJAX call while reloading the grid for pagination, filtering, sorting etc. In our case it is
products_upsell.
When the serializer block is initialized, it calls a method defined by data_callback on our grid block instance to retrieve initial data. In our case it is getSelectedUpsellProducts(). Here is the code for this method in Upsell Grid Block:
public function getSelectedUpsellProducts()
{
$products = array();
foreach (Mage::registry('current_product')->getUpSellProducts() as $product) {
$products[$product->getId()] = array('position' => $product->getPosition());
}
return $products;
}
Here, it returns already saved upsell products. The serializer JS object stores it in the serializer field. So initially, when our grid is loaded for the first time, the serializer sets saved upsell products as selected.
In the grid block, a protected method _prepareColumns() is used to define the grid columns. The first column of upsell grid block is defined as checkbox:
$this->addColumn('in_products', array(
'header_css_class' => 'a-center',
'type' => 'checkbox',
'name' => 'in_products',
'values' => $this->_getSelectedProducts(),
'align' => 'center',
'index' => 'entity_id'
));
Here, type is defined as 'checkbox' so the column will display checkboxes for each row. values defines the checkbox values i.e. product ids for which checkboxes should be selected. Here, $this->_getSelectedProducts() is called to get the selection data.
There is also a method getGridUrl() defined in the upsell grid block:
public function getGridUrl()
{
return $this->_getData('grid_url') ? $this->_getData('grid_url') : $this->getUrl('*/*/upsellGrid', array('_current'=>true));
}
This method defines the URL to be called for AJAX reloading while paginating, sorting or filtering. This URL should return only a Grid block without including any other blocks such as the Serializer block.
The action method for this URL is defined as:
public function upsellGridAction()
{
$this->_initProduct();
$this->loadLayout();
$this->getLayout()->getBlock('catalog.product.edit.tab.upsell')
->setProductsUpsell($this->getRequest()->getPost('products_upsell', null));
$this->renderLayout();
}
The layout XML for it is defined as:
<adminhtml_catalog_product_upsellgrid>
<block type="core/text_list" name="root">
<block type="adminhtml/catalog_product_edit_tab_upsell" name="catalog.product.edit.tab.upsell"/>
</block>
</adminhtml_catalog_product_upsellgrid>
So the Serializer block adds a hidden field with a name as defined by hidden_input_name. When a user selects any row, the serialized data is stored in that hidden field. When a user browses next/previous pages, sorts the grid or filters by a value, the grid is reloaded via AJAX. The selection data is added as a parameter to this AJAX call with parameter name defined by reload_param_name, which is products_upsell in our case. As mentioned above in the code snippet of upsellGridAction(), the value of the POST parameter products_upsell is retrieved and passed in setProductsUpsell() on our upsell grid block instance. So calling getProductsUpsell() on upsell grid block instance will return previously selected data.
I mentioned above that the call $this->_getSelectedProducts() is used for getting selection data. Here is the code for this method:
protected function _getSelectedProducts()
{
$products = $this->getProductsUpsell();
if (!is_array($products)) {
$products = array_keys($this->getSelectedUpsellProducts());
}
return $products;
}
As mentioned earlier, the selection data can be retrieved by calling getProductsUpsell() on the upsell block instance. Here, it is attempted to retrieve selection data by calling this method. But in case, we are editing an existing product and if, after loading the Upsell Products tab, we haven’t made any changes, the grid should display already saved upsell products as selected. However, if there is no previous selection data found, the grid attempts to retrieve any edited upsell products by calling $this->getSelectedUpsellProducts().
Finally, when the product form is submitted, the selection data can be retrieved from the POST object by the hidden_input_name key. In our case it is links[upsell]. This data is still in serialized format. Magento provides a Helper Method for decoding serialized data: Mage::helper('adminhtml/js')->decodeGridSerializedInput().
The product controller calls _initSave() before doing actual save operation. This method contains code, which is responsible for decoding serialized input:
$links = $this->getRequest()->getPost('links');
...
if (isset($links['upsell']) && !$product->getUpsellReadonly()) {
$product->setUpSellLinkData(Mage::helper('adminhtml/js')->decodeGridSerializedInput($links['upsell']));
}
So, to summarize, the complete execution sequence of the Grid Serializer block:
- Initially, Grid and Serializer blocks are loaded
- When Serializer block is initialized, it loads saved selection data, stores it in a hidden field in serialized format and marks the corresponding entities as selected in the grid block.
- When a user makes any changes on the grid, it is automatically reflected in the hidden serializer field.
- While paging, filtering or sorting, only the grid is reloaded not the serializer. So our serializer data is not lost.
- While reloading, the current selection data is passed in the POST parameters to an AJAX call.
- The Grid block attempts to retrieve data from the POST. If no data is found, it retrieves the saved data.
- When the form is submitted, selection data is retrieved in serialized format.
- Finally the selection data is decoded and stored in the database.
Summary
Here, we have taken the example of Upsell Product management to understand how to use the Grid and Serializer blocks in combination as a replacement for multi-select boxes. In my next tutorial in this series, I will discuss more advanced usage of the Grid Serializer to create custom widgets that provide a more fully featured and user friendly interface.
Originally published on magebase.com. Copyright © 2012 Magebase - All Rights Reserved.




Proud members of the
I try to set somethig like :
protected function _prepareCollection()
{
$tm_id = $this->getRequest()->getParam(‘id’);
if(!isset($tm_id)) {
$tm_id = 0;
}
$pData = Mage::getResourceModel(‘checkprices/changedprice_changedprice’)->getPriceChanges($tm_id);
$this->setCollection($pData);
return parent::_prepareCollection();
}
In grid.php , getPriceChanges this method retrive me data , but i am getting
” Fatal error: Call to a member function setPageSize() on a non-object in” error. Can you please help me.
Sorry, this isn’t related to this article.
i still remember u taught that to me.
[...] == "undefined"){ addthis_share = [];}After reading this helpful article about Understanding the Grid Serializer Block I found that after the user clicked the “reset filter” button on the grid to reveal [...]
Hello Paryankbhai,
Great Post!!!
I have created one Grid using serialize it is working fine but i have to add select all and unselect all options on that grid , i have tried it,
i have also want to keep selected data as selected when i go to any page and come back.
can you please help me ?
Thanks,Harsh
Hi
I added to this grid, a new column that is a dropdown , with the option to modify it for each product listed in the grid. But I can’t manage this dropdown’s selected value to send it to post to other pages, so when i return to initial page, the selected value is gone.
Any suggestions on how to change the post array that it’s sent?
Many thanks in advance
Thank you for this tutorial. I must take time to read and follow your guide, it’s very detailed.
You can edit the template for the grid block in the xml update from you module, so you don’t have to put an editable or visible “position” column. Instead you add a hidden field with the name you like (the same you set in the addColumnInputName method). You have to put this hidden input after the checkbox, like at the en of the TR tag.
Greetings and thanks for the serializer explanation.