Aplicación de lista de tareas con tecnología de WordPress

WordPress es una herramienta increíble para crear un blog. Con su vasto mar de complementos y temas, combinado con una API que se extiende a todos los rincones del sistema, puede crear casi cualquier tipo de aplicación web con él. Esto lo hace avanzar de un sistema de administración de contenido a una plataforma completa para crear aplicaciones web.

En este tutorial, vamos a crear un complemento de WordPress que se conecte a la API. A continuación, presentará una aplicación de lista de tareas simple, editada por AJAX, en el /todo URL de su sitio de WordPress. Lo mejor es que este es un complemento y no un tema, lo que significa que puede usarlo en cualquier sitio de WordPress independientemente del tema. ¡Empecemos!

Tu primer complemento de WordPress

Si no ha escrito un complemento de WordPress antes, esto es lo que necesita saber:

  • Los complementos son archivos PHP que residen en la carpeta /wp-content/plugins;
  • Los complementos pueden ser un solo archivo PHP con un nombre único o una carpeta con ese archivo dentro, junto con recursos e inclusiones adicionales (lea la guía de inicio);
  • Los complementos se describen mediante un encabezado de comentario en el archivo PHP principal. Necesita esto para que su complemento sea reconocido;
  • Los complementos hacen su trabajo conectándose a eventos específicos en la ejecución de WordPress. Hay una referencia con todos los filtros y acciones disponibles;
  • La documentación es tu amiga.

Para desarrollar un complemento, instale una copia local de WordPress y cree un nuevo archivo PHP en la carpeta /wp-content/plugins. Después de esto, coloque el comentario de encabezado que ve a continuación y active su complemento desde el panel de administración.

Si solo desea probar la aplicación Todo que estamos escribiendo hoy, simplemente puede obtener el zip de descarga e instalarlo desde el panel de administración de WordPress (elija Complementos->Cargar ).

Aquí está el archivo de complemento principal para nuestra aplicación:

tz-todoapp.php

/*
Plugin Name: Todo App
Plugin URI: https://tutorialzine.com
Description: This is a demo plugin for a Tutorialzine tutorial
Version: 1.0
Author: Martin Angelov
Author URI: https://tutorialzine.com
License: GPL2
*/

define('TZ_TODO_FILE', __FILE__);
define('TZ_TODO_PATH', plugin_dir_path(__FILE__));

require TZ_TODO_PATH.'includes/tzTodo.php';

new tzTodo();

Puede ver el comentario del encabezado en la parte superior. Esta es la descripción que el sistema utiliza para presentar en la página de activación del complemento. Después de esto, estamos definiendo dos constantes para el archivo del complemento (se usa como identificador en algunas llamadas a funciones) y la ruta de la carpeta. Después de esto, estamos incluyendo la clase tzTodo y creando un nuevo objeto.

La clase tzTodo

Esta clase contiene toda la funcionalidad del complemento. En su constructor, nos conectamos a una serie de acciones:para la inicialización, ajax y el diseño del tipo de publicación personalizada que definiremos. En el cuerpo de la clase tenemos métodos que realizan una funcionalidad útil cuando se activan estas acciones y también definen un tipo de publicación personalizada 'tz_todo '.

incluye/tzTodo.php

class tzTodo {

    public function __construct(){

        add_action( 'init', array($this,'init'));

        // These hooks will handle AJAX interactions. We need to handle
        // ajax requests from both logged in users and anonymous ones:

        add_action('wp_ajax_nopriv_tz_ajax', array($this,'ajax'));
        add_action('wp_ajax_tz_ajax', array($this,'ajax'));

        // Functions for presenting custom columns on
        // the custom post view for the todo items

        add_filter( "manage_tz_todo_posts_columns", array($this, 'change_columns'));

        // The two last optional arguments to this function are the
        // priority (10) and number of arguments that the function expects (2):

        add_action( "manage_posts_custom_column", array($this, "custom_columns") , 10, 2 );
    }

    public function init(){

        // When a URL like /todo is requested from the,
        // blog we will directly include the index.php
        // file of the application and exit 

        if( preg_match('/\/todo\/?$/',$_SERVER['REQUEST_URI'])){
            $base_url = plugins_url( 'app/' , TZ_TODO_FILE);
            require TZ_TODO_PATH.'/app/index.php';
            exit;
        }

        $this->add_post_type();
    }

