Best way to add a dynamic grid as input in custom extension Adminhtml
It took me way to long to realize that Tier prices looks the way I want. So after looking into how Magento does it with Tier pricing I ended up doing the following. Sorry in advance for the huge blocks of code but I thought it might be interesting for future reference.
In my form class Redkiwi_Rkstorelocator_Block_Adminhtml_Rkstorelocator_Edit_Tab_General
class Redkiwi_Rkstorelocator_Block_Adminhtml_Rkstorelocator_Edit_Tab_General extends Mage_Adminhtml_Block_Widget_Form
{
protected function _prepareForm()
{
$form = new Varien_Data_Form();
$this->setForm($form);
$fieldset = $form->addFieldset('rkstorelocator_form', array('legend'=>Mage::helper('rkstorelocator')->__('Store information')));
[...]
$officehours_field = $fieldset->addField('office_hours', 'text', array(
'name' => 'office_hours',
'label' => Mage::helper('rkstorelocator')->__('Office hours'),
'required' => false,
));
$office_hours = $form->getElement('office_hours');
$office_hours->setRenderer(
$this->getLayout()->createBlock('rkstorelocator/adminhtml_rkstorelocator_edit_renderer_officehours')
);
[...]
}
}
Now for the Office hours block class Redkiwi_Rkstorelocator_Block_Adminhtml_Rkstorelocator_Edit_Renderer_Officehours
.
class Redkiwi_Rkstorelocator_Block_Adminhtml_Rkstorelocator_Edit_Renderer_Officehours
extends Mage_Adminhtml_Block_Widget
implements Varien_Data_Form_Element_Renderer_Interface
{
/**
* Initialize block
*/
public function __construct()
{
$this->setTemplate('rkstorelocator/officehours.phtml');
}
/**
* Render HTML
*
* @param Varien_Data_Form_Element_Abstract $element
* @return string
*/
public function render(Varien_Data_Form_Element_Abstract $element)
{
$this->setElement($element);
return $this->toHtml();
}
}
And the template .phtml file adminhtml/default/default/template/rkstorelocator/officehours.phtml
<?php
$_htmlId = $this->getElement()->getHtmlId();
$_htmlClass = $this->getElement()->getClass();
$_htmlName = $this->getElement()->getName();
$_readonly = $this->getElement()->getReadonly();
$collection = Mage::registry('rkstorelocator_data')
->getOpeningHours()
->setOrder('sortorder', 'ASC');
$_counter = 0;
?>
<tr>
<td class="label"><?php echo $this->getElement()->getLabel() ?></td>
<td colspan="10" class="grid hours">
<table id="attribute-options-table" class="dynamic-grid rkstorelocator-officehours" cellspacing="0" cellpadding="0"><tbody>
<tr>
<th><?php echo $this->__('Day label') ?></th><th><?php echo $this->__('Opening hour') ?></th><th><?php echo $this->__('Closing hour') ?></th><th><?php echo $this->__('Sortorder') ?></th>
<th><button id="add_new_option_button" title="Add Option" type="button" class="scalable add"><span><span><span><?php echo $this->__('Add Option') ?></span></span></span></button></th>
</tr>
<?php foreach ($collection as $_item): ?>
<tr class="option-row rkstorelocator-officehours-dayrow" id="hour-row-<?php echo $_counter?>">
<td><input name="<?php echo $_htmlName; ?>[value][option_<?php echo $_counter ?>][dayindicator]" value="<?php echo $_item->getDayindicator() ?>" class="input-text" type="text"></td>
<td><input name="<?php echo $_htmlName; ?>[value][option_<?php echo $_counter ?>][openinghour]" value="<?php echo $_item->getOpeninghour() ?>" class="input-text" type="text"></td>
<td><input name="<?php echo $_htmlName; ?>[value][option_<?php echo $_counter ?>][closinghour]" value="<?php echo $_item->getClosinghour() ?>" class="input-text" type="text"></td>
<td><input name="<?php echo $_htmlName; ?>[value][option_<?php echo $_counter ?>][sortorder]" value="<?php echo $_item->getSortorder() ?>" class="input-text" type="text"></td>
<td class="a-left" id="delete_button_container_option_<?php echo $_counter ?>'">
<input name="<?php echo $_htmlName; ?>[value][option_<?php echo $_counter ?>][id]" value="<?php echo $_item->getId() ?>" type="hidden">
<input id="delete-row-<?php echo $_counter ?>" type="hidden" class="delete-flag" name="<?php echo $_htmlName; ?>[delete][option_<?php echo $_counter ?>]" value=""/>
<button onclick="$('hour-row-<?php echo $_counter ?>').style.display='none'; $('delete-row-<?php echo $_counter ?>').setValue(1);" title="Delete" type="button" class="scalable delete delete-option"><span><span><span>Delete</span></span></span></button>
</td>
</tr>
<?php
$_counter++;
endforeach;
?>
</tbody></table>
<script type="text/javascript">//<![CDATA[
var _form_html_row = '<tr class="option-row rkstorelocator-officehours-dayrow" id="hour-row-{{id}}"><td><input name="<?php echo $_htmlName; ?>[value][option_{{id}}][dayindicator]" value="" class="input-text" type="text"></td><td><input name="<?php echo $_htmlName; ?>[value][option_{{id}}][openinghour]" value="" class="input-text" type="text"></td><td><input name="<?php echo $_htmlName; ?>[value][option_{{id}}][closinghour]" value="" class="input-text" type="text"></td><td><input name="<?php echo $_htmlName; ?>[value][option_{{id}}][sortorder]" value="" class="input-text" type="text"></td><td class="a-left" id="delete_button_container_option_{{id}}"><input name="<?php echo $_htmlName; ?>[value][option_{{id}}][id]" value="" type="hidden"><input id="delete-row-{{id}}" type="hidden" class="delete-flag" name="<?php echo $_htmlName; ?>[delete][option_{{id}}]" value=""/><button onclick="$(\'hour-row-{{id}}\').style.display=\'none\'; $(\'delete-row-{{id}}\').setValue(1);" title="Delete" type="button" class="scalable delete delete-option"><span><span><span>Delete</span></span></span></button></td></tr>';
var _rkstorelocator_counter = <?php echo $_counter?>;
$('add_new_option_button').observe('click', function(){
$('attribute-options-table').insert(_form_html_row.replace(/\{\{id\}\}/ig, _rkstorelocator_counter));
_rkstorelocator_counter++;
});
//]]></script>
</td>
</tr>
And the result:
Dear future Googlers, By the time you read this Magento 2.x will be released. Let's hope Magento has made this kind of stuff a little bit easier. :)
I give some of my codes written based on Magento templates. Maybe it will be useful.
Some tab interface:
<?php
class Ssd_Shower_Block_Adminhtml_Shower_Edit_Tab_Options
extends Mage_Adminhtml_Block_Template
implements Mage_Adminhtml_Block_Widget_Tab_Interface
{
/** set own teplate */
public function __construct()
{
$this->setTemplate('pregnancy/list/options.phtml');
}
/** here some implementation of tab interfeys */
/** options for every row, they will be rendered as dynamic row with inputs */
public function getOptionValues()
{
$period=$this->getData('period');
$optionsArr = Mage::helper('shower')->getTipList($period);
$values = array();
foreach ($optionsArr as $option) {
$value = array();
$value['id'] = $option->getId();
$value['period_id'] = $period->getId();
$value['tip_content'] = $option->getTip_content();
$value['sort_order'] = $option->getSort_order();
$value['update'] = 1;
$values[] = new Varien_Object($value);
}
return $values;
}
}
?>
And pregnancy/list/options.phtml template:
<div class="entity-edit" id="manage-options-panel">
<div class="entry-edit-head">
<h4 class="icon-head head-edit-form fieldset-legend">Some title</h4>
</div>
<div class="box">
<div class="hor-scroll">
<table class="dynamic-grid" cellspacing="0" cellpadding="0" width="100%">
<tr id="grid_head">
<th style="width:90%!important"><?php echo Mage::helper('pregnancy')->__('Checklist Items') ?></th>
<th class="w-150"><?php echo Mage::helper('pregnancy')->__('Position') ?></th>
<th class="w-150">
<button id="add_new_option_button" class="scalable add" style="" onclick="" type="button">
<span><?php echo Mage::helper('pregnancy')->__('Add Checklist Item') ?></span>
</button>
</th>
</tr>
<tr id="attribute-options-table">
</tr>
<tr class="no-display template" id="row-template">
<td><input name="tip[{{id}}][tip_content]"
value="{{tip_content}}"
class="input-text required-option full"
type="text" disabled="disabled"/></td>
<td class="a-center"><input class="input-text" type="text" name="tip[{{id}}][sort_order]"
value="{{sort_order}}"/></td>
<td class="a-left">
<input type="hidden" class="delete-flag" name="tip[{{id}}][delete]" value=""/>
<input type="hidden" class="update-flag" name="tip[{{id}}][update]" value="{{update}}"/>
<button class="scalable delete delete-option" type="button"><span>Delete</span></button>
</td>
</tr>
</table>
</div>
<input type="hidden" id="option-count-check" value=""/>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
var optionDefaultInputType = 'text';
//template for dynamic row
var templateText =
'<tr class="option-row">' +
'<td><input name="tip[{{id}}][tip_content]" value="{{tip_content}}" class="input-text required-option full" type="text"/><\/td>' +
'<td><input class="input-text" type="text" name="tip[{{id}}][sort_order]" value="{{sort_order}}"/><\/td>' +
'<td class="a-left">' +
'<input type="hidden" class="delete-flag" name="tip[{{id}}][delete]" value="" />' +
'<input type="hidden" class="update-flag" name="tip[{{id}}][update]" value="{{update}}"/>' +
'<button class="scalable delete delete-option" type="button"><span><?=$this->__("Delete")?></span></button>' +
'<\/td>' +
'<\/tr>';
var attributeOption = {
table : $('attribute-options-table'),
templateSyntax : /(^|.|\r|\n)({{(\w+)}})/,
templateText : templateText,
itemCount : 0,
totalItems : 0,
//add dynamic row function
add : function(data) {
this.template = new Template(this.templateText, this.templateSyntax);
if (!data.id) {
data = {};
data.id = 'option_' + this.itemCount;
}
if (!data.intype)
data.intype = optionDefaultInputType;
Element.insert(this.table, {before: this.template.evaluate(data)});
this.bindRemoveButtons();
this.itemCount++;
this.totalItems++;
this.updateItemsCountField();
},
//remove dynamic row function
remove : function(event) {
if (confirm('<?php echo $this->__("Do you really delete this tip?");?>')) {
var element = $(Event.findElement(event, 'tr'));
element.ancestors().each(function(parentItem) {
if (parentItem.hasClassName('option-row')) {
element = parentItem;
throw $break;
} else if (parentItem.hasClassName('box')) {
throw $break;
}
});
if (element) {
var elementFlags = element.getElementsByClassName('delete-flag');
if (elementFlags[0]) {
elementFlags[0].value = 1;
}
element.addClassName('no-display');
element.addClassName('template');
element.hide();
this.totalItems--;
this.updateItemsCountField();
}
}
},
updateItemsCountField: function() {
if (this.totalItems > 0) {
$('option-count-check').value = '1';
} else {
$('option-count-check').value = '';
}
},
bindRemoveButtons : function() {
var buttons = $$('.delete-option');
for (var i = 0; i < buttons.length; i++) {
if (!$(buttons[i]).binded) {
$(buttons[i]).binded = true;
Event.observe(buttons[i], 'click', this.remove.bind(this));
}
}
}
}
if ($('row-template')) {
$('row-template').remove();
}
attributeOption.bindRemoveButtons();
if ($('add_new_option_button')) {
Event.observe('add_new_option_button', 'click', attributeOption.add.bind(attributeOption));
}
Validation.addAllThese([
['required-option', '<?php echo Mage::helper('pregnancy')->__('Failed') ?>', function(v) {
return !Validation.get('IsEmpty').test(v);
}]
]);
Validation.addAllThese([
['required-options-count', '<?php echo Mage::helper('pregnancy')->__('Options is required') ?>', function(v) {
return !Validation.get('IsEmpty').test(v);
}]
]);
<?php
/** pulling data from Ssd_Shower_Block_Adminhtml_Shower_Edit_Tab_Options **/
if ($options = $this->getOptionValues()) {
foreach ($options as $_value): ?>
attributeOption.add(<?php echo $_value->toJson() ?>);
<?php endforeach; } ?>
//]]>
</script>