Revised Dijit/Tree Menu for Symfony

This is the final implementation for the tree menu I'm using for a Symfony application.

The first approach wrote the JSON for the menu store into the template. This was a little awkward and as the application grew, the amount of content increased.

Next, I had an idea that I could create a multi-tiered tree which would have a static top tier, but include content in the lower levels. The value of this was that the user could navigate directly to content, for example the details of a client. Lazy loading was used to limit the number of requests and size of responses. It worked. But it was slow. And I realized that as the amount of data increased, it would get slower. In addition, navigating through three or more levels on a tree is inefficient. So, I wanted something that was faster and streamlined.

Revised Dijit/Tree Menu for Symfony

The first change I made was to add an autocomplete search box, which allows the user enter the name of the item they need. It could be the name of a person, a barcode or a company. The search code isn't complete.

Then I rewrote the JsonRenderer and the MenuStoreController such that there is only a static menu delivered via Ajax.

services.yml

These are the service configurations.

Code

app.admin.menu_store_controller:
        class: AppBundle\Controller\Api\Admin\Common\MenuStoreController
 
    app.menu_renderer:
        class: AppBundle\Menu\JsonRenderer
        arguments: [ "@twig",  "knp_menu.html.twig", "@knp_menu.matcher", {"translator": "@translator" }]
        tags:
            - { name: knp_menu.renderer, alias: json }

MenuStoreController

The MenuStoreController calls the KnpMenuBuilder, then the JsonRenderer and delivers the result as JSON to the client.

PHP

<?php
 
namespace AppBundle\Controller\Api\Admin\Common;
 
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations\View;
 
class MenuStoreController extends FOSRestController
{
    /**
     * @View()
     */
    public function getAdminmenuActionRequest $request )
    {
        $this->denyAccessUnlessGranted'ROLE_ADMIN'null'Unable to access this page!' );
        $adminMenu $this->get'app.menu_builder' )->createAdminMenu( [] );
        $renderer $this->get'app.menu_renderer' );
        return array_values$renderer->render$adminMenu ) );
    }
 
}

JsonRenderer

The JsonRenderer iterates through the menu and creates the data for the dijit/Tree.

PHP

<?php
 
namespace AppBundle\Menu;
 
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Renderer\RendererInterface;
 
class JsonRenderer implements RendererInterface
{
 
    /**
     * @var \Twig_Environment
     */
    private $environment;
    private $matcher;
    private $defaultOptions;
    private $translator;
 
    /**
     * @param \Twig_Environment $environment
     * @param string            $template
     * @param MatcherInterface  $matcher
     * @param array             $defaultOptions
     */
    public function __construct( \Twig_Environment $environment$templateMatcherInterface $matcher, array $defaultOptions array() )
    {
        $this->environment $environment;
        $this->matcher $matcher;
        $this->defaultOptions array_mergearray(
            'depth' => null,
            'matchingDepth' => null,
            'currentAsLink' => true,
            'currentClass' => 'current',
            'ancestorClass' => 'current_ancestor',
            'firstClass' => 'first',
            'lastClass' => 'last',
            'template' => $template,
            'compressed' => false,
            'allow_safe_labels' => false,
            'clear_matcher' => true,
            'leaf_class' => null,
            'branch_class' => null
                ), $defaultOptions );
    }
 
    public function renderItemInterface $item, array $options array() )
    {
        $options array_merge$this->defaultOptions$options );
        if( empty( $options['depth'] ) )
        {
            $options['depth'] = PHP_INT_MAX;
        }
 
        $this->translator $options['translator'];
 
        $itemIterator = new \Knp\Menu\Iterator\RecursiveItemIterator$item );
 
        $iterator = new \RecursiveIteratorIterator$itemIterator, \RecursiveIteratorIterator::SELF_FIRST );
 
        $tree = [];
        $parent null;
        $levelParent[0] = null;
 
        $lastLevel null;
        foreach( $iterator as $item )
        {
            $translatedLabel $this->translator->trans$item->getLabel() );
            $id strtolower$item->getName() );
            $level $item->getLevel();
            if( $level <= $options['depth'] )
            {
                $node = [];
                $node['id'] = $id;
                $node['name'] = $translatedLabel;
                $node['uri'] = $item->getUri();
                $node['has_children'] = $item->hasChildren();
                $node['level'] = $level;
                if( $lastLevel !== null )
                {
                    if( $level $lastLevel )
                    {
                        $parent $levelParent[$level] = $lastNode['id'];
                    }
                    else
                    {
                        if( $level $lastLevel )
                        {
                            $parent $levelParent[$level];
                        }
                    }
                    $lastParent $parent;
                }
                $node['parent'] = $parent;
                $tree[$id] = $node;
                $lastLevel $level;
                $lastNode $node;
            }
        }
        return $tree;
    }
 
}

menu.js

menu.js requests the menu data and renders it on the page.

Code

define([
    "dojo/dom",
    "dojo/request/xhr",
    "dojo/store/Memory",
    "dijit/tree/ObjectStoreModel",
    "dijit/Tree",
    "dojo/store/JsonRest",
    "dijit/form/ComboBox",
    "dojo/i18n!app/nls/core",
    "dojo/domReady!"
], function (dom,
        xhr, Memory, ObjectStoreModel, Tree, JsonRest, ComboBox, core) {
//"use strict";
    function run() {
 
        var searchInput, searchStore;
 
        xhr.get("/api/menustore/adminmenus/", {
            handleAs: "json"
        }).then(function (res) {
            var i, l, store = [], memory, model;
            l = res.length;
            for( i = 0; i < l; i++ ) {
                store.push(res[i]);
            }
            memory = new Memory({
                data: store,
                getChildren: function (object) {
                    return this.query({parent: object.id});
                }
            });
 
            // Create the model
            var model = new ObjectStoreModel({
                store: memory,
                query: {id: 'admin'},
                mayHaveChildren: function(object) {
                    return typeof object.has_children !== "undefined" && object.has_children;
                }
            });
 
            // Create the Tree.
            var tree = new Tree({
                id: "admin-menu",
                model: model,
                persist: true,
                showRoot: false,
                onClick: function (item) {
                    if( typeof item.uri !== "undefined" && item.uri !== null ) {
                        location.href = item.uri;
                    }
                },
                getIconClass: function (item, opened) {
                    return (item && item.has_children) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
                }
            }, "admin-left-menu");
            tree.startup();
        });
 
        searchStore = new JsonRest({
            target: '/api/store/search',
            useRangeHeaders: false,
            idProperty: 'id'});
        searchInput = new ComboBox({
            trim: true,
            "class": "search",
            store: searchStore,
            searchAttr: "name",
            placeholder: core.search
        }, "search");
        searchInput.startup();
        searchInput.on("change", function (evt) {
            console.log(evt);
            console.log(searchInput.store);
        });
 
    }
    return {
        run: run
    };
});
//# sourceURL=menu.js