Manipulación fácil del DOM de una tabla con MooTools

Como mencioné anteriormente, he utilizado MooTools en algunos de los proyectos que he realizado con la Fundación donde trabajo. Una de las funcionalidades que frecuentemente se requiere implementar es la de permitir agregar y remover dinámicamente filas o registros a una tabla, para esto utilizamos JavaScript. El diseño que mas utilizamos es la mas simple de todas: manipular el DOM de la tabla localmente en el cliente y dejarle el trabajo a la aplicación del lado del servidor de manejar el almacenamiento de los registros.

Históricamente realicé una primera versión de esta funcionalidad utilizando las funciones de manipulación del árbol DOM de JavaScript pero no tenía experiencia con ellas y apenas estaba aprendiéndolas por lo que su diseño poco eficiente con una implementación terrible. Después conocí las librerías de JavaScript e hice una segunda versión utilizando MooTools; el resultado fue algo mejor, sin embargo no muy eficiente: debía crearse funciones JavaScript (implementación incluída) por cada tabla que se fuera a manipular :-|. Aún no conocía el buen uso de las funciones $ y $$.

Ahora he creado una nueva versión que como lo que me gusta … es muy simple. Permite agregar y remover filas de cualquier tabla e inclusive permite manejar varias tablas en la misma página que se identifican con su atributo id a través de un único conjunto de funciones que se incluyen a través de una etiqueta <script>.

En el script se definen dos funciones: TDAgregarRegistro(tablaId) y TDRemoverRegistros(tablaId) que permite agregar y remover filas (seleccionadas) de una tabla respectivamente.

El usuario deberá por su parte definir las siguientes funciones para determinar comportamiento de las funciones base.

  1. obtEstructuraRegistro(tablaId): determina la estructura de las filas o registros de la tabla.
  2. obtPropiedadesCampo(tablaId): determina las propiedades o atributos de un campo de la tabla.
  3. obtPropiedadesFila(tablaId): determina las propiedades o atributos de una fila de la tabla.

Cada una de estas funciones recibe el id de la tabla a la cual se hace referencia, con este valor la función deberá determinar su respuesta, la cual es un arreglo asociativo. Por la simplicidad requerida por la solución, por ahora se soportan dos tipos de campos: de texto y de selección.

Enlaces:

Artículo relacionado: Manipulación fácil del DOM de una tabla con Prototype.

Introducción a MooTools

MooTools es una librería de JavaScript que promete abstraer un nivel mas la generación de código en este lenguaje facilitando el desarrollo rápido e independiente de navegador. Incluye además soporte para efectos visuales, manipulación del árbol DOM, AJAX y JSON entre otras cositas interesantes.

Valerio Proietti, el desarrollador de MooTools lo define de las siguiente manera en su página web.

Object-Oriented JavaScript framework designed for the intermediate to advanced JavaScript developer. It allows you to write powerful, flexible, and cross-browser code with its elegant, well documented, and coherent API.

MooTools code respects strict standards and doesn’t throw any warnings. It’s well commented and has meaningful variable names: a joy to browse and a snap to understand.

MooTools is compatible and fully tested with Safari, internet explorer 6 and 7, Firefox (and its mozilla friends), Opera and Camino.

Hasta ahora no le he dado un uso extenso a la librería pero me ha parecido interesante y útil, lo suficiente para incluírla en dos de los proyectos que desarrollo en la Fundación.

A mi manera de ver esta librería tiene dos problemas que contrastan contra sus múltiples ventajas.

  1. Es el producto de un desarrollador versus el producto de una comunidad entera como sucede con otros proyectos de código abierto.
  2. La cantidad y calidad de la documentación que se encuentra en Internet es baja si se compara con otras librerías como Prototype.

Con respecto al segundo punto, mas allá del API, los demos y el Mootorial de CNET no he encontrado documentación oficial o elaborada. Hacen falta publicaciones y tutoriales completos y elaborados que permitan conocer de manera concisa y rápida a esta librería.

La excepción del comentario es el sitio de Jourmoly en el cual hay algunos artículos de MooTools que me fueron muy útiles hace un par de meses cuando estaba apenas aprendiendo y que aún me remito a ellos cuando entre tantos lenguajes y librerías se me olvidan algunos detalles.

Actualmente el sitio tiene publicados artículos acerca de los siguientes temas.

Introspección de Objetos con Prototype

La introspección de objetos permite consultar información del objeto mismo. En Prototype los métodos descritos a continuación pertenecen a la clase Object.

