
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.
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.
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
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 getAdminmenuAction( Request $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
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, $template, MatcherInterface $matcher, array $defaultOptions = array() )
{
$this->environment = $environment;
$this->matcher = $matcher;
$this->defaultOptions = array_merge( array(
'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 render( ItemInterface $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.
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
Print article | This entry was posted by elvis on 02/14/18 at 07:31:00 pm . Follow any responses to this post through RSS 2.0. |