    // This method is called when an
    // AJAX request is made to the plugin

    public function ajax(){
        $id = -1;
        $data = '';
        $verb = '';

        $response = array();

        if(isset($_POST['verb'])){
            $verb = $_POST['verb'];
        }

        if(isset($_POST['id'])){
            $id = (int)$_POST['id'];
        }

        if(isset($_POST['data'])){
            $data = wp_strip_all_tags($_POST['data']);
        }

        $post = null;

        if($id != -1){
            $post = get_post($id);

            // Make sure that the passed id actually
            // belongs to a post of the tz_todo type

            if($post && $post->post_type != 'tz_todo'){
                exit;
            }
        }

        switch($verb){
            case 'save':

                $todo_item = array(
                    'post_title' => $data,
                    'post_content' => '',
                    'post_status' => 'publish',
                    'post_type' => 'tz_todo',
                );

                if($post){

                    // Adding an id to the array will cause
                    // the post with that id to be edited
                    // instead of a new entry to be created.

                    $todo_item['ID'] = $post->ID;
                }

                $response['id'] = wp_insert_post($todo_item);
            break;

            case 'check':

                if($post){
                    update_post_meta($post->ID, 'status', 'Completed');
                }

            break;

            case 'uncheck':

                if($post){
                    delete_post_meta($post->ID, 'status');
                }

            break;

            case 'delete':
                if($post){
                    wp_delete_post($post->ID);
                }
            break;
        }

        // Print the response as json and exit

        header("Content-type: application/json");

        die(json_encode($response));

    }

    private function add_post_type(){

        // The register_post_type function
        // will make a new Todo item entry
        // in the wordpress admin menu

        register_post_type( 'tz_todo',
            array(
                'labels' => array(
                    'name' => __( 'Todo items' ),
                    'singular_name' => __( 'Todo item' )
                ),
                'public' => true,
                'supports' => array('title') // Only a title is allowed for this type
            )
        );
    }

    public function change_columns($cols){

        // We need to customize the columns
        // shown when viewing the Todo items
        // post type to include a status field

        $cols = array(
            'cb'       => '<input type="checkbox" />',
            'title'      => __( 'Task' ),
            'status' => __( 'Status' ),
            'date'     => __( 'Date' ),
        );

        return $cols;
    }

    public function custom_columns( $column, $post_id ) {

        // Add content to the status column

        switch ( $column ) {

            case "status":
                // We are requesting the status meta item

                $status = get_post_meta( $post_id, 'status', true);

                if($status != 'Completed'){
                    $status = 'Not completed';
                }

                echo $status;

                break;
        }
    }

}

El método más interesante es probablemente el de AJAX. Aquí estamos recibiendo solicitudes de AJAX que se envían desde la interfaz de jQuery. Dependiendo de la acción a realizar, creamos o eliminamos un elemento del tz_todo escriba el tipo de publicación personalizada y adjunte o elimine metadatos para marcar la tarea como completada. El mérito es de Joost de Valk por sus fragmentos útiles.

En el método init(), puede ver un truco que estoy usando para servir el archivo index.php desde la carpeta de la aplicación del complemento, cuando se solicita la URL /todo. Estoy haciendo coincidir el $_SERVER['REQUEST_URI'] entrada con un patrón. Si la URL solicitada es la que nos interesa, se incluye el archivo index.php y se detiene la ejecución de WordPress. Ahora, cuando alguien visite el http://example.com/todo de su sitio potenciado por WordPress, verán la aplicación.

La aplicación Lista de tareas pendientes

Como viste arriba, al visitar el /todo URL, nuestro complemento incluye el /app/index.php . Este es el archivo que presenta la interfaz que ve en la demostración. Se muestra a continuación.

