Question

I tried this

$installer->addAttribute(
    'catalog_category',
    'ser_attr',
    array(
        'input_renderer' => 'namespace_module/block',
        'backend' => 'adminhtml/system_config_backend_serialized_array',
    )
);

and

class namespace_module/block
    extends Mage_Adminhtml_Block_System_Config_Form_Field_Array_Abstract
{
    protected function _prepareToRender()
    {
        $this->addColumn('price', array(
            'label' => 'price'
        ));

        $this->_addAfter = false;

        return $this;
    }
}

But then when I visit edit category page in admin, there's no html/css/js available, only the table with price row.

By the way, I want something like this: http://www.integer-net.com/2015/03/17/how-to-create-tables-in-magento-system-configuration/ and I followed that tutorial, but it uses system.xml, not a setup script.

Was it helpful?

Solution

Unfortunately this doesn't work as easy as you thought (and I'd hoped). The first reason why this doesn't work is because you are extending a class that belongs to the system configuration part of the admin, not to the Catalog part of the admin.

The second reason why this isn't simple is because there is no built-in support for serialized arrays as an attribute for categories. Fortuntately, this is the case for products (notably the group & tier price attributes).

I've followed the logic of these product attributes and put something together that works. Eventually, it'll look like this;

Serialize Array Category Attribute

First, we need to add an attribute to Magento that belongs to a category. I'll assume you know how to do this programatically so for proof of concepts' sake, I'll put the query here. I'm assuming your extension is called Your_Module.

INSERT INTO eav_attribute SET entity_type_id = 3, attribute_code = 'serialize_array', backend_model = 'your_module/category_attribute_backend_serializearray', backend_type = 'text', frontend_input = 'text', frontend_label = 'Serialize Array';

Then find the attribute ID for this attribute and run this query;

INSERT INTO eav_entity_attribute SET entity_type_id = 3, attribute_set_id = 3, attribute_group_id = 4, attribute_id = ATTRIBUTE_ID_HERE, sort_order = 100;

Now we'll have a nice attribute on the category page. But it isn't configurable like you'd like, it's still a normal text field.

We'll now create the logic needed to show, store and retrieve the serialized array.

First, we'll need to create the backend model. We need this because we need to serialize the array when its being saved by using the beforeSave() function.

<?php

class Your_Module_Model_Category_Attribute_Backend_Serializearray
    extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract
{
    public function beforeSave($object)
    {
        $object->setData('serialize_array', serialize($object->getData('serialize_array')));
    }
}

Next, we'll need to create a form renderer to show the HTML that will make up the select elements and the table.

<?php

class Your_Module_Block_Catalog_Form_Renderer_Attribute_SerializeArray
    extends Mage_Adminhtml_Block_Catalog_Form_Renderer_Fieldset_Element
{
    public function __construct()
    {
        $this->setTemplate('your/module/serialize_array.phtml');
    }

    public function getOptions() {
        return array(
            array('name' => 'Option 1'),
            array('name' => 'Option 2')
        );
    }

    public function getOtherOptions() {
        return array('Other Option 1', 'Other Option 2');
    }

    public function getValues() {
        $_category = Mage::registry('current_category');
        $array = $_category->getSerializeArray();
        if($array) {
            return unserialize($array);
        }
        return array();
    }

    public function getElementHtml()
    {
        $element = $this->getElement();
        if(!$element->getValue()) {
            return parent::getElementHtml();
        }
        $element->setOnkeyup("onUrlkeyChanged('" . $element->getHtmlId() . "')");
        $element->setOnchange("onUrlkeyChanged('" . $element->getHtmlId() . "')");

        $data = array(
            'name' => $element->getData('name') . '_create_redirect',
            'disabled' => true,
        );
        $hidden =  new Varien_Data_Form_Element_Hidden($data);
        $hidden->setForm($element->getForm());

        $storeId = $element->getForm()->getDataObject()->getStoreId();
        $data['html_id'] = $element->getHtmlId() . '_create_redirect';
        $data['label'] = Mage::helper('catalog')->__('Create Permanent Redirect for old URL');
        $data['value'] = $element->getValue();
        $data['checked'] = Mage::helper('catalog')->shouldSaveUrlRewritesHistory($storeId);
        $checkbox = new Varien_Data_Form_Element_Checkbox($data);
        $checkbox->setForm($element->getForm());

        return parent::getElementHtml() . '<br/>' . $hidden->getElementHtml() . $checkbox->getElementHtml() . $checkbox->getLabelHtml();
    }

    protected function _prepareLayout()
    {
        $button = $this->getLayout()->createBlock('adminhtml/widget_button')
            ->setData(array(
                'label' => Mage::helper('catalog')->__('Add Option'),
                'onclick' => 'return serializeArrayControl.addItem()',
                'class' => 'add'
            ));
        $button->setName('add_serialize_array_item_button');

        $this->setChild('add_button', $button);
        return parent::_prepareLayout();
    }

    public function getAddButtonHtml()
    {
        return $this->getChildHtml('add_button');
    }

}

