Lista de tareas pendientes con AJAX con PHP, MySQL y jQuery

En este tutorial, estamos creando una aplicación de lista de tareas sencilla basada en AJAX, con PHP, MySQL y jQuery. En el proceso, demostraremos las capacidades de programación orientada a objetos de PHP, jugaremos con la interfaz de usuario de jQuery e implementaremos algunas funciones agradables de AJAX.

Para comprender mejor los pasos de este tutorial, puede continuar y descargar el archivo de demostración disponible desde el botón de arriba.

Paso 1 - PHP

Como este es más un tutorial orientado al desarrollador, vamos a comenzar con la parte de PHP. A diferencia de los tutoriales anteriores, esta vez estamos aprovechando las características de programación orientada a objetos de PHP 5.

Toda la funcionalidad disponible para el usuario final (crear, editar, eliminar y reordenar los elementos de tareas pendientes) se implementa como diferentes métodos de una clase, que se explican en detalle a continuación.

todo.clase.php - Parte 1

/* Defining the ToDo class */

class ToDo{

    /* An array that stores the todo item data: */

    private $data;

    /* The constructor */
    public function __construct($par){
        if(is_array($par))
            $this->data = $par;
    }

    /*
        This is an in-build "magic" method that is automatically called
        by PHP when we output the ToDo objects with echo.
    */

    public function __toString(){

        // The string we return is outputted by the echo statement

        return '
            <li id="todo-'.$this->data['id'].'" class="todo">

                <div class="text">'.$this->data['text'].'</div>

                <div class="actions">
                    <a href="" class="edit">Edit</a>
                    <a href="" class="delete">Delete</a>
                </div>

            </li>';
    }

El constructor toma la matriz pasada como parámetro y la almacena en $data propiedad de la clase. Esta matriz es una fila extraída de la base de datos con mysql_fetch_assoc() y contiene el id y el texto del elemento pendiente.

Después de esto está la magia __toString() método, que se llama internamente cuando intentamos hacer eco de un objeto de esta clase. La cadena que devuelve contiene el marcado utilizado por cada elemento de tarea:un

  • elemento con una identificación única y un nombre de clase "todo", dentro del cual tenemos el texto del todo y los dos hipervínculos de acción.

    todo.class.php - Parte 2

     /*
            The edit method takes the ToDo item id and the new text
            of the ToDo. Updates the database.
        */
    
        public static function edit($id, $text){
    
            $text = self::esc($text);
            if(!$text) throw new Exception("Wrong update text!");
    
            mysql_query("   UPDATE tz_todo
                            SET text='".$text."'
                            WHERE id=".$id
                        );
    
            if(mysql_affected_rows($GLOBALS['link'])!=1)
                throw new Exception("Couldn't update item!");
        }
    
        /*
            The delete method. Takes the id of the ToDo item
            and deletes it from the database.
        */
    
        public static function delete($id){
    
            mysql_query("DELETE FROM tz_todo WHERE id=".$id);
    
            if(mysql_affected_rows($GLOBALS['link'])!=1)
                throw new Exception("Couldn't delete item!");
        }
    
        /*
            The rearrange method is called when the ordering of
            the todos is changed. Takes an array parameter, which
            contains the ids of the todos in the new order.
        */
    
        public static function rearrange($key_value){
    
            $updateVals = array();
            foreach($key_value as $k=>$v)
            {
                $strVals[] = 'WHEN '.(int)$v.' THEN '.((int)$k+1).PHP_EOL;
            }
    
            if(!$strVals) throw new Exception("No data!");
    
            // We are using the CASE SQL operator to update the ToDo positions en masse:
    
            mysql_query("   UPDATE tz_todo SET position = CASE id
                            ".join($strVals)."
                            ELSE position
                            END");
    
            if(mysql_error($GLOBALS['link']))
                throw new Exception("Error updating positions!");
        }

    La definición de la clase continúa con una serie de métodos estáticos. Esos son métodos especiales, a los que se puede acceder sin necesidad de crear un objeto de la clase. Por ejemplo, puede llamar al método de edición escribiendo:ToDo::edit($par1,$par2) .

    Observe cómo estamos usando excepciones para manejar errores. Cuando ocurre una excepción, la ejecución del script se detiene y depende del resto del script detectarlo y generar el estado apropiado.

    También puede encontrar interesante la forma en que estamos actualizando la base de datos con las nuevas posiciones de los elementos pendientes. Estamos usando el CASE operador, disponible en MySQL. De esta manera, no importa cuántos todos haya en la base de datos, solo ejecutamos una consulta.

    todo.class.php - Parte 3

     /*
            The createNew method takes only the text of the todo as a parameter,
            writes to the database and outputs the new todo back to
            the AJAX front-end.
        */
    
        public static function createNew($text){
    
            $text = self::esc($text);
            if(!$text) throw new Exception("Wrong input data!");
    
            $posResult = mysql_query("SELECT MAX(position)+1 FROM tz_todo");
    
            if(mysql_num_rows($posResult))
                list($position) = mysql_fetch_array($posResult);
    
            if(!$position) $position = 1;
    
            mysql_query("INSERT INTO tz_todo SET text='".$text."', position = ".$position);
    
            if(mysql_affected_rows($GLOBALS['link'])!=1)
                throw new Exception("Error inserting TODO!");
    
            // Creating a new ToDo and outputting it directly:
    
            echo (new ToDo(array(
                'id'    => mysql_insert_id($GLOBALS['link']),
                'text'  => $text
            )));
    
            exit;
        }
    
        /*
            A helper method to sanitize a string:
        */
    
        public static function esc($str){
    
            if(ini_get('magic_quotes_gpc'))
                $str = stripslashes($str);
    
            return mysql_real_escape_string(strip_tags($str));
        }
    
    } // closing the class definition
    

