Lista de elementos ordenables y editables usando HTML5, jQuery, PHP y MySQL

El tutorial de hoy va a ser un poco más largo de lo habitual pero, no hace mucho que tuve que crear algo como esto en la empresa y me pareció algo útil y que podrá ser aprovechado por muchos. Lo que vamos a crear es una lista de elementos (obtenidos de una base de datos) que podremos editar sobre la propia lista usando el atributo contenteditable, ordenar gracias a jQuery UI, PHP y MySQL y añadir/eliminar elementos de la lista usando jQuery, PHP y MySQL.

Esta es una captura de pantalla de cómo quedará nuestra lista:

Cuánto trabajo, ¿no? Ya verás lo fácil que esto resulta. Los usos son múltiples aunque, el más común, es el de una lista de tareas. En mi caso, es una lista de categorías así que voy a simplificar un poco el código creado.


Lo primero que vamos a hacer es plantear las tablas MySQL. Lo que necesitamos es una tabla en la base de datos que tenga los siguientes campos:

  • **#id** – Este campo almacenará la ID del elemento de la lista. Es la clave primaria.
  • **nombre** – En este campo almacenaremos el propio texto del elemento.
  • **orden** – Lo habéis adivinado, aquí vamos a almacenar el orden en que se muestran los elementos.

Este es el código necesario para crear la tabla:

CREATE TABLE IF NOT EXISTS `elementos` (  
 `id` INT NOT NULL AUTO_INCREMENT ,
 `nombre` VARCHAR(255) NULL ,
 `orden` INT NOT NULL ,
 PRIMARY KEY (`id`))
 ENGINE = InnoDB

Ahora, vamos a insertar 5 elementos de ejemplo, para nuestra lista de cosas pendientes a hacer por San Valentín…

INSERT INTO `elementos` (`id`, `nombre`, `orden`)  
VALUES  
    (NULL ,  'Comprar pasteles',  '1'),
    (NULL ,  'Comprar rosas',  '2'),
    (NULL ,  'Comprar bombones',  '3'),
    (NULL ,  'Comprar peluches',  '4'),
    (NULL ,  'Comprar tarjeta',  '5');

Creo que ya está bien de SQL por ahora, y lo que vamos a hacer es pasar al código PHP/HTML que va a contener nuestra lista.

<?php  
    $link = mysqli_connect('servidor', 'usuario', 'contraseña', 'bdatos');
    $consulta = "SELECT * FROM elementos ORDER BY orden";
    $resultado = mysqli_query($link, $consulta);
    $elementos = null;
    while ($datos = mysqli_fetch_assoc($resultado)){
        $elementos[$datos['id']] = $datos['nombre'];
    }
?>
<!DOCTYPE html>  
<html>  
<head>  
    <title>Lista editable y ordenable</title>
    <meta charset="utf-8">
    <link href='http://fonts.googleapis.com/css?family=Lilita+One' rel='stylesheet' type='text/css'>
    <link href="pagina.css" rel="stylesheet" type="text/css" media="all">    
</head>  
<body>  
    <div id="wrapper">
        <h1>Lista de tareas</h1>
        <ul id="lista">
            <?php 
                foreach ($elementos as $id => $nombre)
                    echo '<li id="elemento-'.$id.'" contenteditable="true">'.$nombre.'</li>';
            ?>
        </ul>
        <div id="form">
            <input type="radio" name="editar-ordenar" id="editar1" value="editar" checked="checked"/>
            <label for="editar1">Editar</label>
            <input type="radio" name="editar-ordenar" value="ordenar" id="ordenar1"/>
            <label for="ordenar1">Ordenar</label>
            <form id="formulario" method="post">
                <input type="text" id="campo-nombre" name="nombre" placeholder="Nuevo elemento">
                <input type="submit" value="Añadir">
            </form>           
        </div>

    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
</body>  
</html>  