We'll also need the actual HTML. Create the template file

app/design/adminhtml/base/default/template/your/module/serialize_array.phtml

with this HTML;

<?php
/** @var $this Your_Module_Block_Catalog_Form_Renderer_Attribute_SerializeArray */
$_htmlId = $this->getElement()->getHtmlId();
$_htmlClass = $this->getElement()->getClass();
$_htmlName = $this->getElement()->getName();
$_readonly = $this->getElement()->getReadonly();

$_showFirstColumn = true;
?>
<tr>
    <td class="label"><?php echo $this->getElement()->getLabel(); ?></td>
    <td colspan="10" class="grid tier">
        <table cellspacing="0" class="data border" id="serialize_array_table">
            <?php if ($_showFirstColumn) : ?>
                <col width="135" />
            <?php endif; ?>
            <col width="120" />
            <col />
            <col width="1" />
            <thead>
            <tr class="headings">
                <th <?php if (!$_showFirstColumn): ?>style="display: none;"<?php endif; ?>><?php echo Mage::helper('catalog')->__('First Option'); ?></th>
                <th><?php echo Mage::helper('catalog')->__('Second Option'); ?></th>
                <th><?php echo Mage::helper('catalog')->__('Free field'); ?></th>
                <th class="last"><?php echo Mage::helper('catalog')->__('Action'); ?></th>
            </tr>
            </thead>
            <tbody id="<?php echo $_htmlId; ?>_container"></tbody>
            <tfoot>
            <tr>
                <td <?php if (!$_showFirstColumn): ?>style="display: none;"<?php endif; ?>></td>
                <td colspan="4" class="a-right"><?php echo $this->getAddButtonHtml(); ?></td>
            </tr>
            </tfoot>
        </table>

        <script type="text/javascript">
            //<![CDATA[
            var serializeArrayRowTemplate = '<tr>'
                + '<td<?php if (!$_showFirstColumn): ?> style="display:none"<?php endif; ?>>'
                + '<select class="<?php echo $_htmlClass; ?> required-entry" name="<?php echo $_htmlName; ?>[{{index}}][first_option]" id="serialize_array_row_{{index}}_first_option">'
                <?php foreach ($this->getOptions() as $_websiteId => $_info) : ?>
                + '<option value="<?php echo $_websiteId; ?>"><?php echo $this->jsQuoteEscape($this->escapeHtml($_info['name'])); ?><?php if (!empty($_info['currency'])) : ?> [<?php echo $this->escapeHtml($_info['currency']); ?>]<?php endif; ?></option>'
                <?php endforeach; ?>
                + '</select></td>'
                + '<td><select class="<?php echo $_htmlClass; ?> custgroup required-entry" name="<?php echo $_htmlName; ?>[{{index}}][second_option]" id="serialize_array_row_{{index}}_second_option">'
                <?php foreach ($this->getOtherOptions() as $_groupId => $_groupName): ?>
                + '<option value="<?php echo $_groupId; ?>"><?php echo $this->jsQuoteEscape($this->escapeHtml($_groupName)); ?></option>'
                <?php endforeach; ?>
                + '</select></td>'
                + '<td><input class="<?php echo $_htmlClass; ?> required-entry" type="text" name="<?php echo $_htmlName; ?>[{{index}}][freefield]" value="{{freefield}}" id="serialize_array_row_{{index}}_freefield" /></td>'
                + '<td class="last"><input type="hidden" name="<?php echo $_htmlName; ?>[{{index}}][delete]" class="delete" value="" id="serialize_array_row_{{index}}_delete" />'
                + '<button title="<?php echo $this->jsQuoteEscape(Mage::helper('catalog')->__('Delete')); ?>" type="button" class="scalable delete icon-btn delete-product-option" id="serialize_array_row_{{index}}_delete_button" onclick="return serializeArrayControl.deleteItem(event);">'
                + '<span><?php echo $this->jsQuoteEscape(Mage::helper('catalog')->__('Delete')); ?></span></button></td>'
                + '</tr>';

            var serializeArrayControl = {
                template: new Template(serializeArrayRowTemplate, new RegExp('(^|.|\\r|\\n)({{\\s*(\\w+)\\s*}})', '')),
                itemsCount: 0,
                addItem : function () {
                    <?php if ($_readonly): ?>
                    if (arguments.length < 3) {
                        return;
                    }
                    <?php endif; ?>
                    var data = {
                        first_option: '', // default value
                        second_option: '', // default value
                        freefield: '', // default value
                        readOnly: false,
                        index: this.itemsCount++
                    };

                    if(arguments.length >= 3) {
                        data.first_option = arguments[0];
                        data.second_option = arguments[1];
                        data.freefield = arguments[2];
                    }
                    if (arguments.length == 4) {
                        data.readOnly = arguments[3];
                    }

                    Element.insert($('<?php echo $_htmlId; ?>_container'), {
                        bottom : this.template.evaluate(data)
                    });

                    $('serialize_array_row_' + data.index + '_first_option').setValue(data.first_option);
                    $('serialize_array_row_' + data.index + '_second_option').setValue(data.second_option);
                    $('serialize_array_row_' + data.index + '_freefield').setValue(data.freefield);

                    if (data.readOnly == '1') {
                        ['first_option', 'second_option', 'freefield', 'delete'].each(function(element_suffix) {
                            $('serialize_array_row_' + data.index + '_' + element_suffix).disabled = true;
                        });
                        $('serialize_array_row_' + data.index + '_delete_button').hide();
                    }

                    <?php if ($_readonly): ?>
                    $('<?php echo $_htmlId; ?>_container').select('input', 'select').each(this.disableElement);
                    $('<?php echo $_htmlId; ?>_container').up('table').select('button').each(this.disableElement);
                    <?php else: ?>
                    $('<?php echo $_htmlId; ?>_container').select('input', 'select').each(function(element) {
                        Event.observe(element, 'change', element.setHasChanges.bind(element));
                    });
                    <?php endif; ?>
                },
                disableElement: function(element) {
                    element.disabled = true;
                    element.addClassName('disabled');
                },
                deleteItem: function(event) {
                    var tr = Event.findElement(event, 'tr');
                    if (tr) {
                        Element.select(tr, '.delete').each(function(element) {
                            element.value='1';
                        });
                        Element.select(tr, ['input', 'select']).each(function(element) {
                            element.hide();
                        });
                        Element.hide(tr);
                        Element.addClassName(tr, 'no-display template');
                    }
                    return false;
                }
            };
            <?php foreach ($this->getValues() as $_item) : ?>
            serializeArrayControl.addItem('<?php echo $_item['first_option']; ?>', '<?php echo $_item['second_option']; ?>', '<?php echo $_item['freefield']; ?>');
            <?php endforeach; ?>
            <?php if ($_readonly) : ?>
            $('<?php echo $_htmlId; ?>_container').up('table').select('button')
                .each(serializeArrayControl.disableElement);
            <?php endif; ?>
            //]]>
        </script>
    </td></tr>

There might be some useless/nonsensical cruft in this HTML since I copied it from

app/design/adminhtml/default/default/template/catalog/product/edit/price/group.phtml

and adjusted it a bit.

Clear your cache, reindex (if you're using catalog flat tables) and you should be good to go!

Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top