    El acceso a métodos estáticos de la misma clase se puede hacer fácilmente con self:: palabra clave. De esta manera estamos usando esc() método para desinfectar los datos de usuario entrantes.

    Observe también el createNew() método. En él, después de ejecutar la consulta INSERT en la base de datos, usamos la identificación única asignada automáticamente devuelta con mysql_insert_id() y crea un nuevo objeto de tareas pendientes, que luego se repite en el front-end.

    Ahora echemos un vistazo a cómo se usa esta clase.

    demo.php - Parte 1

    // Select all the todos, ordered by position:
    $query = mysql_query("SELECT * FROM `tz_todo` ORDER BY `position` ASC");
    
    $todos = array();
    
    // Filling the $todos array with new ToDo objects:
    
    while($row = mysql_fetch_assoc($query)){
        $todos[] = new ToDo($row);
    }

    Después de incluir todo.class.php en demo.php , seleccionamos los elementos de tareas pendientes y recorremos el conjunto de resultados de MySQL, completando $todos matriz con objetos.

    demo.php - Parte 2

    // Looping and outputting the $todos array. The __toString() method
    // is used internally to convert the objects to strings:
    
    foreach($todos as $item){
        echo $item;
    }

    Más adelante en la página, estos objetos se repiten. Gracias a __toString() método discutido anteriormente, todo el marcado se genera automáticamente, por lo que no tenemos que lidiar con nada de eso.

    El front-end emite varias llamadas AJAX diferentes. Hacer un archivo separado para manejar cada uno de ellos sería un poco exagerado, por lo que la mejor solución es agruparlos en un solo archivo de manejo AJAX. Esto se hace en ajax.php , que puedes ver a continuación.

    ajax.php

    $id = (int)$_GET['id'];
    
    try{
    
        switch($_GET['action'])
        {
            case 'delete':
                ToDo::delete($id);
                break;
    
            case 'rearrange':
                ToDo::rearrange($_GET['positions']);
                break;
    
            case 'edit':
                ToDo::edit($id,$_GET['text']);
                break;
    
            case 'new':
                ToDo::createNew($_GET['text']);
                break;
        }
    
    }
    catch(Exception $e){
    //  echo $e->getMessage();
        die("0");
    }
    
    echo "1";

    Con la ayuda de una instrucción switch, decidimos cuál de los métodos estáticos de la clase ToDo ejecutar. Si se produce un error en uno de estos métodos, se envía una excepción. Porque todo el interruptor está encerrado en una instrucción de prueba , la ejecución de la secuencia de comandos se detiene y el control pasa a la declaración catch, que genera un cero y sale de la secuencia de comandos.

    Podría repetir (o escribir en un registro) exactamente qué tipo de error ocurrió al descomentar la línea 26.

    Paso 2:MySQL

    El tz_todo la tabla contiene y asigna los identificadores únicos de los elementos pendientes (a través de la configuración de incremento automático del campo), la posición, el texto y la marca de tiempo dt_added.

    Puede encontrar el SQL que recreará la tabla en table.sql en el archivo de descarga. Además, si planea ejecutar la demostración en su propio servidor, no olvide completar sus datos de inicio de sesión en connect.php .

    Paso 3 - XHTML

    Como PHP genera la mayor parte del marcado, nos quedamos con el resto del XHTML de la página. Primero necesitamos incluir jQuery , interfaz de usuario de jQuery y las hojas de estilo en el documento. Se considera una buena práctica incluir las hojas de estilo en la sección principal y los archivos JavaScript justo antes del cierre etiqueta.