Object.inspect(e) : string Retorna la representanción de cadena del elemento, útil para labores de depuración.
Object.isArray(e) : boolean Retorna verdadero si el elemento es un arreglo, falso de lo contrario.
Object.isElement(e) : boolean Retorna verdadero si el elemento es un objeto DOM, falso de lo contrario.
Object.isFunction(e) : boolean Retrona verdadero si el elemento es una función, falso de lo contrario.
Object.isHash(e) : boolean Retorna verdadero si el elemento es un hash, falso de lo contrario.
Object.isNumber(e) : boolean Retorna verdadero si el elemento es un número, falso de lo contrario.
Object:isString(e) : boolean Retorna verdadero si el elemento es una cadena, falso de lo contrario.
Object.isUndefined(e) : boolean Retorna verdadero si el valor del elemento es undefined, falso de lo contrario.
Object.keys(e) : [string, …] Retorna un arreglo con las llaves del elemento.
Object.values(e) : [string, …] Retorna un arreglo con los valores del elemento.
Object.toQueryString(e) : string Retorna la representación de consulta en cadena codificada para URL del elemento.

La función inspect realiza el siguiente procedimiento para calcular la representación en cadena de un objeto.

  • Si el objeto es null o undefined utiliza los nombres de estos valores.
  • Si el objeto presenta el método inspect, este es llamado.
  • De lo contrario se utiliza el método toString presente en todos los objetos.

Revise el siguiente ejemplo para conocer como utilizar estos métodos de introspección.

Para mas información acerca de estos métodos consulte el API referente a la clase Object.

Funciones básicas de Prototype

Para incluír Prototype en las páginas web se debe agregar la siguiente línea a la sección de head, reemplazando la ruta src con la adecuada.

La función $() reemplaza a la expresión document.getElementById del JavaScript estándar revisando el árbol de elementos DOM y retornando una versión ampliada del elemento en cuestión. Esta función recibe como parámetro la etiqueta del ID del elemento o directamente la referencia al objeto DOM para obtener su versión ampliada. Más información.

a = document.getElementById('seccion_1');         // JavaScript convencional.
b = $('seccion_1');     // Estilo Prototype.

a.style.display = 'none';    // JavaScript convencional.
b.hide();     // Gracias a la extensión de los elementos que provee Prototype.

Esta función puede utilizarse con varios parámetros en un único llamado.

secciones = $('seccion_1', 'seccion_2', 'seccion_3');

Esto equivale a decir lo siguiente.

secciones = [];
secciones[0] = $('seccion_1');secciones[1] = $('seccion_2');
secciones[2] = $('seccion_3');

La función $w() convierte cadenas con palabras separadas por espacios en arreglos cuyas celdas corresponden a estas palabras. Más información.

numeros = $w('uno dos tres cuatro');

El contenido de numeros será entonces [‘uno’, ‘dos’, ‘tres’, ‘cuatro’].

La función $$() realiza búsquedas basadas en los selectores de CSS, el resultado de esta función será siempre un arreglo toda vez que la búsqueda podrá arrojar múltiples resultados. Más información.

$$('div')     // Todos los divs.
$$('#seccion_1')     // Igual que $('seccion_1') pero en forma de arreglo.
$$('li.resaltado')     // Todos los LI que tengan la clase 'resaltado'.
$$('#seccion_1 p')     // Todos los P dentro de seccion_1.

Existen múltiples opciones para esta función flexible, para consultar mas información al respecto revise el API correspondiente.

La función $A() convierte (promueve a) los elementos estilo colección (o arreglo) en objetos Array de Prototype. La ventaja de utilizar este método es que enriqucerá el resultado con los método de la clase Array como each, indexOf y toJSON, facilitando el procesamiento de la información contenida en él. Más información.

Esta función opera bajo las siguientes premisas:

  • Si el valor es null, undefined o false, el arreglo resultante es vació.
  • Si el objeto presenta el método toArray() entonces este método es utilizado.
  • De lo contrario los valores del objeto son recorridos y manipulados de la manera convencional.

La función $F() toma un elemento campo de un formulario, ya sea el objeto DOM o su respectivo ID y retorna el valor contenido en el campo. Es una versión mejorada de la sentencia document.getElementByID(‘elemento’).value. Más información.

Esta función opera bajo las siguientes premisas:

  • Para las listas de selección múltiple se obtiene un arreglo con cada uno de los valores selccionados.
  • Para los checkboxes y radiobuttons no seleccionados se obtiene null.
  • Para cualquier otro elemento se obtiene una cadena de texto con el valor del campo.

