Como crear un árbol con Ext y Rails (Parte 1) +

Por Boris Barroso

Saludos a todos en mi primer tutorial, en este tutorial voy a enseñarles a usar Ruby on Rails y Ext JS para poder manipular árboles con el plugin para Rails betternestedset que permite reemparentar y además ordenar nodos dentro de alguna rama del árbol, permitiendo crear menús u otro tipo de jerarquías.

Los árboles son elementos muy flexibles que permiten estructurar la información de forma jerárquica, en este tutorial aprenderán a realizar la manipulación, creación y borrado de nodos, osea que preparense por que es un tutor muy completo que constará de dos partes.

Debo comenzar indicando que para la realización de este tutorial he utilizado la versión 2.1 de Ruby on Rails y la versión 2.2 de Ext JS. Comencemos creando la aplicación, desde ahora tomaremos la ruta “/home/miapp/” como la ruta oficial para este tutor, ustedes pueden crear su aplicación donde mas les convenga corriendo el sigueinte comando.

rails miapp

Una ves creada la aplicación, si usan Ruby on Rails 2.1 vayan al archivo
/home/miapp/config/initializers/new_rails_defaults.rb
y modifiquen de la siguiente manera el archivo como se muestra.

ActiveRecord::Base.include_root_in_json = false #Cambiar a false

Esto nos permitirá presentar los objetos JSON de los nodos de forma que Ext pueda entender, de lo contrario creara el JSON con su raiz de la siguente manera {modelo: {id: 1, parent_id: “null”, text: “Primer Nodo” }} y nosotros necesitamos {id: 1, parent_id: “null”, text: “Primer Nodo” }

Luego de haber creado la aplicación y haber realizado las modificaciones pertinentes es necesario descargar la librería Ext de aquí, y descarguen Ext JS 2.2 SDK, copien el archivo comprimido a: /home/miapp/public/ y extraigan el archivo, deben descargar la versión 2 o superior, si descargaron la versión 2.2 se creara un directorio ext-2.2 y renombrenlo por ext ahora sabemos que tenemos la librería Ext en la dirección /home/miapp/public/ext/ y la podemos utilizar. Por defecto Rails usa la base de datos SQLite la cual usaremos en este tutorial, si desean usar otra base de datos realizando modificaciones en  el archivo /home/miapp/config/database.yml. Ahora debemos crear el Modelo, vayan a la carpeta /home/miapp/ y ejecuten el siguiente comando para crear el modelo

ruby script/generate model Category parent_id:integer text:string lft:integer rgt:integer

El modelo que creamos Category tiene un parent_id que es el id del nodo padre, lft que es el left del nodo y nos sirve para ordenar el árbol, rgt, es el rigth o derecha del nodo. Ahora debemos generar la tabla e instalar el plugin betternested set de la siguiente forma

rake db:create
rake db:migrate
script/plugin install svn://rubyforge.org/var/svn/betternestedset/tags/stable/betternestedset

La primera línea crea la base de datos, la segunda crea las tablas en base al modelo que creamos, el nombre de la tabla que se crea es categories (Nota: es necesario de que tengan permisos en su base de datos para poder ejecutar los primeros dos comandos), y la tercera línea instala la versión estable del plugin betternestedset, ahora modifiquen en su editor el modelo /home/miapp/app/models/category.rp de la siguiente forma.

class Category < ActiveRecord::Base
  acts_as_nested_set #Definir que el modelo funcione como Nested

  #Busqueda de nodos sin padres
  def self.root_nodes
    find(:all, :conditions => 'parent_id IS NULL')
  end

  #Busqueda de hijos o sino mostrar los nodos root_nodes
  def self.find_children(start_id = nil)
    start_id.to_i == 0 ? root_nodes : find(start_id).children
  end

  #definición de hojas, son nodos que no tienen hijos
  def leaf
    unknown? || children_count == 0
  end

end

Primero acts_as_nested_set permite al  modelo funcionar como un nested_set o árbol nested, la función def self.root_node hace referencia al mismo objeto y busca a todos los nodos raiz, aquellos que no tienen padres, la función def self.find_children(start_id = nil) realiza la busqueda de los hijos de acuerdo al parametro que se le pasa que es el id del nodo y por ultimo la función def leaf nos permite determinar si un nodo es una hoja (osea no tiene hijos). Ahora debemos ir a la carpeta de nuestra aplicación /home/miapp/ y hacer correr el comando ruby script/console para ejecutar la consola y hacer correr los siguientes comandos:

c = Category.new(:text => "Frameworks")
c.save #Crea el nodo raiz
ruby = Category.new(:text => "Ruby on Rails")
ruby.save #Crea el nodo Ruby on Rails
ruby.move_to_child_of c #Hace que el nodo raíz sea el padre de el Nodo Ruby o Rails
node = Category.new(:text => "Model")
node.save
node.move_to_child_of ruby
node = Category.new(:text => "View")
node.save
node.move_to_child_of ruby
node = Category.new(:text => "Controller")
node.save
node.move_to_child_of ruby
node = Category.new(:text => "Ext JS")
node.save
node.move_to_child_of c
node = Category.new(:text => "Symfony")
node.save
node.move_to_child_of c
node = Category.new(:text => "Struts")
node.save
node.move_to_child_of c
node = Category.new(:text => "CakePHP")
node.save
node.move_to_child_of c

Con esto hemos creado un árbol completo con sus ramas, sigan en el modo consola para poder ver que es lo que sucedio. En la primera linea hemos creado el Padre de todos los demas llamado “Frameworks”, luego hemos creado el nodo “Ruby on Rails” que es el que contiene ademas hijos “Model”, “View” y “Controller” y asi hemos añadido los demas hijos al root. La función move_to_child_of nos permite asignar a un nodo un padre es necesario pasar a esta función el nodo o el id del nodo al cual se le quiere aumentar el nodo hijo. Vuelvan al modo consola y hagan correr los siguientes comandos.

Category.first.children #Muestra los hijos de la raíz "Frameworks"
#Busca el nodo con "Ruby on Rails"
ruby = Category.find(:first, :conditions => {:text => 'Ruby on Rails'})
ruby.children #mostrar todos los hijos del nodo

El primer comando presenta los nodos del nodo raíz, osea el nodo “Frameworks”, el segundo comando selecciona el nodo con el nombre “Ruby on Rails” y el siguiente comando presenta todos sus hijos, como pueden ver el plugin hace que el manjo de árboles sea muy sencillo.

Ahora tenemos que generar el controlador. Ejecuten el siguiente comando desde la carpeta de su aplicación

ruby script/generate controller Categories index create edit move delete

Esto generará el controlador y la vistas respectivas para cada acción. Ahora debemos crear la vista principal la cual nos permita cargar todos los archivos de JavaScript y CSS que necesarios. Vayan al archivo /home/miapp/app/views/categories/index.html.erb este es el archivo por defecto que se carga cuando se acceden enl el browser a http://localhost:3000/categories, y el cual debemos editar de la siguiente manera:

  Arbol Ext con Rails
  <%= stylesheet_link_tag "../ext/resources/css/ext-all.css" %>
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag "../ext/adapter/prototype/ext-prototype-adapter.js" %>
  <%= javascript_include_tag "../ext/ext-all.js" %>

<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>

Esta primera parte nos permite cargar los Scripts y CSS ncesarios luego debemos ir a /home/miapp/app/controllers/categories.rb y editar la función index de la siguiente forma

#Metodo por defecto
  def index
    @category = Category.first #Buscar el primer nodo
    respond_to do |format|
      #Respuesta en caso de que la llamada no sea AJAX
      format.html {render :layout => false} #No usar layout
      #Respuesta en caso de que sea AJAX
      format.json { render :json => Category.find_children(params[:node]) }
    end
  end

El metodo index primero busca el nodo inicial Category.first, despues se ve que tipo de respuesta debe ejecutarse respond_to do |format| permite seleccionar el layout indicado para la respuesta, en caso de que sea AJAX respondera con un objeto JSON, caso contrario renderea la vista index.html.erb. Ahora debemos crear el árbol en la vista /home/miapp/app/views/categories/index.html.erb, adicionen este código dentro de una etiqueta <script type=”text/javascript”>

//Carga una imagen necesaria para poder crear los espacios para otras imagenes
Ext.BLANK_IMAGE_URL = "../ext/resources/images/default/s.gif";
//Inicio de Ext
Ext.onReady(function() {

  //Captura de errores ypresentación una ventana para poder hacer debuggin
  Ext.Ajax.on('requestexception', function(conn, resp, opt) {
    error = new Ext.Window({ title: 'Error', html: resp.responseText, width:640, height: 480, autoScroll: true});
    error.show();
  });
  //Crear un nodo que cargue asicronicamente (AJAX) sus hijos
  root = new Ext.tree.AsyncTreeNode({
    text: 'Invisible Root',
    id:'0',
    draggable: false
  });
  var currentNode = root;

//Crear Arbol
  var tree = new Ext.tree.TreePanel({
    id: 'treePanel',
    loader: new Ext.tree.TreeLoader({
      url:'/categories/',
      requestMethod:'GET',
      baseParams:{format:'json'}
    }),
    width: 200,
    height: 300,
    enableDD: true, //Permite Drag and Drop
    containerScroll: true,
    //ID del tag en el cual se renderiza
    renderTo: 'category-tree',
    root: root,
    rootVisible: false, //No queremos ver el nodo raiz
    tbar : [{text: "Crear", handler: createNode},
      {text: "Borrar", handler: deleteNode}],
    //Definición de eventos
    listeners: {
      //Se define el nodo actual al que se haya seleccionado para poder crear
      //hijos a partir del nodo seleccionado
      click: {fn: function(node) { currentNode = node} }
      //beforeappend: {fn: function() {currentNode.expand() } }
    }
  });
  //expandir el nodo raiz para poder ver el primer nodo
  root.expand();
});