    <link rel="stylesheet" href="jquery-ui.css" type="text/css" />
    <link rel="stylesheet" type="text/css" href="styles.css" />
    
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="jquery-ui.min.js"></script>
    <script type="text/javascript" src="script.js"></script>

    Después de esto, podemos pasar a codificar el resto de la página.

    demo.php

    <div id="main">
    
        <ul class="todoList">
    
        <?php
    
            // Looping and outputting the $todos array. The __toString() method
            // is used internally to convert the objects to strings:
    
            foreach($todos as $item){
                echo $item;
            }
    
            ?>
    
        </ul>
    
        <a id="addButton" class="green-button" href="">Add a ToDo</a>
    
    </div>
    
    <!-- This div is used as the base for the confirmation jQuery UI dialog box. Hidden by CSS. -->
    <div id="dialog-confirm" title="Delete TODO Item?">Are you sure you want to delete this TODO item?</div>

    Cada tarea es un li elemento dentro de todoList lista desordenada. De esta manera, podemos usar más tarde el método ordenable de jQuery UI para convertirlo fácilmente en un elemento ordenable interactivo. Además, en el proceso, mejoramos el valor semántico del código.

    Paso 4 - CSS

    Ahora pasemos al estilo de todos. Aquí solo se proporcionan partes de la hoja de estilo original para una mejor legibilidad. Puedes encontrar el resto en styles.css en el archivo de descarga.

    estilos.css - Parte 1

    /* The todo items are grouped into an UL unordered list */
    
    ul.todoList{
        margin:0 auto;
        width:500px;
        position:relative;
    }
    
    ul.todoList li{
        background-color:#F9F9F9;
        border:1px solid #EEEEEE;
        list-style:none;
        margin:6px;
        padding:6px 9px;
        position:relative;
        cursor:n-resize;
    
        /* CSS3 text shadow and rounded corners: */
    
        text-shadow:1px 1px 0 white;
    
        -moz-border-radius:6px;
        -webkit-border-radius:6px;
        border-radius:6px;
    }
    
    ul.todoList li:hover{
        border-color:#9be0f9;
    
        /* CSS3 glow effect: */
        -moz-box-shadow:0 0 5px #A6E5FD;
        -webkit-box-shadow:0 0 5px #A6E5FD;
        box-shadow:0 0 5px #A6E5FD;
    }
    

    La lista de tareas pendientes ul se centra horizontalmente en la página y se le asigna un posicionamiento relativo. El li elementos dentro de él (los elementos de tareas pendientes) comparten una serie de reglas CSS3. Desafortunadamente, estos no funcionan en navegadores más antiguos, pero como son únicamente para fines de presentación, incluso navegadores tan antiguos como IE6 pueden disfrutar de un script completamente funcional, aunque no tan bonito como se pretendía.

    estilos.css - Parte 2

    /* The edit textbox */
    
    .todo input{
        border:1px solid #CCCCCC;
        color:#666666;
        font-family:Arial,Helvetica,sans-serif;
        font-size:0.725em;
        padding:3px 4px;
        width:300px;
    }
    
    /* The Save and Cancel edit links: */
    
    .editTodo{
        display:inline;
        font-size:0.6em;
        padding-left:9px;
    }
    
    .editTodo a{
        font-weight:bold;
    }
    
    a.discardChanges{
        color:#C00 !important;
    }
    
    a.saveChanges{
        color:#4DB209 !important;
    }

    En la segunda parte del código, diseñamos el cuadro de texto de entrada, que se muestra cuando se edita el elemento de tareas pendientes, y los enlaces para guardar y cancelar.

    Paso 5:jQuery

    Pasando al código JavaScript. Aquí estamos usando dos de los componentes de la interfaz de usuario de jQuery UI:ordenable y diálogo . Estos solos nos ahorran al menos un par de horas de tiempo de desarrollo, que es uno de los beneficios de usar una biblioteca bien pensada como jQuery.

    script.js - Parte 1