Como veis, el código es bastante sencillo y estándar.  Vamos a revisar la parte de PHP primero:

  • Primero establecemos un enlace a la base de datos, que nos devuelve un $link
  • Luego, almacenamos la consulta en una cadena y en la que le pedimos todos los elementos de la tabla elementos y que nos lo ordene por el campo orden. Guardamos el resultado de la ejecución en la variable $resultado.
  • Después, inicializamos la variable $elementos que es donde vamos a almacenar todos los elementos de nuestra lista para, acto seguido, comenzar un bucle en el que extraer todos los datos de la consulta que acabamos de realizar.
  • Finalmente, y un poco más abajo, con un bucle foreach, vamos incluyendo cada elemento dentro de la lista#lista.

Si os habéis fijado, usamos el atributo contenteditable=”true” el cual nos permite editar el texto del elemento li sobre el que hagamos click.

Al final hay un formulario para añadir nuevos elementos y elegir entre ordenar y editar elementos. Por limitaciones de contenteditable, no nos es posible hacer click para arrastrar y ordenar si un elemento tiene este atributo así que tendremos que ir alternando con los botones de opciones.

Este es el código que tiene la lista:

<ul id="lista">  
  <li id="elemento-1" contenteditable="true">Comprar pasteles</li>
  <li id="elemento-2" contenteditable="true">Comprar rosas</li>
  <li id="elemento-3" contenteditable="true">Comprar bombones</li>
  <li id="elemento-4" contenteditable="true">Comprar peluches</li>
  <li id="elemento-5" contenteditable="true">Comprar tarjeta</li>
</ul>  

Ahora, antes de ponernos manos a la obra con JavaScript, vamos a hacer el script PHP que se encargará de insertar, editar, eliminar y reordenar los elementos en la base de datos.

<?php  
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') && isset($_POST['accion'])){  
    $link = mysqli_connect('servidor', 'usuario', 'contraseña', 'bdatos');

    $devolver = null;
    $consulta = '';
    $accion = $_POST['accion'];
    switch($accion){
        case 'insertar':{ // Inserción de un nuevo elemento
            $nombre = mysqli_real_escape_string($link, $_POST['nombre']);
            $orden = mysqli_real_escape_string($link, $_POST['orden']);
            $consulta = "INSERT INTO elementos (nombre, orden) VALUES ('".$nombre."', ".$orden.") ";
            if (mysqli_query($link, $consulta)){
                $devolver = array ('valor' => mysqli_insert_id($link));
            }
            break;
        }
        case 'eliminar':{ // Eliminación de un nuevo elemento
            $id = mysqli_real_escape_string($link, $_POST['id']);
            $orden = mysqli_real_escape_string($link, $_POST['orden']);




            $consulta = 'DELETE FROM elementos WHERE id = '.$id;
            if (mysqli_query($link, $consulta)){
                $consulta = "UPDATE elementos SET orden = orden -1 WHERE orden > ".$orden;
                mysqli_query($link,$consulta);
                $devolver = array ('realizado' => true);
            }           
            break;
        }
        case 'editar':{ // Edición de un elemento
            $id = mysqli_real_escape_string($link, $_POST['id']);
            $nombre = mysqli_real_escape_string($link, $_POST['nombre']);
            $consulta = "UPDATE elementos SET nombre = '".$nombre."' WHERE id = ".$id;
            if (mysqli_query($link, $consulta)){
                $devolver = array ('realizado' => true);
            }
            break;
        }
        case 'ordenar':{ // Ordenar los elementos
            $puntos = explode(',',$_POST['puntos']);
            $consulta = 'UPDATE elementos SET orden = CASE id '.PHP_EOL;
            foreach ($puntos as $index => $id){
              $idPunto = explode('-', $id);
              $idPunto = mysqli_real_escape_string($link,$idPunto[1]);
              $orden = mysqli_real_escape_string($link, ($index + 1));
                $consulta .= 'WHEN '.$idPunto.' THEN '.$orden.''.PHP_EOL;
            }
            $consulta .= 'ELSE orden'.PHP_EOL.'END';
            echo $consulta;
            if (mysqli_query($link, $consulta)){
                $devolver = array ('realizado' => true);
            }           
            break;
        }
    }
    if ($devolver)
        echo json_encode($devolver);
}
else {  
    die('No se está accediendo correctamente');
}
?>

