23

I am trying to add Category tree to custom extention, the category tree which is in one of the tab of edit product page

2 Answers 2

27

Prepare yourself, this is going to be a long one. Here goes.
You will need the following files:

app/code/local/[Namespace]/[Module]/Block/Adminhtml/[Entity]/Edit/Tab/Categories.php - the tab that will render the categories.

<?php
class [Namespace]_[Module]_Block_Adminhtml_[Entity]_Edit_Tab_Categories
    extends Mage_Adminhtml_Block_Catalog_Category_Tree {
    protected $_categoryIds = null;
    protected $_selectedNodes = null;
    public function __construct() {
        parent::__construct();
        $this->setTemplate('[namespace]_module/[entity]/edit/tab/categories.phtml');
        $this->_withProductCount = false;
    }
    public function get[Entity](){
        return Mage::registry('current_[entity]'); //use other registration key if you have one
    }

    public function getCategoryIds(){
        if (is_null($this->_categoryIds)){
            $categories = $this->get[Entity]()->getSelectedCategories();
                $ids = array();
                foreach ($categories as $category){
                    $ids[] = $category->getId();
                }
                $this->_categoryIds = $ids;
        }
        return $this->_categoryIds;
    }
    public function getIdsString(){
        return implode(',', $this->getCategoryIds());
    }
    public function getRootNode(){
        $root = $this->getRoot();
        if ($root && in_array($root->getId(), $this->getCategoryIds())) {
            $root->setChecked(true);
        }
        return $root;
    }

    public function getRoot($parentNodeCategory = null, $recursionLevel = 3){
        if (!is_null($parentNodeCategory) && $parentNodeCategory->getId()) {
            return $this->getNode($parentNodeCategory, $recursionLevel);
        }
        $root = Mage::registry('category_root');
        if (is_null($root)) {
            $rootId = Mage_Catalog_Model_Category::TREE_ROOT_ID;
            $ids = $this->getSelectedCategoryPathIds($rootId);
            $tree = Mage::getResourceSingleton('catalog/category_tree')
                ->loadByIds($ids, false, false);
            if ($this->getCategory()) {
                $tree->loadEnsuredNodes($this->getCategory(), $tree->getNodeById($rootId));
            }
            $tree->addCollectionData($this->getCategoryCollection());
            $root = $tree->getNodeById($rootId);
            Mage::register('category_root', $root);
        }
        return $root;
    }
    protected function _getNodeJson($node, $level = 1){
        $item = parent::_getNodeJson($node, $level);
        if ($this->_isParentSelectedCategory($node)) {
            $item['expanded'] = true;
        }
        if (in_array($node->getId(), $this->getCategoryIds())) {
            $item['checked'] = true;
        }
        return $item;
    }
    protected function _isParentSelectedCategory($node){
        $result = false;
        // Contains string with all category IDs of children (not exactly direct) of the node
        $allChildren = $node->getAllChildren();
        if ($allChildren) {
            $selectedCategoryIds = $this->getCategoryIds();
            $allChildrenArr = explode(',', $allChildren);
            for ($i = 0, $cnt = count($selectedCategoryIds); $i < $cnt; $i++) {
                $isSelf = $node->getId() == $selectedCategoryIds[$i];
                if (!$isSelf && in_array($selectedCategoryIds[$i], $allChildrenArr)) {
                    $result = true;
                    break;
                }
            }
        }
        return $result;
    }
    protected function _getSelectedNodes(){
        if ($this->_selectedNodes === null) {
            $this->_selectedNodes = array();
            $root = $this->getRoot();
            foreach ($this->getCategoryIds() as $categoryId) {
                if ($root) {
                    $this->_selectedNodes[] = $root->getTree()->getNodeById($categoryId);
                }
            }
        }
        return $this->_selectedNodes;
    }

    public function getCategoryChildrenJson($categoryId){
        $category = Mage::getModel('catalog/category')->load($categoryId);
        $node = $this->getRoot($category, 1)->getTree()->getNodeById($categoryId);
        if (!$node || !$node->hasChildren()) {
            return '[]';
        }
        $children = array();
        foreach ($node->getChildren() as $child) {
            $children[] = $this->_getNodeJson($child);
        }
        return Mage::helper('core')->jsonEncode($children);
    }
    public function getLoadTreeUrl($expanded = null){
        return $this->getUrl('*/*/categoriesJson', array('_current' => true));
    }
    public function getSelectedCategoryPathIds($rootId = false){
        $ids = array();
        $categoryIds = $this->getCategoryIds();
        if (empty($categoryIds)) {
            return array();
        }
        $collection = Mage::getResourceModel('catalog/category_collection');
        if ($rootId) {
            $collection->addFieldToFilter('parent_id', $rootId);
        }
        else {
            $collection->addFieldToFilter('entity_id', array('in'=>$categoryIds));
        }

        foreach ($collection as $item) {
            if ($rootId && !in_array($rootId, $item->getPathIds())) {
                continue;
            }
            foreach ($item->getPathIds() as $id) {
                if (!in_array($id, $ids)) {
                    $ids[] = $id;
                }
            }
        }
        return $ids;
    }
}

