Add Category Tree in Magento® Custom Admin Form

Adding category selector in Magento can be a tedious task. It was tedious before I began. But as I started, it turned out to be most easy task. Only few files will do it for you. The benefit is that you can add this code anywhere in your module. No need to create a tab in admin form.

Magento-categories

1. Create category block

You need to create a block that can serve as base block for category subtree view. This blick will extend Mage_Adminhtml_Block_Catalog_Category_Tree. Here is the code for this block:

<?php  
class Webspeaks_MyModule_Block_Adminhtml_Categories extends Mage_Adminhtml_Block_Catalog_Category_Tree  {

    protected $_categoryIds = null;
    protected $_selectedNodes = null;

    public function __construct() {
        parent::__construct();
        $this->setTemplate('mymodule/categories.phtml');
        $this->_withProductCount = false;
    }

    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->getSelectedCategotyPathIds($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;
    }

}

2. Create the template for category subtree

In this template we will render the category tree view. In normal scenarios you dont need to change the code of this template.

<div class="entry-edit">
    <div class="entry-edit-head">
        <h4 class="icon-head head-edit-form fieldset-legend">
            <?php echo Mage::helper('mymodule')->__('Categories') ?>
        </h4>
    </div>
    <fieldset id="grop_fields">
        <input type="hidden" name="category_ids" id="importer_categories" value="<?php echo $this->getIdsString() ?>">
        <div id="my-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('importer-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 = $('importer_categories').value.split(',');
        ids.push(id);
        $('importer_categories').value = ids.join(',');
    }
    function categoryRemove(id) {
        var ids = $('importer_categories').value.split(',');
        while (-1 != ids.indexOf(id)) {
            ids.splice(ids.indexOf(id), 1);
        }
        $('importer_categories').value = ids.join(',');
    }
</script>
<?php endif; ?>

3. Load ExtJs

Above template uses ExtJs for rendering category treeview. So we need to load ExtJs on our page. In the admin controller of our module, add below line:

<?php
class Webspeaks_MyModule_Adminhtml_MyPageController extends Mage_Adminhtml_Controller_Action
{
    public function indexAction()
    {
        $this->loadLayout();
        $this->getLayout()->getBlock('head')->setCanLoadExtJs(true);    // This line needs to be added for loading ExtJs
        $this->renderLayout();
    }

}

Also in thge layout file, you need to add followinf lines:

<?xml version="1.0"?>
<layout version="0.1.0">
    <adminhtml_mypage_index>
        <!-- Tell magento to load extjs -->
        <reference name="head">
            <action method="setCanLoadExtJs">
                <value>1</value>
            </action>
        </reference>
        <reference name="content">
            <!-- Your module template here -->
        </reference>
    </adminhtml_mypage_index>
</layout>

4. Create Category Actions

Category tree need to load data json data for categories. We need to create an action in the same controller which renders the category tree. Add following action in above controller:

<?php
class Webspeaks_MyModule_Adminhtml_MyPageController extends Mage_Adminhtml_Controller_Action
{
    ...

    public function categoriesJsonAction()
    {
        $this->getResponse()->setBody(
            $this->getLayout()->createBlock('mymodule/adminhtml_categories')
                ->getCategoryChildrenJson($this->getRequest()->getParam('category'))
        );
    }
}

5. Render the categories template

Our infrastructure ios ready now. We just need to render out categories block on our page. Just add following line and category tree should be visible:

<?php echo $this->getLayout()->createBlock('mymodule/adminhtml_categories')->toHtml() ?>

Adding the above block in your admin form will render the categories subtree.
When you submit the form, you can get the comma separated values of categories as:

<?php
$categories = $this->getRequest()->getParam('category_ids');
?>

I hope you will find above tutorial helpful.

Written by Arvind Bhardwaj

Arvind is a Magento and WordPress expert with more than 6 years of industry wide experience.

Website: http://www.webspeaks.in/

4 thoughts on “Add Category Tree in Magento® Custom Admin Form

  1. Hi Arvind,

    Thanks for the document, I have followed your instructions to list the category tree structure in Admin, its half success for me, I am not able to get the selected category id's (importer_categories) from the tree once the category is selected. Also the ids are pre added with comma separator(eg: ,2).
    It will be good if i got the idea of how the category id fetches from tree. Then i can resolve the issue of comma separator.

    Looking forward for your reply.

    Thanks
    Elizabeth

  2. Hi Arvind,
    Following steps did added the category tree to my custom module. But can you tell me if it will display only 1st level categories or their sub categories as well, as i can see only 1st level ones. Also, i am not able to get the selected categories on posting the form. Can you please guide for these issues?
    Thanks
    Suyati

Leave a Reply

Your email address will not be published. Required fields are marked *

16 − 7 =