Este código es un poco más complejo y, aunque se podría optimizar más, he querido dejarlo así por simplicidad y comprensión. Vamos a desgranarlo paso a paso.

  1. Comprobamos que la petición venga desde Ajax y que la variable $_POST contenga el valor acción. En caso de que no sea así, no hace nada. Esto es una medida de seguridad para evitar accesos indeseados a nuestro controlador.
  2. Conectamos con la base de datosy obtenemos la acción a realizar. Todos los pasos siguientes sanean primero los datos, para evitar inyecciones de SQL en nuestro código. 1. En caso de que vayamos a insertar, la consulta es bastante sencilla y almacenamos en $devolver la id del elemento que acabamos de insertar.
  3. Eliminar no tiene mayor complicación, pero veréis que se ejecuta una segunda consulta. Esta consulta reajusta los órdenes para que todos los elementos cuyo orden fuera superior al eliminado, resten 1 a su actual orden.
  4. Editar no tiene complejidad y únicamente nos aseguramos de que vamos a devolver realizado (en caso de que todo haya ido bien).
  5. En el caso de ordenar es un poco más complejo ya que la consulta que vamos a realizar conlleva condicionales para actualizar el valor orden en todos los valores, dependiendo de la id.  La cadena recibida es similar a esta: ‘elemento-2,elemento-1,elemento-3,elemento-5′por lo que es necesario separar los elementos por ‘,’ y por ‘-’. Un ejemplo de consulta realizada en este caso: UPDATE elementos SET orden = CASE id WHEN 1 THEN 3 WHEN 2 THEN 1 WHEN 3 THEN 2 WHEN 4 THEN 5 WHEN 5 THEN 4 ELSE orden END
  6. En caso de que todo haya ido bien, devolvemos como Json lo almacenado en la variable $devolver.

Vamos ahora con la última parte aunque quizá sea la más compleja, la parte JavaScript.

$(function(){
    var formulario = $('#formulario'), ordenando = false, lista = $('#lista'),
            elementos = lista.find('li');
    lista.sortable({
        update: function(event,ui){
            var ordenPuntos = $(this).sortable('toArray').toString();
            $.ajax({
                type: 'POST',
                url: 'controlador.php',
                dataType: 'json',
                data: {
                    accion: 'ordenar',
                    puntos: ordenPuntos
                }
            });
        }
    });
    lista.sortable('disable');
    $('input[name="editar-ordenar"]').on('change', function(){
        if ($(this).val() == 'ordenar'){
            lista.sortable('enable');
            elementos.attr('contenteditable',false);
            ordenando = true;
        }
        else{
            lista.sortable('disable');
            elementos.attr('contenteditable',true);
            ordenando = false;
        }
    });


    formulario.on('submit',function(evento){ //Cuando el formulario se envía, vamos a insertar
        evento.preventDefault();
        var nombre = $('#campo-nombre').val();
        $('#campo-nombre').val('');

        $.ajax({
            type: 'POST',
            url: 'controlador.php',
            dataType: 'json',
            data: {
                accion: 'insertar',
                nombre: nombre,
                orden: elementos.length + 1 // El orden es el número de elementos + 1
            },
            success: function (devolver){
                if (devolver.valor){
                    $('<li>',{
                        id : 'elemento-' + devolver.valor,
                        'class': ordenando ? 'ordenable' : '',
                        text: nombre,
                        'contenteditable' : true
                    }).hide().appendTo($('#lista')).fadeIn('slow');
                }
            }
        });
    });
    lista.on('keydown', 'li', function(evento){
        var punto = $(this);
        var idPunto = punto.attr('id').split('-');
        idPunto = idPunto[1];

        switch(evento.keyCode){
            case 27:{ //Escape
                document.execCommand('undo');
                punto.blur();
                break;
            }
            case 46:{ //Suprimir
                if (confirm('¿Seguro que quiere eliminar este elemento?')){
                    $.ajax({
                        type: 'POST',
                        data: {
                            accion: 'eliminar',
                            orden: punto.index(),
                            id: idPunto
                        },
                        url: 'controlador.php',
                        success: function(e){
                            punto.fadeOut('slow').remove();
                        }
                    });
                }
                break;
            }
            case 13:{ //Enter
                evento.preventDefault();
                var texto = punto.text();
                punto.blur();
                $.ajax({
                    type: 'POST',
                    data: {
                        accion: 'editar',
                        id: idPunto,
                        nombre: texto
                    },
                    url: 'controlador.php'
                });
                break;
            }
        }
    });
});