app/design/adminhtml/default/default/[namespace]_[module]/[entity]/tab/edit/categories.phtml - the template needed to render the categories

<div class="entry-edit">
    <div class="entry-edit-head">
        <h4 class="icon-head head-edit-form fieldset-legend">
            <?php echo Mage::helper('[module]')->__('Categories') ?>
        </h4>
    </div>
    <fieldset id="grop_fields">
        <input type="hidden" name="category_ids" id="[entity]_categories" value="<?php echo $this->getIdsString() ?>">
        <div id="[entity]-categories" class="tree"></div>
    </fieldset>
</div>
<?php if($this->getRootNode() && $this->getRootNode()->hasChildren()): ?>
<script type="text/javascript">
    Ext.EventManager.onDocumentReady(function() {
        var categoryLoader = new Ext.tree.TreeLoader({
           dataUrl: '<?php echo $this->getLoadTreeUrl()?>'
        });
        categoryLoader.createNode = function(config) {
            config.uiProvider = Ext.tree.CheckboxNodeUI;
            var node;
            if (config.children && !config.children.length) {
                delete(config.children);
                node = new Ext.tree.AsyncTreeNode(config);
            }
            else {
                node = new Ext.tree.TreeNode(config);
            }
            return node;
        };
        categoryLoader.on("beforeload", function(treeLoader, node) {
            treeLoader.baseParams.category = node.attributes.id;
        });

        categoryLoader.on("load", function(treeLoader, node, config) {
            varienWindowOnload();
        });
        var tree = new Ext.tree.TreePanel('[entity]-categories', {
            animate:true,
            loader: categoryLoader,
            enableDD:false,
            containerScroll: true,
            rootUIProvider: Ext.tree.CheckboxNodeUI,
            selModel: new Ext.tree.CheckNodeMultiSelectionModel(),
            rootVisible: '<?php echo $this->getRootNode()->getIsVisible() ?>'
        });
        tree.on('check', function(node) {
            if(node.attributes.checked) {
                categoryAdd(node.id);
            } else {
                categoryRemove(node.id);
            }
            varienElementMethods.setHasChanges(node.getUI().checkbox);
        }, tree);
        var root = new Ext.tree.TreeNode({
            text: '<?php echo $this->jsQuoteEscape($this->getRootNode()->getName()) ?>',
            draggable:false,
            checked:'<?php echo $this->getRootNode()->getChecked() ?>',
            id:'<?php echo $this->getRootNode()->getId() ?>',
            disabled: <?php echo ($this->getRootNode()->getDisabled() ? 'true' : 'false') ?>,
            uiProvider: Ext.tree.CheckboxNodeUI
        });
        tree.setRootNode(root);
        bildCategoryTree(root, <?php echo $this->getTreeJson() ?>);
        tree.addListener('click', categoryClick.createDelegate(this));
        tree.render();
        root.expand();
    });
    function bildCategoryTree(parent, config){
        if (!config) {
            return null;
        }
        if (parent && config && config.length){
            for (var i = 0; i < config.length; i++){
                config[i].uiProvider = Ext.tree.CheckboxNodeUI;
                var node;
                var _node = Object.clone(config[i]);
                if (_node.children && !_node.children.length) {
                    delete(_node.children);
                    node = new Ext.tree.AsyncTreeNode(_node);

                }
                else {
                    node = new Ext.tree.TreeNode(config[i]);
                }
                parent.appendChild(node);
                node.loader = node.getOwnerTree().loader;
                if(config[i].children){
                    bildCategoryTree(node, config[i].children);
                }
            }
        }
    }
    function categoryClick(node, e){
        if (node.disabled) {
            return;
        }
        node.getUI().check(!node.getUI().checked());
        varienElementMethods.setHasChanges(Event.element(e), e);
    };
    function categoryAdd(id) {
        var ids = $('[entity]_categories').value.split(',');
        ids.push(id);
        $('[entity]_categories').value = ids.join(',');
    }
    function categoryRemove(id) {
        var ids = $('[entity]_categories').value.split(',');
        while (-1 != ids.indexOf(id)) {
            ids.splice(ids.indexOf(id), 1);
        }
        $('[entity]_categories').value = ids.join(',');
    }
