Assuming you have the module without the product relation here is what you need in addition.
First create a relation table between your entity and the products.
Add this in config.xml
inside the global/models/[module]_resource/entities
<[entity]_product>
<table>[entity]_product</table>
</[entity]_product>
Add this in one of the upgrade scripts.
$table = $this->getConnection()
->newTable($this->getTable('[module]/[entity]_product'))
->addColumn('rel_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
'unsigned' => true,
'identity' => true,
'nullable' => false,
'primary' => true,
), 'Relation ID')
->addColumn('[entity]_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
'unsigned' => true,
'nullable' => false,
'default' => '0',
), '[Entity] ID')
->addColumn('product_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
'unsigned' => true,
'nullable' => false,
'default' => '0',
), 'Product ID')
->addColumn('position', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
'nullable' => false,
'default' => '0',
), 'Position')
->addIndex($this->getIdxName('[module]/[entity]_product', array('product_id')), array('product_id'))
->addForeignKey($this->getFkName('[module]/[entity]_product', '[entity]_id', '[module]/[entity]', 'entity_id'), '[entity]_id', $this->getTable('[module]/[entity]'), 'entity_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
->addForeignKey($this->getFkName('[module]/[entity]_product', 'product_id', 'catalog/product', 'entity_id'), 'product_id', $this->getTable('catalog/product'), 'entity_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
->setComment('[Entity] to Product Linkage Table');
$this->getConnection()->createTable($table);
Now create the grid block. [Namespace]/[Module]/Block/Adminhtml/[Entity]/Edit/Tab/Product.php
<?php
class [Namespace]_[Module]_Block_Adminhtml_[Entity]_Edit_Tab_Product
extends Mage_Adminhtml_Block_Widget_Grid {
public function __construct(){
parent::__construct();
$this->setId('product_grid');
$this->setDefaultSort('position');
$this->setDefaultDir('ASC');
$this->setUseAjax(true);
if ($this->get[Entity]()->getId()) {
$this->setDefaultFilter(array('in_products'=>1));
}
}
protected function _prepareCollection() {
$collection = Mage::getResourceModel('catalog/product_collection');
$collection->addAttributeToSelect('price');
$adminStore = Mage_Core_Model_App::ADMIN_STORE_ID;
$collection->joinAttribute('product_name', 'catalog_product/name', 'entity_id', null, 'left', $adminStore);
if ($this->get[Entity]()->getId()){
$constraint = '{{table}}.[entity]_id='.$this->get[Entity]()->getId();
}
else{
$constraint = '{{table}}.[entity]_id=0';
}
$collection->joinField('position',
'[module]/[entity]_product',
'position',
'product_id=entity_id',
$constraint,
'left');
$this->setCollection($collection);
parent::_prepareCollection();
return $this;
}
protected function _prepareMassaction(){
return $this;
}
protected function _prepareColumns(){
$this->addColumn('in_products', array(
'header_css_class' => 'a-center',
'type' => 'checkbox',
'name' => 'in_products',
'values'=> $this->_getSelectedProducts(),
'align' => 'center',
'index' => 'entity_id'
));
$this->addColumn('product_name', array(
'header'=> Mage::helper('catalog')->__('Name'),
'align' => 'left',
'index' => 'product_name',
));
$this->addColumn('sku', array(
'header'=> Mage::helper('catalog')->__('SKU'),
'align' => 'left',
'index' => 'sku',
));
$this->addColumn('price', array(
'header'=> Mage::helper('catalog')->__('Price'),
'type' => 'currency',
'width' => '1',
'currency_code' => (string) Mage::getStoreConfig(Mage_Directory_Model_Currency::XML_PATH_CURRENCY_BASE),
'index' => 'price'
));
$this->addColumn('position', array(
'header'=> Mage::helper('catalog')->__('Position'),
'name' => 'position',
'width' => 60,
'type' => 'number',
'validate_class'=> 'validate-number',
'index' => 'position',
'editable' => true,
));
}
protected function _getSelectedProducts(){
$products = $this->get[Entity]Products();
if (!is_array($products)) {
$products = array_keys($this->getSelectedProducts());
}
return $products;
}
public function getSelectedProducts() {
$products = array();
$selected = Mage::registry('current_[entity]')->getSelectedProducts();
if (!is_array($selected)){
$selected = array();
}
foreach ($selected as $product) {
$products[$product->getId()] = array('position' => $product->getPosition());
}
return $products;
}
public function getRowUrl($item){
return '#';
}
public function getGridUrl(){
return $this->getUrl('*/*/productsGrid', array(
'id'=>$this->get[Entity]()->getId()
));
}
public function get[Entity](){
return Mage::registry('current_[entity]');
}
protected function _addColumnFilterToCollection($column){
// Set custom filter for in product flag
if ($column->getId() == 'in_products') {
$productIds = $this->_getSelectedProducts();
if (empty($productIds)) {
$productIds = 0;
}
if ($column->getFilter()->getValue()) {
$this->getCollection()->addFieldToFilter('entity_id', array('in'=>$productIds));
}
else {
if($productIds) {
$this->getCollection()->addFieldToFilter('entity_id', array('nin'=>$productIds));
}
}
}
else {
parent::_addColumnFilterToCollection($column);
}
return $this;
}
}
Now add this tab in the list of tabs. In [Namespace]_[Module]_Block_Adminhtml_[Entity]_Edit_Tabs::_beforeToHtml
add this below the main tab.
$this->addTab('products', array(
'label' => Mage::helper('[module]')->__('Associated products'),
'url' => $this->getUrl('*/*/products', array('_current' => true)),
'class' => 'ajax'
));
Now you need the controller actions to handle the products.
Add these methods to the admin controller for you entity:
public function productsAction(){
$this->_initEntity(); //if you don't have such a method then replace it with something that will get you the entity you are editing.
$this->loadLayout();
$this->getLayout()->getBlock('[entity].edit.tab.product')
->set[Entity]Products($this->getRequest()->getPost('[entity]_products', null));
$this->renderLayout();
}
public function productsgridAction(){
$this->_init[Entity]();
$this->loadLayout();
$this->getLayout()->getBlock('[entity].edit.tab.product')
->set[Entity]Products($this->getRequest()->getPost('[entity]_products', null));
$this->renderLayout();
}
Now the layout for these 2 actions. In the admin layout file for your module add these 2 handles.
<adminhtml_[module]_[entity]_products>
<block type="core/text_list" name="root" output="toHtml">
<block type="[module]/adminhtml_[entity]_edit_tab_product" name="[entity].edit.tab.product"/>
<block type="adminhtml/widget_grid_serializer" name="product_grid_serializer">
<reference name="product_grid_serializer">
<action method="initSerializerBlock">
<grid_block_name>[entity].edit.tab.product</grid_block_name>
<data_callback>getSelectedProducts</data_callback>
<hidden_input_name>products</hidden_input_name>
<reload_param_name>[entity]_products</reload_param_name>
</action>
<action method="addColumnInputName">
<input_name>position</input_name>
</action>
</reference>
</block>
</block>
</adminhtml_[module]_[entity]_products>
<adminhtml_[module]_[entity]_productsgrid>
<block type="core/text_list" name="root" output="toHtml">
<block type="[module]/adminhtml_[entity]_edit_tab_product" name="[entity].edit.tab.product"/>
</block>
</adminhtml_[module]_[entity]_productsgrid>
Now saving the data. In the saveAction
of your admin controller add this right before calling $[entity]->save()
$products = $this->getRequest()->getPost('products', -1);
if ($products != -1) {
$[entity]->setProductsData(Mage::helper('adminhtml/js')->decodeGridSerializedInput($products));
}
In your entity model add these methods and a member variable that will process the product relation:
protected $_productInstance = null;
public function getProductInstance(){
if (!$this->_productInstance) {
$this->_productInstance = Mage::getSingleton('[module]/[entity]_product');
}
return $this->_productInstance;
}
protected function _afterSave() {
$this->getProductInstance()->save[Entity]Relation($this);
return parent::_afterSave();
}
public function getSelectedProducts(){
if (!$this->hasSelectedProducts()) {
$products = array();
foreach ($this->getSelectedProductsCollection() as $product) {
$products[] = $product;
}
$this->setSelectedProducts($products);
}
return $this->getData('selected_products');
}
public function getSelectedProductsCollection(){
$collection = $this->getProductInstance()->getProductCollection($this);
return $collection;
}
Now you need the entity-product relation model.
Create [Namespace]/[Module]/Model/[Entity]/Product.php
<?php
class [Namespace]_[Module]_Model_[Entity]_Product
extends Mage_Core_Model_Abstract {
protected function _construct(){
$this->_init('[module]/[entity]_product');
}
public function save[Entity]Relation($[entity]){
$data = $[entity]->getProductsData();
if (!is_null($data)) {
$this->_getResource()->save[Entity]Relation($[entity], $data);
}
return $this;
}
public function getProductCollection($[entity]){
$collection = Mage::getResourceModel('[module]/[entity]_product_collection')
->add[Entity]Filter($[entity]);
return $collection;
}
}
You also need a resource model. [Namespace]/[Module]/Model/Resource/[Entity]/Product.php
<?php
class [Namespace]_[Module]_Model_Resource_[Entity]_Product
extends Mage_Core_Model_Resource_Db_Abstract {
protected function _construct(){
$this->_init('[module]/[entity]_product', 'rel_id');
}
public function save[Entity]Relation($[entity], $data){
if (!is_array($data)) {
$data = array();
}
$deleteCondition = $this->_getWriteAdapter()->quoteInto('[entity]_id=?', $[entity]->getId());
$this->_getWriteAdapter()->delete($this->getMainTable(), $deleteCondition);
foreach ($data as $productId => $info) {
$this->_getWriteAdapter()->insert($this->getMainTable(), array(
'[entity]_id' => $[entity]->getId(),
'product_id' => $productId,
'position' => @$info['position']
));
}
return $this;
}
}
and a collection resource model. I promise that this is the last one. [Namespace]/[Module]/Model/Resource/[Entity]/Product/Collection.php
<?php
class [Namespace]_[Module]_Model_Resource_[Entity]_Product_Collection
extends Mage_Catalog_Model_Resource_Product_Collection {
protected $_joinedFields = false;
public function joinFields(){
if (!$this->_joinedFields){
$this->getSelect()->join(
array('related' => $this->getTable('[module]/[entity]_product')),
'related.product_id = e.entity_id',
array('position')
);
$this->_joinedFields = true;
}
return $this;
}
public function add[Entity]Filter($[entity]){
if ($[entity] instanceof [Namespace]_[Module]_Model_[Entity]){
$[entity] = $[entity]->getId();
}
if (!$this->_joinedFields){
$this->joinFields();
}
$this->getSelect()->where('related.[entity]_id = ?', $[entity]);
return $this;
}
}
All you need to do is to replace the values between []
([Namespace]
, [Module]
, [module]
, ...) with your real values.
You may encounter some errors, because the way you structured your module may be a little different of what I have in mind. But with some debugging and changes you can get it to work. All the heavy lifting is there.
That's it.
Note: The code above was copy/pasted (and renamed the file names) from what was generated with UMC. You can use that to create your full module without having to worry about linking your entity to products. You just say in the UI "Link entity to products:Yes".
This is not spam. The extension is free.
Seem, I found an answer, but it doesn't show that Magento works from the box with it... (in the example "regions" entities not countries as in answer)
Add checkbox column:
/**
* Add columns to grid
*
* @return $this
* @throws Exception
*/
protected function _prepareColumns()
{
$this->addColumn(
'selected_filter',
array(
'type' => 'checkbox',
'align' => 'center',
'index' => 'region_id',
'field_name' => 'selected_regions',
'values' => $this->getSelectedItems(),
)
);
//other your code...
}
Catching selected rows (in grid tab class):
/**
* Set selected items to collection
*
* This method needed to make proper working checkbox filtering
* 'cause it does not work from the box.
*
* @param array $data
* @return $this
*/
protected function _setFilterValues($data)
{
if (isset($data['selected_filter'])) {
$condition = null;
if (1 === (int)$data['selected_filter']) {
$condition = 'in';
} elseif (0 === (int)$data['selected_filter']) {
$condition = 'nin';
}
if ($condition) {
/** @var $column Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Checkbox */
$column = $this->getColumn('selected_filter');
$column->getFilter()->setValue($data['selected_filter']);
$this->getCollection()->addFieldToFilter(
'main_table.region_id',
array(
$condition => $column->getValues()
)
);
}
unset($data['selected_filter']);
}
if ($data) {
parent::_setFilterValues($data);
}
return $this;
}
Get selected items implementation (in grid tab class):
/**
* Get selected regions IDs
*
* @return array
*/
public function getSelectedItems()
{
if ($this->getRequest()->isPost()) {
//get from request on ajax update
return (array)$this->getRequest()->getParam('selected_regions');
}
//here your logic to get IDs from database
return array(1, 2, 3, 4); //example data
}
Adding serializer into a layout update file (added to "left" block):
<!--Serializer for the regions grid-->
<block type="adminhtml/widget_grid_serializer" name="related_grid_serializer">
<action method="initSerializerBlock">
<!--Grid block name in layout-->
<grid_block_name>your_grid_block_name</grid_block_name>
<!--Callback of grid block-->
<data_callback>getSelectedItems</data_callback>
<!--Param name to send to save,
you will get it from $_POST['countries']['regions']
(it means from request object by the same path)-->
<hidden_input_name>countries[regions]</hidden_input_name>
<!--Param name of checkboxes,
it's needed to for ajax requests
and proper work of yes/no/any dropdown-->
<reload_param_name>selected_regions</reload_param_name>
</action>
<!--Add serializer setting form ID if it wasn't rendered within the form-->
<action method="setFormId">
<id>edit_form</id>
</action>
</block>
So, we need to add a bit self code... Please notify me if there is a native approach exists.
Best Answer
By default, Magento allows more than 1 input field. It is only possible if serialized grid is set to Multi-dimensional mode.
--SOLUTION--
It can be achieved in following way:
File:
/Vendor/Module/view/adminhtml/layout/folder_controller_action.xml
--TRACING--
File:
/lib/web/mage/adminhtml/grid.js
In
grid.js
If you noticerowinit
function, it checks the length ofinputs
. If total no. ofinputs
is greater then zero then it will execute afor
loop:Now, this
for
loop is only executed ifmultidimensionalMode
is set totrue
. If you trace back, you will find thatmultidimensionalMode
is set ininitialize
function in following code:So,
multidimensionalMode
useslength
property oninputsToManage
which is theJSON
form of ourinput_names
argument defined in ourlayout xml
file.So, you have to declare
xsi:type
ofinput_names
asarray
inlayout xml
file and assign values to thename
attribute ofitem
starting from zero(as array indices are numbered). This will form aJSON
as below:Hence,
this.multidimensionalMode
will now be set totrue