Tree Behavior o cómo crear una estructura jerárquica
Miércoles, 14 Octubre 2009
En esta ocasión hablaremos de cómo crear una estructura jerárquica utilizando el Tree Behavior de CakePHP. Este comportamiento facilita muchísimo las cosas a la hora de manipular árboles jerárquicos de datos.
Utilizaremos también la librería de Javascript Ext JS para poder manipular el árbol gráficamente de manera sencilla, utilizando drag and drop.
Lo primero que tendremos que hacer será añadir en la tabla de la base de datos de la entidad. Utilizando los nombres por defecto de Cake, serían los siguientes campos:
- parent_id: hace referencia al padre del elemento.
- lft: almacena el identificador del elemento de la izquierda en el mismo nivel.
- rght: almacena el identificador del elemento de la derecha en el mismo nivel.
Incluiremos también los campos id y nombre, en una entidad denominada, por ejemplo, Categoria.
El modelo lo haríamos de la siguiente manera:
Este comportamiento tiene funciones muy útiles para la manipulación del árbol. Las más utilizadas quizás sean las siguientes:
- children: devuelve los hijos de un nodo concreto, pudiendo seleccionar si deseamos obtener sólo los hijos directos o todos los hijos en el árbol.
- generatetreelist: función muy útil para obtener los elementos a introducir en un select de HTML.
- getpath: devuelve todo el path hasta el nodo especificado en un array.
- movedown y moveup: sirven para bajar o subir un nivel de un nodo del árbol.
- removefromtree: elimina un nodo del árbol sin perder la estructura, es decir, modificando el padre de sus hijos.
- reorder: reordena un nodo (arrastrando también a sus hijos) en función de los parámetros especificados.
Para realizar la manipulación visual del árbol de categorías crearemos las siguientes funciones en el controlador app/controllers/categorias_controller.php sería:
<?php
class CategoriasController extends AppController{
var $helpers = array( 'Javascript');
// Función para organizar las categorías
function organizar() {}
function getnodes() {
Configure::write('debug', 0);
// obtener el identificador del padre que se envío por POST vía Ajax
$parent = intval($this->params['form']['node']);
// encontrar los hijos directos del nodo anterior
$nodes = $this->Categoria->children($parent, true, null, 'Categoria.lft ASC');
$this->set(compact('nodes'));
$this->render('getnodes', 'ajax');
}
function reorder()
{
Configure::write('debug', 0);
// delta es la diferencia en la posición (1 = nodo siguiente, -1 = nod anterior)
$node = intval($this->params['form']['node']);
$delta = intval($this->params['form']['delta']);
if ($delta > 0) {
$this->Categoria->movedown($node, abs($delta));
} elseif ($delta < 0) {
$this->Categoria->moveup($node, abs($delta));
}
exit('1');
}
function reparent()
{
Configure::write('debug', 0);
$node = intval($this->params['form']['node']);
$parent = intval($this->params['form']['parent']);
$position = intval($this->params['form']['position']);
// guardamos el nuevo padre de la categoría
$this->Categoria->id = $node;
$this->Categoria->saveField('parent_id', $parent);
// Si position == 0, nos movemos al inicio.
// En otro caso, calculamos la distancia que nos moveremos ($delta).
if ($position == 0) {
$this->Categoria->moveup($node, true);
} else {
$count = $this->Categoria->childcount($parent, true);
$delta = $count - $position - 1;
if ($delta > 0) {
$this->Categoria->moveup($node, $delta);
}
}
exit('1');
}
}
?>
Sólo nos quedaría hacer las vistas. En este caso, necesitamos crear 2 vistas: organizar.ctp y getnodes.ctp.
// app/views/categorias/organizar.ctp
<?php echo $html->css('/js/ext-2.0.1/resources/css/ext-custom.css'); ?>
<?php echo $javascript->link('/js/ext-2.0.1/ext-custom.js'); ?>
<script type="text/javascript">
Ext.BLANK_IMAGE_URL = '<?php echo $html->url('/js/ext-2.0.1/resources/images/default/s.gif') ?>';
Ext.onReady(function(){
var getnodesUrl = '<?php echo $html->url('/categorias/getnodes') ?>';
var reorderUrl = '<?php echo $html->url('/categorias/reorder') ?>';
var reparentUrl = '<?php echo $html->url('/categorias/reparent') ?>';
var Tree = Ext.tree;
var tree = new Tree.TreePanel({
el:'tree-div',
autoScroll:true,
animate:true,
enableDD:true,
containerScroll: true,
rootVisible: true,
loader: new Ext.tree.TreeLoader({
dataUrl:getnodesUrl
})
});
var root = new Tree.AsyncTreeNode({
text:'Categorías',
draggable:false,
id:'root'
});
tree.setRootNode(root);
tree.setHeight('auto');
var oldPosition = null;
var oldNextSibling = null;
tree.on('startdrag', function(tree, node, event){
oldPosition = node.parentNode.indexOf(node);
oldNextSibling = node.nextSibling;
});
tree.on('movenode', function(tree, node, oldParent, newParent, position){
if (oldParent == newParent){
var url = reorderUrl;
var params = {'node':node.id, 'delta':(position-oldPosition)};
} else {
var url = reparentUrl;
var params = {'node':node.id, 'parent':newParent.id, 'position':position};
}
tree.disable();
Ext.Ajax.request({
url:url,
params:params,
success:function(response, request) {
// if the first char of our response is zero, then we fail the operation,
// otherwise we re-enable the tree
if (response.responseText.charAt(0) != 1){
request.failure();
} else {
tree.enable();
}
},
failure:function() {
// we move the node back to where it was beforehand and
// we suspendEvents() so that we don't get stuck in a possible infinite loop
tree.suspendEvents();
oldParent.appendChild(node);
if (oldNextSibling){
oldParent.insertBefore(node, oldNextSibling);
}
tree.resumeEvents();
tree.enable();
alert("Oh no! Your changes could not be saved!");
}
});
});
tree.render();
root.expand();
});
</script>
<div id="tree-div"></div>
Y la última vista:
<?php // app/views/categorias/getnodes.ctp $data = array(); foreach ($nodes as $node) {$data[] = array("text" => $node['Categoria']['nombre'],"id" => $node['Categoria']['id']); } echo $javascript->object($data); ?>
Ya sólo nos quedaría descargarnos la librería ExtJS y copiarla en la carpeta /app/webroot/js. En el ejemplo, se ha utilizado la versión 2.0.1.
Categoria
