Magento – Best way to add a dynamic grid as input in custom extension Adminhtml

adminhtmlextensionsmagento-1.7

I've build a custom store locator extension with it's own grid and edit pages in the Adminhtml and everything works great. For the opening hours of the stores I would like to implement a dynamic grid like for the attribute options.

enter image description here

Now I've found a solution but I'm hoping there is a better, or at least cleaner way.
What I have so far is adding a renderer to the field in the form fieldset

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', 'editor', array(
            'name'      => 'office_hours',
            'label'     => Mage::helper('rkstorelocator')->__('Office hours'),
            'required'  => false,
        ));

        $officehours_block = $this->getLayout()
                                ->createBlock('rkstorelocator/adminhtml_rkstorelocator_edit_renderer_officehours')
                                ->setData(array(
                                    'name'      => 'office_hours',
                                    'label'     => Mage::helper('rkstorelocator')->__('Office hours'),
                                    'required'  => false,
                                ));

        $officehours_field->setRenderer($officehours_block);

        [...]
    }
}

And a block class to render

class Redkiwi_Rkstorelocator_Block_Adminhtml_Rkstorelocator_Edit_Renderer_Officehours
 extends Mage_Adminhtml_Block_Abstract 
 implements Varien_Data_Form_Element_Renderer_Interface 
{

    public function render(Varien_Data_Form_Element_Abstract $element) 
    {
        $required_indicator = $this->getData('required') ? '<span class="required">*</span>' : '' ;

        $html = '
<table id="attribute-options-table" class="dynamic-grid rkstorelocator-officehours" cellspacing="0" cellpadding="0"><tbody>
    <tr>
        <th>Day indicator</th>
        <th>Opening hour</th>
        <th>Closing hour</th>
        <th>
            <button id="add_new_option_button" title="Add Option" type="button" class="scalable add"><span><span><span>Add Option</span></span></span></button>
        </th>
    </tr>
</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="'.$this->getData('name').'[value][option_{{id}}][0]" value="" class="input-text required-option" type="text"></td><td><input name="'.$this->getData('name').'[value][option_{{id}}][2]" value="" class="input-text required-option" type="text"></td><td><input name="'.$this->getData('name').'[value][option_{{id}}][2]" value="" class="input-text required-option" type="text"></td><td class="a-left" id="delete_button_container_option_{{id}}"><input type="hidden" class="delete-flag" name="'.$this->getData('name').'[delete][option_{{id}}]" value=""/><button onclick="$(\\\'hour-row-{{id}}\\\').remove();" title="Delete" type="button" class="scalable delete delete-option"><span><span><span>Delete</span></span></span></button></td></tr>\';

var _rkstorelocator_counter = 0;

$(\'add_new_option_button\').on(\'click\', \'button\', function(){
    $(\'attribute-options-table\').insert(_form_html_row.replace(/\{\{id\}\}/ig, _rkstorelocator_counter));
    _rkstorelocator_counter++;
});

//]]></script>
        ';
        return $html;
    }

}

Which gives me the following result
enter image description here

Now this basically works but getting the current values in there will be quite messy and all in all I'm not too proud of the code I've written (as you might imagine).

I've Googled for several solutions but all generally take this approach. Does anyone know a cleaner way to do this?

Best Answer

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: enter image description here

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. :)

Related Topic