Son unas 145 líneas de código y vamos a pasar a desgranarlas poco a poco.

  1. Nos encargamos de establecer algunas variables en las cuales almacenamos algunos objetos jQuery que usaremos más tarde.
  2. Luego, hacemos que los elementos de lista, que contiene el elemento ul con id #lista, se puedan ordenar y le añadimos una función gestora del evento update, es decir, cuando actualizamos el orden de la lista. Esta función toma el orden actual de los elementos gracias el método toArray de sortable, que convierte en una matriz, todos los elementos en su actual orden. Luego, convertimos esta matriz en cadena, usando el método toString(). A partir de aquí, realizamos una petición Ajax a nuestro controlador indicándole que vamos a ordenar elementos y le pasamos el orden actual de puntos.Finalmente, desactivamos el hecho de que la lista se pueda ordenar, ya que el estado inicial de nuestra lista permite editar y no ordenar.
  3. Asignamos una función gestora del evento change para los botones de opciones que cambian entre ordenar y editar. En caso de que vayamos a ordenar, activamos la lista como sortable, establecemos el atributo contenteditable a false de todos los elementos de la lista, y dejamos la variable ordenando como false. En caso contrario, hacemos justo el proceso opuesto.
  4. La siguiente función escucha el evento submit de nuestro formulario, lo cual implica que vamos a insertar un nuevo elemento en la lista. En este caso, obtenemos los datos que nos facilita el usuario y, si la inserción tiene éxito, añadimos un nuevo elemento li a nuestra lista, usando la id que nos devuelve nuestro controlador. Además, le damos un efecto de desvanecimiento a la hora de añadirlo para hacerlo un poco más bonito.
  5. Finalmente, asignamos una función gestora del evento keydown sobre todos los elementos de la lista (presentes y futuros) para el caso en que estemos editándola. Tras obtener datos básicos estamos pendientes de tres códigos en particular de teclas:
  6. 27 (Escape): Si pulsamos escape, el navegador ejecuta el comando deshacer, descartando cualquier texto escrito, y perdemos el foco sobre el elemento.
  7. 46 (Suprimir): Si pulsamos suprimir, se nos preguntará si queremos eliminar este elemento, en caso afirmativo, enviaremos una petición a nuestro controlador y, al completarse, eliminaremos el el elemento de la lista.
  8. 13 (Enter): Al pulsar Enter, se entiende que queremos guardar los cambios realizados. Para evitar insertar un salto de línea, usamos preventDefault sobre el evento. Finalmente, realizamos una petición Ajax a nuestro controlador, que se encargará de hacer los honores y modificar el texto.

Como podéis observar, el proceso no es difícil y el producto puede ser útil en multitud de ocasiones. Si aun no controláis sobre peticiones Ajax, os recomiendo nuestra introducción a Ajax.

La demo, aunque obtiene los datos de la base de datos, no permite insertar ni borrar elementos para evitar problemas.

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.