    $(document).ready(function(){
        /* The following code is executed once the DOM is loaded */
    
        $(".todoList").sortable({
            axis        : 'y',              // Only vertical movements allowed
            containment : 'window',         // Constrained by the window
            update      : function(){       // The function is called after the todos are rearranged
    
                // The toArray method returns an array with the ids of the todos
                var arr = $(".todoList").sortable('toArray');
    
                // Striping the todo- prefix of the ids:
    
                arr = $.map(arr,function(val,key){
                    return val.replace('todo-','');
                });
    
                // Saving with AJAX
                $.get('ajax.php',{action:'rearrange',positions:arr});
            }
        });
    
        // A global variable, holding a jQuery object
        // containing the current todo item:
    
        var currentTODO;
    
        // Configuring the delete confirmation dialog
        $("#dialog-confirm").dialog({
            resizable: false,
            height:130,
            modal: true,
            autoOpen:false,
            buttons: {
                'Delete item': function() {
    
                    $.get("ajax.php",{"action":"delete","id":currentTODO.data('id')},function(msg){
                        currentTODO.fadeOut('fast');
                    })
    
                    $(this).dialog('close');
                },
                Cancel: function() {
                    $(this).dialog('close');
                }
            }
        });

    Para mostrar el cuadro de diálogo, necesitamos tener un div base, que se convertirá en un cuadro de diálogo. El contenido del div se mostrará como el texto del diálogo, y el contenido del atributo de título del div se convertirá en el título de la ventana de diálogo. Puede encontrar este div (id=dialog-confirm ) en demo.php .

    script.js - Parte 2

      // When a double click occurs, just simulate a click on the edit button:
        $('.todo').live('dblclick',function(){
            $(this).find('a.edit').click();
        });
    
        // If any link in the todo is clicked, assign
        // the todo item to the currentTODO variable for later use.
    
        $('.todo a').live('click',function(e){
    
            currentTODO = $(this).closest('.todo');
            currentTODO.data('id',currentTODO.attr('id').replace('todo-',''));
    
            e.preventDefault();
        });
    
        // Listening for a click on a delete button:
    
        $('.todo a.delete').live('click',function(){
            $("#dialog-confirm").dialog('open');
        });
    
        // Listening for a click on a edit button
    
        $('.todo a.edit').live('click',function(){
    
            var container = currentTODO.find('.text');
    
            if(!currentTODO.data('origText'))
            {
                // Saving the current value of the ToDo so we can
                // restore it later if the user discards the changes:
    
                currentTODO.data('origText',container.text());
            }
            else
            {
                // This will block the edit button if the edit box is already open:
                return false;
            }
    
            $('<input type="text">').val(container.text()).appendTo(container.empty());
    
            // Appending the save and cancel links:
            container.append(
                '<div class="editTodo">'+
                    '<a class="saveChanges" href="">Save</a> or <a class="discardChanges" href="">Cancel</a>'+
                '</div>'
            );
    
        });

    Observe el uso de jQuery live() método para enlazar eventos. Estamos usando live() , en lugar de bind() , porque live() puede escuchar eventos en cualquier elemento, incluso aquellos que aún no existen. De esta manera, nos aseguramos de que todos los elementos pendientes agregados en el futuro a la página por el usuario también activarán los mismos controladores de eventos que los existentes actualmente.

    script.js - Parte 3

      // The cancel edit link:
    
        $('.todo a.discardChanges').live('click',function(){
            currentTODO.find('.text')
                        .text(currentTODO.data('origText'))
                        .end()
                        .removeData('origText');
        });
    
        // The save changes link:
    
        $('.todo a.saveChanges').live('click',function(){
            var text = currentTODO.find("input[type=text]").val();
    
            $.get("ajax.php",{'action':'edit','id':currentTODO.data('id'),'text':text});
    
            currentTODO.removeData('origText')
                        .find(".text")
                        .text(text);
        });
    
        // The Add New ToDo button:
    
        var timestamp;
        $('#addButton').click(function(e){
    
            // Only one todo per 5 seconds is allowed:
            if(Date.now() - timestamp<5000) return false;
    
            $.get("ajax.php",{'action':'new','text':'New Todo Item. Doubleclick to Edit.'},function(msg){
    
                // Appending the new todo and fading it into view:
                $(msg).hide().appendTo('.todoList').fadeIn();
            });
    
            // Updating the timestamp:
            timestamp = Date.now();
    
            e.preventDefault();
        });
    
    }); // Closing $(document).ready()

    En la última parte del código, vinculamos eventos a Guardar y Cancelar enlaces, que se agregan al todo al editarlo. También configuramos un detector de eventos para "Agregar" botón. Observe cómo evitamos la inundación al limitar la tasa de envío de todos nuevos a uno cada 5 segundos.

    ¡Con esto, nuestra lista de tareas pendientes de AJAX está completa!

    Conclusión

    Hoy creamos un simple script web ToDo habilitado para AJAX con PHP, MySQL y jQuery. Puede usarlo para crear su propia aplicación de gestión de tareas o convertirlo en una aplicación web completa.

    ¿Qué opinas? ¿Cómo modificarías este código?