La función $H() convierte (promueve a) un hash de JavaScript un objeto Hash de Prototype con sus consiguientes métodos ampliados. Más información.

opciones = $H({color: 'rojo', sabor: 'salado', tipo: 'alimento'});

opciones.keys();    // Retorna ['color', 'sabor', 'tipo'].
opciones.values();    // Retorna ['rojo', 'salado', 'alimento'].

La función $R() crea objetos rango (ObjectRange) de Prototype con sus respectivos métodos y usos útiles. Más información.

$R(1, 10).each(function(i)
{
// Se ejecuta 10 veces iterando a i desde 1 hasta 10.
});

$A($R(1, 10));    // Retorna el arreglo [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

Jugando con InnerHTML (3)

Al ejemplo anterior de tablas dinámicas utilizando JavaScript que permitía agregar y eliminar filas dinámicamente del lado del cliente le hice algunas modificaciones que encontré necesarias cuando lo utilicé en un caso práctico. El ejemplo permite ahora agregar un documento de identidad que actuará como llave primaria del registro. El documento se muestra como texto estático y además se incluye como campo oculto (hidden) para que sea reconocido por la aplicación del lado del servidor.

Los demás campos son presentados en la tabla como campos editables (input) para permitir su modificación por parte del usuario hasta el momento de hacer el envío.

El ejemplo puede consultarse en el siguiente enlace: Test InnerHTML 3

Menú de opciones con bordes redondeados

Uno de los portales que se están desarrollando en la Fundación tenía una barrita de menú con botones redondeados. Los amantes del Flash dijeron rápidamente que ellos lo hacían fácilmente en Flash que no me preocupara; todo quedó así … hasta hoy. Hoy lo estuvieron pensando y se dieron cuenta que era muy difícil y dispendioso crear las imágenes y tratar de cambiar de color el botoncito según esté seleccionado o no. Por suerte yo ya estoy superando esta gripa y amanecí con ganas de un poco de carpintería.

Lo primero que hice fue buscar la librería de JavaScript Nifty Corners que ya había visto con anterioridad para tal fin, sin embargo en la búsqueda también encontré una segunda y dejé que la revisión de los ejemplos determinara cual iba a utilizar. Resulta que Curvy Corners si permite darle un borde al botón redondeado … o al menos si lo muestra en un ejemplo, así que la escogí.

Creo algunos DIV de clase miBoton con IDs boton1, boton2, boton3, … Ahí es donde entra a jugar Curvy Corners.

Se incluye la librería de Javascript.

<script type="text/JavaScript" src="rounded_corners_lite.inc.js"></script>

En la función onLoad de la página se especifican las propiedades de la redondez y se asocia esta con los elementos de la clase miBoton.

settings =
{
tl: { radius: 10 },
tr: { radius: 10 },
bl: { radius: 10 },
br: { radius: 10 },
antiAlias: true,
autoPad: true,
validTags: ["div"]
}

var myBoxObject = new curvyCorners(settings, "miBoton");
myBoxObject.applyCornersToAll();

Por mi parte creo la función cargarBotones() que establece la referencia global a los botones incluyendo el objeto HTML y enlace a donde se deberá dirigir el navegador cuando le hagan click.

Igualmente implementé la función colorearBotones() que le pone el fondo verde a los botones no seleccionados y naranja a los seleccionados.

La solución fue muy sencilla. Lo bueno es que hasta ahora cumple con lo que necesitaba el cliente y se desarrolló bastante rápido la solución. Lo malo fue que, como cosa rara, cuando verifiqué que funcionara con IE7 … todo se derrumbó.

Inicialmente sólo aparecía la cajita de los botones, vacía y cuadrada. Nada funcionaba. Lo probé en mi equipo que tiene instalado el JavaScript Debugger de Microsoft y se quejaba de una linea de la librería de CurvyCorners … ni idea porqué. Verifiqué el ejemplo y este funcionaba sin problemas así que comparé mi código con el del ejemplo … nada extraño. Hice una copia y empecé a convertir mi código en el código de ejemplo hasta que lo hice funcionar … no había nada raro … solamente el color de los botones. Había puesto que fueran green normalmente y orange si los habían seleccionado; pues parece que IE no entiende inglés, sólo hex.

Cambié la especificación de los colores y todo empezó a fallar extrañamente, a ratos, sin sentido. Quité los llamados de colorearBotones() y algunos botones se coloreaban y otros no. Obra de satanás. Comenté el código de la función y coloreamiento se detuvo.