Ahora hay bastante que explicar, pero la verdad es que no tanto, primero es necesario cargar una imagen en blanco, luego inciamos a Ext con Ext.onReady(function() {, en cuanto iniciamos existe una función (Ext.Ajax.on(’requestexception’, function(conn, resp, opt) {) que nos ayudara a realizar el debuggin, si ustedes han trabajado con Ajax ya saben lo feo que es usar Firebug para leer el debugin del Servidor, esta función nos permite capturar las excepciones de Ajax y presentar los errores en una Ventana. Luego comenzamos con la creación del nodo root, este es el nodo principal y es necesario crearlo para poder emparentar los demas nodos a este, en este caso ocultamos a este nodo ya que no es necesario mostrarlo, luego creamos el arbol (var tree = new Ext.tree.TreePanel({),  lean los comentarios del código para entender por que se definen los parametros de esta forma, la parte que explicare es la parte de los eventos listeners: { existen un evento click el cual nos permite seleccionar el nodo actual el cual esta seleccionado. Hasta ahora si ejecutan podrán ver un árbol que carga sus nodos hijos cuando se hace click en el boton “+”. Hasta aquí todo fue sencillo ahora comenzaremos con la parte divertida el movimiento de nodos y reemparentamiento de los mismos. De nuevo vayan al archivo /home/miapp/app/controllers/categories.rb y modifiquen el procedimiento def move

#Procedimiento que permite mover y reenparentar los nodos del arbol
  def move
    msg = '' #Mensaje a presentar, inicialmente vacio
    node = Category.find(params[:node]) #buscar nodo por id
    sibling = params[:sibling].to_i # pariente del nodo o padre
    #Manejo de excepciones
    begin
      case params[:move] #Que parametro se paso al mover
        when ‘left’
          #movimiento a la izquierda mejora su posición (sube)
          node.move_to_left_of sibling
        when ‘right’
          #movimiento a la derecha, baja de posición (baja)
          node.move_to_right_of sibling
        else
          #Se le asigna un nuevo padre
          node.move_to_child_of sibling
      end
      save = true
    rescue ActiveRecord::RecordNotSaved
        #Esta parte se ejecuta en caso de que no se salve correctamente
        save = false
        msg = ‘No se pudo salvar los datos’
    end
    #Presentar el resultado JSON
    render :json => {:success => save, :prev => node.self_and_siblings, :msg => msg }
  end

Bueno, este es el mas complicado de todos los procedimientos que usaremos. Se crea una variable msg que es el mensaje que se presenta en la respuesta JSON, node = Category.find(params[:node]) se busca al nodo por el id, sibling = params[:sibling].to_i se asigna el id del pariente a la variable sibling o tambien puede ser el padre en algunos casos, el parametro params[:move] es el que nos indica cual es movimiento que se va a realizar a la izquierda, derecha o reemparentamiento, en caso de que no se salve correctamente la variable save = false y luego se realiza la presentación render :json => {:success… Ahora prueben en su navegador como funciona y muevan los nodos, veran que se pueden mover arriba, abajo o a otro padre

Bueno esta será la primera parte de este tutorial disculpas si encuentran errores en este tutor, muy pronto publicare la segunda parte en la cual les mostrare como crear nuevos nodos, como editar el texto y como borrar nodos. De todas formas les doy un vinculo para descargar el códio fuente de la aplicación completa, solo que deben seguir las primeras partes del tutor para poder crear su base de datos y configurarla, además que deben copiar en el directorio de su aplicacción /home/miapp/app/ arbolExtRails

6 comentarios en “Como crear un árbol con Ext y Rails (Parte 1)”

  1. [...] la primera parte de este tutorial pudimos ver como se crea un árbol, como usar el plugin “betternestedset” de [...]

  2. This is great info to know.

  3. HI, i think i am doing great with the tuto, but i don’t know where to go with the routes???

    plz xplain

  4. Do not use routes, erease all of them, it will wor fine, if you want to see a working application ussing Ext JS and the tree you can download at http://github.com/boriscy/enano/tree/master

  5. Hay, evidentemente, mucho que saber sobre esto. Creo que usted ha hecho algunos buenos puntos de Caracter

  6. Wow! lo que una idea!

Su comentario