</script>
<?php endif; ?>

In your form file where you add the tabs of your custom entity add this also:

    $this->addTab('categories', array(
        'label' => Mage::helper('[module]')->__('Associated categories'),
        'url'   => $this->getUrl('*/*/categories', array('_current' => true)),
        'class'    => 'ajax'
    ));

In the admin controller of your custom entity these 2 actions that will handled the requests for categories:

public function categoriesAction(){
    $this->_init[Entity]();
    $this->loadLayout();
    $this->renderLayout();
}
public function categoriesJsonAction(){
    $this->_init[Entity]();
    $this->getResponse()->setBody(
        $this->getLayout()->createBlock('[module]/adminhtml_[entity]_edit_tab_categories')
            ->getCategoryChildrenJson($this->getRequest()->getParam('category'))
    );
}

and make sure that in the same controller this method exists:

protected function _init[Entity](){
    $[entity]Id  = (int) $this->getRequest()->getParam('id');
    $[enity]    = Mage::getModel('[module]/[entity]');

    if ($[entity]Id) {
        $[entity]->load($[entity]Id);
    }
    Mage::register('current_[entity]', $[entity]);
    return $[entity];
}

In the admin layout file of your module add this handle for the categories action:

<adminhtml_[module]_[entity]_categories>
    <block type="core/text_list" name="root" output="toHtml">
        <block type="[module]/adminhtml_[entity]_edit_tab_categories" name="[entity].edit.tab.categories"/>
    </block>
</adminhtml_[module]_[entity]_categories>

Now let's proceed to the saving of your data.
For this you will need the following in one of your module's install/upgrade scripts. This will create a table where the linked values will be stored

$table = $this->getConnection()
    ->newTable($this->getTable('[module]/[entity]_category'))
    ->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('category_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
    ), 'Category ID')
    ->addColumn('position', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'nullable'  => false,
        'default'   => '0',
    ), 'Position')
    ->addIndex($this->getIdxName('[module]/[entity]_category', array('category_id')), array('category_id'))
    ->addForeignKey($this->getFkName('[module]/[entity]_category', '[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]_category', 'category_id', 'catalog/category', 'entity_id'),    'category_id', $this->getTable('catalog/category'), 'entity_id', Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addIndex(
    $this->getIdxName(
        '[module]/[entity]_category',
        array('[entity]_id', 'category_id'),
        Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE
    ),
    array('[entity]_id', 'category_id'),
    array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE))
    ->setComment('[Entity] to Category Linkage Table');
$this->getConnection()->createTable($table);

Declare your table. Add this in config.xml inside the <[module]_resource><entities> tag

<[entity]_category>
    <table>[module]_[entity]_category</table>
</[entity]_category>

You will need a model for linking to categories:
app/code/local/[Namespace]/[Module]/Model/[Entity]/Category.php

<?php

class [Namespace]_[Module]_Model_[Entity]_Category
    extends Mage_Core_Model_Abstract {
    protected function _construct(){
        $this->_init('[module]/[entity]_category');
    }
    public function save[Entity]Relation($[entity]){
        $data = $[entity]->getCategoriesData();
        if (!is_null($data)) {
            $this->_getResource()->save[Entity]Relation($[entity], $data);
        }
        return $this;
    }
    public function getCategoryCollection($[entity]){
        $collection = Mage::getResourceModel('[module]/[entity]_category_collection')
            ->add[Entity]Filter($[entity]);
        return $collection;
    }
}