Tuvo que pasar otro rato para que me diera cuenta que ese útil navegador no estaba refrescando bien la página cada vez que yo le daba F5 y me estaba mostrando versiones antiguas. Conclusión con IE es mejor darle siempre CTRL-F5 para que refresque completamente.

Se puede consultar el código fuente del ejemplo haciendo click en el siguiente enlace: test_curvycorners

Jugando con InnerHTML (2)

Bien, encontré una mejor forma de hacerlo … con DOM.

Insertar una nueva fila en una tabla (destino) es fácil, sólo es establecer donde se quiere e insertarla.

var nuevo_indice = destino.rows.length;
fila = destino.insertRow(nuevo_indice);

Después es necesario especificar el contenido de las celdas, para el ejemplo, un checkbox y dos valores de texto.

celda = fila.insertCell(0);
valor = document.createElement("input");
valor.type = "checkbox";
valor.id = "marca";
valor.name = "marca";
valor.value = nuevo_indice;
celda.appendChild(valor);

celda = fila.insertCell(1);
valor = document.createTextNode(nombres + " - " + nuevo_indice);
celda.appendChild(valor);

celda = fila.insertCell(2);
valor = document.createTextNode(apellidos);
celda.appendChild(valor);

Para remover las filas obtengo una referencia a los elementos que sean input y pertenezcan a la tabla.

var inputs = origen.getElementsByTagName("input");

Después filtro los que sean checkbox y sean los que me interesan (id=’marca’). Los almaceno en un arreglo.

for (i=0; i<inputs.length; i++)
{
if (inputs[i].type == "checkbox" &&
inputs[i].id == "marca" &&
inputs[i].checked)
{
checkboxes[chk_cuenta] = inputs[i];
chk_cuenta ++;
}
}

Después de filtrados procedo a eliminarlos.

for (i=0; i<checkboxes.length; i++)
{
origen.deleteRow(checkboxes[i].value - 1*i);
}

En este punto encontré un serio problema. El value de los checkboxes seleccionados me indica cuales son las filas que deseo remover, pero después de remover la primera de ellas la continuidad de las filas se altera ya que se eliminó un elemento. Para compensar esto se disminuye 1*i el índice de la fila, siendo 0 para el primer caso cuando no se ha alterado, 1 cuando se ha removido una fila y así sucesivamente.

La solución anterior soluciona el problema durante un procedimiento de eliminación de filas, sin embargo al terminar la tabla queda con sus índices alterados y por consiguiente va a fallar la siguiente eliminación y posiblemente la adición de filas también va a ser confusa. Para esto tuve que corregir los índices de las filas tan pronto como se termina la remoción.

for (i=0; i<inputs.length; i++)
{
if (inputs[i].type == "checkbox" &&
inputs[i].id == "marca")
{
inputs[i].value = i + 1;
}
}

El archivo fuente del ejemplo se puede descargar del siguiente enlace: test_inner2.html

Jugando con InnerHTML

Con este ejemplo muy sencillo es posible agregar filas de una tabla en el lado del cliente sin hacer un requerimiento al servidor y ahorrarse uno la consabida lentitud, útil en momentos de agregar items a una lista. Obviamente, a diferencia de Ajax, los elementos sólo viven en el lado del cliente hasta que se envían, en un sólo requerimiento, hacia el servidor donde son almacenados en la base de datos.

La idea es crear una tabla (info) que va a contener los datos.

<table id="info" name="info" class="tabla">
<tr class="titulos">
<td>
Nombres
</td>
<td>
Apellidos
</td>
</tr>
</table>

Un formulario desde el cual se agregarán nueva información.

<form id="datos" name="datos" onsubmit="return agregar_fila(this)">
Nombres: <input type="text" id="nombres" name="nombres" value="" size="25" />
Apellidos: <input type="text" id="apellidos" name="apellidos" value="" size="25" />

<input type="submit" id="agregar" name="agregar" value="Agregar" />
</form>

Y una función de JavaScript que agregará la nueva información (nombres y apellidos) a la tabla.

function agregar_fila(origen)
{
nom = origen.nombres.value;
apl = origen.apellidos.value;

document.getElementById("info").innerHTML += "<tr class='fila'>
<td>
" + nom + "
</td>
<td>
" + apl + "
</td>
</tr>";

return false;
}

El archivo de ejemplo puede descargarse desde el siguiente enlace: test_inner.html