/aplicación/index.php

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Todo App Powered By WordPress | Tutorialzine Demo</title>

        <!-- This is important! It fixes the paths of the css and js files -->
        <base href="<?php echo $base_url ?>"></base>

        <!-- The stylesheets -->
        <link rel="stylesheet" href="assets/css/styles.css" />

        <script>

            // This is the URL where we need to make our AJAX calls.
            // We are making it available to JavaScript as a global variable.

            var ajaxurl = '<?php echo admin_url('admin-ajax.php')?>';
        </script>
    </head>

    <body>

        <div id="todo">
            <h2>Todo List <a href="#" class="add"
                title="Add new todo item!">✚</a></h2>
            <ul>
                <?php

                    $query = new WP_Query(
                        array( 'post_type'=>'tz_todo', 'order'=>'ASC')
                    );

                    // The Loop
                    while ( $query->have_posts() ) :
                        $query->the_post();
                        $done = get_post_meta(get_the_id(), 'status', true) ==
                            'Completed';
                    ?>

                        <li data-id="<?php the_id()?>"
                            class="<?php echo ($done ? 'done' : '')?>">
                            <input type="checkbox"
                                <?php echo ($done ? 'checked="true"' : '')?> />
                            <input type="text"
                                value="<?php htmlspecialchars(the_title())?>"
                                placeholder="Write your todo here" />
                            <a href="#" class="delete" title="Delete">✖</a>
                        </li>

                    <?php endwhile; ?>
            </ul>
        </div>

        <!-- JavaScript includes.  -->
        <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
        <script src="assets/js/script.js"></script>

    </body>
</html>

Aquí estamos usando la clase WP_Query para solicitar todas las publicaciones del tz_todo escriba en orden ascendente empezando por el más antiguo. Sigue un bucle while ordinario que puede reconocer si ha creado temas de WordPress.

En la parte inferior, tenemos la última versión de la biblioteca jQuery en el momento de escribir este artículo y nuestro archivo script.js, que controla la parte frontal de la aplicación.

El código jQuery

¡Nuestra aplicación Todo está casi terminada! Todo lo que tenemos que hacer es escribir el código jQuery que controla la interfaz:

/app/assets/js/script.js

$(function(){

    var saveTimer;
    var todoHolder = $('#todo');

    // Listen for the input event in the text fields:
    todoHolder.on('input','li input[type=text]', function(e){

        // This callback is run on every key press

        var todo = $(this),
            li = todo.closest('li');

        // We are clearing the save timer, so that
        // sending the AJAX request will only
        // happen once the user has stopped typing

        clearTimeout(saveTimer);

        saveTimer = setTimeout(function(){

            ajaxAction('save', li.data('id'), todo.val()).done(function(r){
                if(r.id != li.data('id')){
                    // The item has been written to the database
                    // for the first time. Update its id.
                    li.data('id', r.id);
                }
            });

        }, 1000);

    });

    // Listen for change events on the checkboxes
    todoHolder.on('change', 'li input[type=checkbox]',function(e){

        var checkbox = $(this),
            li = checkbox.closest('li');

        li.toggleClass('done',checkbox.is(':checked'));

        if(checkbox.is(':checked')){
            ajaxAction('check', li.data('id'));
        }
        else{
            ajaxAction('uncheck', li.data('id'));
        }

    });

    // Listen for clicks on the delete link
    todoHolder.on('click', 'li .delete',function(e){

        e.preventDefault();

        var li = $(this).closest('li');

        li.fadeOut(function(){
            li.remove();
        });

        if(li.data('id') != 0){

            // No need to delete items if they are missing an id.
            // This would mean that the item we are deleting
            // does not exist in the database, so the AJAX
            // request is unnecessary.
            ajaxAction('delete', li.data('id'));
        }

    });

    // Clicks on the add new item button)
    todoHolder.on('click','a.add', function(e){
        e.preventDefault();

        var item = $('<li data-id="0">'+
            '<input type="checkbox" /> <input type="text" val="" placeholder="Write your todo here" />'+
            '<a href="#" class="delete">✖</a>'+
            '</li>');

        todoHolder.find('ul').append(item);

        // We are not running an AJAX request when creating elements.
        // We are only writing them to the database when text is entered.
    });

    // A help function for running AJAX requests
    function ajaxAction(verb, id, data){

        // Notice that we are returning a deferred

        return $.post(ajaxurl, {
            'action': 'tz_ajax',
            'verb':verb,
            'id': id,
            'data': data
        }, 'json');

    }
});

El código está muy comentado, por lo que no debería ser difícil de entender. Estoy usando algunas de las funciones jQuery más nuevas, como diferidos y la sintaxis on() para registrar eventos.

¡Listo!

Esta aplicación le dará una buena idea de cómo es desarrollar complementos de WordPress. Como decía al principio, WP se está convirtiendo en una plataforma para desarrollar aplicaciones web. Después de todo, es fácil de configurar, tiene una excelente área de administración con roles de usuario y una multitud de complementos y temas de alta calidad.