and a resource model app/code/local/[Namespace]/[Module]/Model/Resource/[Entity]/Category.php:

<?php

class [Namespace]_[Module]_Model_Resource_[Entity]_Category
    extends Mage_Core_Model_Resource_Db_Abstract {

    protected function  _construct(){
        $this->_init('[module]/[entity]_category', '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 $categoryId) {
            if (!empty($categoryId)){
                $this->_getWriteAdapter()->insert($this->getMainTable(), array(
                    '[entity]_id'      => $[entity]->getId(),
                    'category_id'     => $categoryId,
                    'position'      => 1
                ));
            }
        }
        return $this;
    }
}

and a collection resource model : app/code/local/[Namespace]/[Module]/Model/Resource/[Entity]/Category/Collection.php

<?php
class [Namespace]_[Module]_Model_Resource_[Entity]_Category_Collection
    extends Mage_Catalog_Model_Resource_Category_Collection{
    protected $_joinedFields = false;
    public function joinFields(){
        if (!$this->_joinedFields){
            $this->getSelect()->join(
                array('related' => $this->getTable('[module]/[entity]_category')),
                'related.category_id = main_table.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;
    }
}

Now in the saveAction of your admin controller add this right before calling $[entity]->save()

$categories = $this->getRequest()->getPost('category_ids', -1);
if ($categories != -1) {
    $categories = explode(',', $categories);
    $categories = array_unique($categories);
    $[entity]->setCategoriesData($categories);
}

In your entity model add this at the top of your class: protected $_categoryInstance = null; and these methods anywhere:

protected function _afterSave() {
    $this->getCategoryInstance()->save[Entity]Relation($this);
    return parent::_afterSave();
}
public function getCategoryInstance(){
    if (!$this->_categoryInstance) {
        $this->_categoryInstance = Mage::getSingleton('[module]/[entity]_category');
    }
    return $this->_categoryInstance;
}
public function getSelectedCategories(){
    if (!$this->hasSelectedCategories()) {
        $categories = array();
        foreach ($this->getSelectedCategoriesCollection() as $category) {
            $categories[] = $category;
        }
        $this->setSelectedCategories($categories);
    }
    return $this->getData('selected_categories');
}
public function getSelectedCategoriesCollection(){
    $collection = $this->getCategoryInstance()->getCategoryCollection($this);
    return $collection;
}

That's about it. I hope I didn't miss anything. The code may required some changes because I don't know how exactly is your module built, but the main ideas are there. With some debugging you should get it to work.


Note: The code above was generated using the Ultimate Module Creator v1.9.

5
  • Wonderful post ! You need a little of skills to follow up, but the if you are looking for this kind of answer you should have it enough. There are a typo somewhere (sorry I don't remebere exactly where) a function is called ...categoTy... instead of categoRy. thank you Marius
    – Giuseppe
    Commented Jun 9, 2016 at 10:45
  • Why don't you used Magento category tree api to get data in your module and pass it in view. You can play with it very easily. Commented Jun 9, 2016 at 10:55
  • @SourabhModi. I've recreated the category connection in the same manner that is used for associating categories to a product in the product add/edit form. I thought about keeping it consistent.
    – Marius
    Commented Jun 9, 2016 at 11:09
  • I simply saved my data in a model field called category_ids.
    – Giuseppe
    Commented Jun 9, 2016 at 11:14
  • 1
    @Marius I got your point but the thing is, if you are calling Magento's API than code from your side will be very less and internally Magento is doing the same when api is called as they have done to associate category on product add/edit form. So there is no use of rewriting this code again on your own end as you can use the same functioning by calling a function (API call is a function call) that internally use the same required functioning as of product add/edit form. Commented Jun 9, 2016 at 11:26
2

At least for Magento 1.9 you have to be sure that extJs is loaded.
Use one of the following methods to activate the use of extJS in the backend:

  1. In your controller use this:

    $this->getLayout()->getBlock('head')->setCanLoadExtJs(true);
    
  2. In your layout xml use this:

    <reference name="head">
        <action method="setCanLoadExtJs">
            <value>1</value>
        </action>
    </reference>
    

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.