martes, 10 de enero de 2017

Organización del código

Capítulo 10. Organización del código

10.1. Introducción

Cuando se emprende la tarea de realizar aplicaciones complejas del lado del cliente, es necesario considerar la forma en que se organizará el código. Este capitulo está dedicado a analizar algunos patrones de organización de código para utilizar en una aplicación realizada con jQuery. Además se explorará el sistema de gestión de dependencias de RequireJS.

10.1.1. Conceptos clave

Antes de comenzar con los patrones de organización de código, es importante entender algunos conceptos clave.
  • El código debe estar divido en unidades funcionales — módulos, servicios, etc. Y se debe evitar la tentación de tener todo en un único bloque $(document).ready(). Este concepto se conoce como encapsulación.
  • No repetir código. Identificar piezas similares y utilizar técnicas de herencia.
  • A pesar de la naturaleza de jQuery, no todas las aplicaciones JavaScript trabajan (o tienen la necesidad de poseer una representación) en el DOM.
  • Las unidades de funcionalidad no deben estar fuertemente acopladas (en inglés loosely coupled) — es decir, una unidad de funcionalidad debe ser capaz de existir por si misma y la comunicación con otras unidades debe ser a través de un sistema de mensajes como los eventos personalizados o pub/sub. Por otro lado, siempre que sea posible, de debe mantener alejada la comunicación directa entre unidades funcionales.
El concepto de articulación flexible puede ser especialmente problemático para desarrolladores que hacen su primera incursión en aplicaciones complejas. Por lo tanto, si usted esta empezando a crear aplicaciones, solamente sea consciente de este concepto.

10.2. Encapsulación

El primer paso para la organización del código es separar la aplicación en distintas piezas. Muchas veces, este esfuerzo suele ser suficiente para mantener al código en orden.

10.2.1. El objeto literal

Un objeto literal es tal vez la manera más simple de encapsular código relacionado. Este no ofrece ninguna privacidad para propiedades o métodos, pero es útil para eliminar funciones anónimas, centralizar opciones de configuración, y facilitar el camino para la reutilización y refactorización.
Un objeto literal
var myFeature = {
    myProperty : 'hello',
 
    myMethod : function() {
        console.log(myFeature.myProperty);
    },
 
    init : function(settings) {
        myFeature.settings = settings;
    },
 
    readSettings : function() {
        console.log(myFeature.settings);
    }
};
 
myFeature.myProperty; // 'hello'
myFeature.myMethod(); // registra 'hello'
myFeature.init({ foo : 'bar' });
myFeature.readSettings(); // registra { foo : 'bar' }
El objeto posee una propiedad y varios métodos, los cuales son públicos (es decir, cualquier parte de la aplicación puede verlos). ¿Cómo se puede aplicar este patrón con jQuery? Por ejemplo, en el siguiente código escrito en el estilo tradicional:
// haciendo click en un item de la lista se carga cierto contenido,
// luego utilizando el ID de dicho item se ocultan
// los items aledaños
$(document).ready(function() {
  $('#myFeature li')
    .append('<div/>')
    .click(function() {
      var $this = $(this);
      var $div = $this.find('div');
      $div.load('foo.php?item=' +
        $this.attr('id'),
        function() {
          $div.show();
          $this.siblings()
            .find('div').hide();
        }
      );
    });
});
Si el ejemplo mostrado representa el 100% de la aplicación, es conveniente dejarlo como esta, ya que no amerita hacer una reestructuración. En cambio, si la pieza es parte de una aplicación más grande, estaría bien separar dicha funcionalidad de otras no relacionadas. Por ejemplo, es conveniente mover la URL a la cual se hace la petición fuera del código y pasarla al área de configuración. También romper la cadena de métodos para hacer luego más fácil la modificación.
Utilizar un objeto literal para una funcionalidad jQuery
var myFeature = {
    init : function(settings) {
        myFeature.config = {
            $items : $('#myFeature li'),
            $container : $('<div class="container"></div>'),
            urlBase : '/foo.php?item='
        };
 
        // permite sobreescribir la configuración predeterminada
        $.extend(myFeature.config, settings);
        myFeature.setup();
    },
 
    setup : function() {
        myFeature.config.$items
            .each(myFeature.createContainer)
            .click(myFeature.showItem);
    },
 
    createContainer : function() {
        var $i = $(this),
            $c = myFeature.config.$container.clone()
                     .appendTo($i);
        $i.data('container', $c);
    },
 
    buildUrl : function() {
        return myFeature.config.urlBase +
               myFeature.$currentItem.attr('id');
    },
 
    showItem : function() {
        var myFeature.$currentItem = $(this);
        myFeature.getContent(myFeature.showContent);
    },
 
    getContent : function(callback) {
        var url = myFeature.buildUrl();
        myFeature.$currentItem
            .data('container').load(url, callback);
    },
 
    showContent : function() {
        myFeature.$currentItem
            .data('container').show();
        myFeature.hideContent();
    },
 
    hideContent : function() {
        myFeature.$currentItem.siblings()
            .each(function() {
                $(this).data('container').hide();
            });
    }
};
 
$(document).ready(myFeature.init);
La primera característica a notar es que el código es más largo que el original — como se dijo anteriormente, si este fuera el alcance de la aplicación, utilizar un objeto literal seria probablemente una exageración.
Con la nueva organización, las ventajas obtenidas son:
  • Separación de cada funcionalidad en pequeños métodos. En un futuro, si se quiere cambiar la forma en que el contenido se muestra, será claro en donde habrá que hacerlo. En el código original, este paso es mucho más difícil de localizar.
  • Se eliminaron los usos de funciones anónimas.
  • Las opciones de configuración se movieron a una ubicación central.
  • Se eliminaron las limitaciones que poseen las cadenas de métodos, haciendo que el código sea más fácil para refactorizar, mezclar y reorganizar.
Por sus características, la utilización de objetos literales permiten una clara mejora para tramos largos de código insertados en un bloque $(document).ready(). Sin embargo, no son más avanzados que tener varias declaraciones de funciones dentro de un bloque $(document).ready().

10.2.2. El patrón modular

El patrón modular supera algunas limitaciones del objeto literal, ofreciendo privacidad para variables y funciones, exponiendo a su vez (si se lo desea) una API pública.
El patrón modular
var feature =(function() {
    // variables y funciones privadas
    var privateThing = 'secret',
        publicThing = 'not secret',
        changePrivateThing = function() {
            privateThing = 'super secret';
        },
 
        sayPrivateThing = function() {
            console.log(privateThing);
            changePrivateThing();
        };
 
    // API publica
    return {
        publicThing : publicThing,
        sayPrivateThing : sayPrivateThing
    }
})();
 
feature.publicThing; // registra 'not secret'
 
feature.sayPrivateThing();
// registra 'secret' y cambia el valor de privateThing
En el ejemplo, se autoejecuta una función anónima la cual devuelve un objeto. Dentro de la función, se definen algunas variables. Debido a que ellas son definidas dentro de la función, desde afuera no se tiene acceso a menos que se pongan dentro del objeto que se devuelve. Esto implica que ningún código fuera de la función tiene acceso a la variable privateThing o a la función sayPrivateThing. Sin embargo, sayPrivateThing posee acceso a privateThing y changePrivateThingdebido a estar definidos en el mismo alcance.
El patrón es poderoso debido a que permite tener variables y funciones privadas, exponiendo una API limitada consistente en devolver propiedades y métodos de un objeto.
A continuación se muestra una revisión del ejemplo visto anteriormente, con las mismas características, pero exponiendo un único método público del modulo, showItemByIndex().
Utilizar el patrón modular para una funcionalidad jQuery
$(document).ready(function() {
    var feature = (function() {
        var $items = $('#myFeature li'),
            $container = $('<div class="container"></div>'),
            $currentItem,
 
            urlBase = '/foo.php?item=',
 
            createContainer = function() {
                var $i = $(this),
                    $c = $container.clone().appendTo($i);
 
                $i.data('container', $c);
            },
 
            buildUrl = function() {
                return urlBase + $currentItem.attr('id');
            },
 
            showItem = function() {
                var $currentItem = $(this);
                getContent(showContent);
            },
 
            showItemByIndex = function(idx) {
                $.proxy(showItem, $items.get(idx));
            },
 
            getContent = function(callback) {
                $currentItem.data('container').load(buildUrl(), callback);
            },
 
            showContent = function() {
                $currentItem.data('container').show();
                hideContent();
            },
 
            hideContent = function() {
                $currentItem.siblings()
                    .each(function() {
                        $(this).data('container').hide();
                    });
            };
 
        $items
            .each(createContainer)
            .click(showItem);
 
        return { showItemByIndex : showItemByIndex };
    })();
 
    feature.showItemByIndex(0);
});

10.3. Gestión de Dependencias

NOTAEsta sección esta basada en la excelente documentación de RequireJS y es utilizada con el permiso de James Burke, autor de RequireJS.
Cuando un proyecto alcanza cierto tamaño, comienza a ser difícil el manejo de los módulos de una aplicación, ya que es necesario saber ordenarlos de forma correcta, y comenzar a combinarlos en un único archivo para lograr la menor cantidad de peticiones. También es posible que se quiera cargar código al vuelo luego de la carga de la página.
RequireJS es una herramienta de gestión de dependencias creada por James Burke, la cual ayuda a manejar los módulos, cargarlos en un orden correcto y combinarlos de forma fácil sin tener que realizar ningún cambio. A su vez, otorga una manera fácil de cargar código una vez cargada la página, permitiendo minimizar el tiempo de descarga.
RequireJS posee un sistema modular, que sin embargo, no es necesario seguirlo para obtener sus beneficios. El formato modular de RequireJS permite la escritura de código encapsulado, incorporación de internacionalización (i18n) a los paquetes (para permitir utilizarlos en diferentes lenguajes) e incluso la utilización de servicios JSONP como dependencias.

10.3.1. Obtener RequireJS

La manera más fácil de utilizar RequireJS con jQuery es descargando el paquete de jQuery con RequireJS ya incorporado en él. Este paquete excluye porciones de código que duplican funciones de jQuery.

10.3.2. Utilizar RequireJS con jQuery

Utilizar RequireJS es simple, tan solo es necesario incorporar en la página la versión de jQuery que posee RequireJS incorporado y a continuación solicitar los archivos de la aplicación. El siguiente ejemplo asume que tanto jQuery como los otros archivos están dentro de la carpeta scripts/.
Utilizar RequireJS: Un ejemplo simple
<!DOCTYPE html>
<html>
    <head>
        <title>jQuery+RequireJS Sample Page</title>
        <script src="scripts/require-jquery.js"></script>
        <script>require(["app"]);</script>
    </head>
    <body>
        <h1>jQuery+RequireJS Sample Page</h1>
    </body>
</html>
La llamada a require([.app"]) le dice a RequireJS que cargue el archivo scripts/app.js. RequireJS cargará cualquier dependencia pasada a require() sin la extensión .js desde el mismo directorio que en que se encuentra el archivo require-jquery.js, aunque también es posible especificar la ruta de la siguiente forma:
<script>require(["scripts/app.js"]);</script>
El archivo app.js es otra llamada a require.js para cargar todos los archivos necesarios para la aplicación. En el siguiente ejemplo, app.js solicita dos extensiones jquery.alpha.js y jquery.beta.js (no son extensiones reales, solo ejemplos). Estas extensiones están en la misma carpeta que require-jquery.js:
Un simple archivo JavaScript con dependencias
require(["jquery.alpha", "jquery.beta"], function() {
    //las extensiones jquery.alpha.js y jquery.beta.js han sido cargadas.
    $(function() {
        $('body').alpha().beta();
    });
});

10.3.3. Crear módulos reusables con RequireJS

RequireJS hace que sea fácil definir módulos reusables a través de require.def(). Un modulo RequireJS puede tener dependencias que pueden ser utilizadas para definir un módulo, además de poder devolver un valor — un objeto, una función, u otra cosa — que puede ser incluso utilizado otros módulos.
Si el módulo no posee ninguna dependencia, tan solo se debe especificar el nombre como primer argumento de require.def(). El segundo argumento es un objeto literal que define las propiedades del módulo. Por ejemplo:
Definición de un módulo RequireJS que no posee dependencias
require.def("my/simpleshirt",
    {
        color: "black",
        size: "unisize"
    }
);
El ejemplo debe ser guardado en el archivo my/simpleshirt.js.
Si el modulo posee dependencias, es posible especificarlas en el segundo argumento de require.def() a través de un array) y luego pasar una función como tercer argumento. Esta función será llamada para definir el módulo una vez cargadas todos las dependencias. Dicha función recibe los valores devueltos por las dependencias como un argumento (en el mismo orden en que son requeridas en el array) y luego la misma debe devolver un objeto que defina el módulo.
Definición de un módulo RequireJS con dependencias
require.def("my/shirt",
    ["my/cart", "my/inventory"],
    function(cart, inventory) {
        //devuelve un objeto que define a "my/shirt"
        return {
            color: "blue",
            size: "large"
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);
En este ejemplo, el modulo my/shirt es creado. Este depende de my/cart y my/inventory. En el disco, los archivos están estructurados de la siguiente forma:
my/cart.js
my/inventory.js
my/shirt.js
La función que define my/shirt no es llamada hasta que my/cart y my/inventoryhayan sido cargadas, y dicha función recibe como argumentos a los módulos como cart y inventory. El orden de los argumentos de la función debe coincidir con el orden en que las dependencias se requieren en el array. El objeto devuelto define el módulo my/shirt. Definiendo los módulos de esta forma, my/shirt no existe como un objeto global, ya que múltiples módulos pueden existir en la página al mismo tiempo.
Los módulos no tienen que devolver un objeto; cualquier tipo de valor es permitido.
Definición de un módulo RequireJS que devuelve una función
require.def("my/title",
    ["my/dependency1", "my/dependency2"],
    function(dep1, dep2) {
        // devuelve una función para definir "my/title".
        // Este devuelve o establece
        // el titulo de la ventana
        return function(title) {
            return title ? (window.title = title) : window.title;
        }
    }
);
Solo un módulo debe ser requerido por archivo JavaScript.

10.3.4. Optimizar el código con las herramientas de RequireJS

Una vez incorporado RequireJS para el manejo de dependencias, la optimización del código es muy fácil. Descargue el paquete de RequireJS y colóquelo en cualquier lugar, preferentemente fuera del área de desarrollo web. Para los propósitos de este ejemplo, el paquete de RequireJS esta ubicado en una carpeta paralela al directorio webapp (la cual contiene la página HTML y todos los archivos JavaScript de la aplicación). La estructura de directorios es:
requirejs/ (utilizado para ejecutar las herramientas)
webapp/app.html
webapp/scripts/app.js
webapp/scripts/require-jquery.js
webapp/scripts/jquery.alpha.js
webapp/scripts/jquery.beta.js
Luego, en la carpeta en donde se encuentran require-jquery.js y app.js, crear un archivo llamado app.build.js con el siguiente contenido:
Archivo de configuración para las herramientas de optimización de RequireJS
{
    appDir: "../",
    baseUrl: "scripts/",
    dir: "../../webapp-build",
    //Comentar la siguiente línea si se desea
    //minificar el código por el compilador
    //en su modo "simple"
    optimize: "none",
 
    modules: [
        {
            name: "app"
        }
    ]
}
Para utilizar la herramienta, es necesario tener instalado Java 6. Closure Compiler es utilizado para la minificación del código (en caso que optimize: none esté comentado).
Para comenzar a procesar los archivos, abrir una ventana de comandos, dirigirse al directorio webapp/scripts y ejecutar:
# para sistemas que no son Windows
$ ../../requirejs/build/build.sh app.build.js
 
# para sistemas Windows
c:\> ..\..\requirejs\build\build.bat app.build.js
Una vez ejecutado, el archivo app.js de la carpeta webapp-build/ contendrá todo el código de app.js más el de jquery.alpha.js y jquery.beta.js. Si se abre el archivo app.html (también en la carpeta webapp-build/) podrá notar que ninguna petición se realiza para cargar jquery.alpha.js y jquery.beta.js.

10.4. Ejercicios

10.4.1. Crear un módulo Portlet

Abra el archivo /ejercicios/portlets.html en el navegador. Realice el ejercicio utilizando el archivo /ejercicios/js/portlets.js. El ejercicio consiste en crear una función creadora de portlet que utilice el patrón modular, de tal manera que el siguiente código funcione:
var myPortlet = Portlet({
    title : 'Curry',
    source : 'data/html/curry.html',
    initialState : 'open' // or 'closed'
});
 
myPortlet.$element.appendTo('body');
Cada portlet deberá ser un div con un título, un área de contenido, un botón para abrir/cerrar el portlet, un botón para eliminarlo y otro para actualizarlo. El portlet devuelto por la función deberá tener la siguiente API pública:
myPortlet.open();    // fuerza a abrir
myPortlet.close();   // fuerza a cerrar
myPortlet.toggle();  // alterna entre los estados abierto y cerrado
myPortlet.refresh(); // actualiza el contenido
myPortlet.destroy(); // elimina el portlet de la página
myPortlet.setSource('data/html/onions.html'); // cambia el código

Capítulo 11. Eventos personalizados

11.1. Introducción a los eventos personalizados

Todos estamos familiarizados con los eventos básicos — clickmouseoverfocusblursubmit, etc. — que surgen a partir de la interacción del usuario con el navegador.
Los eventos personalizados permiten conocer el mundo de la programación orientada a eventos (en inglés event-driven programming). En este capítulo, se utilizará el sistema de eventos personalizados de jQuery para crear una simple aplicación de búsqueda en Twitter.
En un primer momento puede ser difícil entender el requisito de utilizar eventos personalizados, ya que los eventos convencionales permiten satisfacer todas las necesidades. Sin embargo, los eventos personalizados ofrecen una nueva forma de pensar la programación en JavaScript. En lugar de enfocarse en el elemento que ejecuta una acción, los eventos personalizados ponen la atención en el elemento en donde la acción va a ocurrir. Este concepto brinda varios beneficios:
  • Los comportamientos del elemento objetivo pueden ser ejecutados por diferentes elementos utilizando el mismo código.
  • Los comportamientos pueden ser ejecutados en múltiples, similares elementos objetivos a la vez.
  • Los comportamientos son asociados de forma más clara con el elemento objetivo, haciendo que el código sea más fácil de leer y mantener.
Un ejemplo es la mejor forma de explicar el asunto. Suponga que posee una lámpara incandescente en una habitación de una casa. La lámpara actualmente esta encendida. La misma es controlada por dos interruptores de tres posiciones y un clapper (interruptor activado por aplausos):
<div class="room" id="kitchen">
    <div class="lightbulb on"></div>
    <div class="switch"></div>
    <div class="switch"></div>
    <div class="clapper"></div>
</div>
Ejecutando el clapper o alguno de los interruptores, el estado de la lámpara cambia. A los interruptores o al clapper no le interesan si la lámpara está encendida o apagada, tan solo quieren cambiar su estado
Sin la utilización de eventos personalizados, es posible escribir la rutina de la siguiente manera:
$('.switch, .clapper').click(function() {
    var $light = $(this).parent().find('.lightbulb');
    if ($light.hasClass('on')) {
        $light.removeClass('on').addClass('off');
    } else {
        $light.removeClass('off').addClass('on');
    }
});
Por otro lado, utilizando eventos personalizados, el código queda así:
$('.lightbulb').bind('changeState', function(e) {
    var $light = $(this);
    if ($light.hasClass('on')) {
        $light.removeClass('on').addClass('off');
    } else {
        $light.removeClass('off').addClass('on');
    }
});
 
$('.switch, .clapper').click(function() {
    $(this).parent().find('.lightbulb').trigger('changeState');
});
Algo importante ha sucedido: el comportamiento de la lámpara se ha movido, antes estaba en los interruptores y en el clapper, ahora se encuentra en la misma lámpara.
También es posible hacer el ejemplo un poco más interesante. Suponga que se ha añadido otra habitación a la casa, junto con un interruptor general, como se muestra a continuación:
<div class="room" id="kitchen">
    <div class="lightbulb on"></div>
    <div class="switch"></div>
    <div class="switch"></div>
    <div class="clapper"></div>
</div>
<div class="room" id="bedroom">
    <div class="lightbulb on"></div>
    <div class="switch"></div>
    <div class="switch"></div>
    <div class="clapper"></div>
</div>
<div id="master_switch"></div>
Si existe alguna lámpara encendida en la casa, es posible apagarlas a través del interruptor general, de igual forma si existen luces apagadas, es posible prenderlas con dicho interruptor. Para realizar esta tarea, se agregan dos eventos personalizados más a la lámpara: turnOn y turnOff. A través de una lógica en el evento changeState se decide qué evento personalizado utilizar:
$('.lightbulb')
    .bind('changeState', function(e) {
        var $light = $(this);
        if ($light.hasClass('on')) {
            $light.trigger('turnOff');
        } else {
            $light.trigger('turnOn');
        }
    })
    .bind('turnOn', function(e) {
        $(this).removeClass('off').addClass('on');
    })
    .bind('turnOff', function(e) {
        $(this).removeClass('off').addClass('on');
    });
 
$('.switch, .clapper').click(function() {
    $(this).parent().find('.lightbulb').trigger('changeState');
});
 
$('#master_switch').click(function() {
    if ($('.lightbulb.on').length) {
        $('.lightbulb').trigger('turnOff');
    } else {
        $('.lightbulb').trigger('turnOn');
    }
});
Note como el comportamiento del interruptor general se ha vinculado al interruptor general mientras que el comportamiento de las lámparas pertenece a las lámparas.
NOTASi esta acostumbrado a la programación orientada a objetos, puede resultar útil pensar de los eventos personalizados como métodos de objetos. En términos generales, el objeto al que pertenece el método se crea a partir del selector jQuery. Vincular el evento personalizado changeState a todos los elementos $('.light') es similar a tener una clase llamada Light con un método changeState, y luego instanciar nuevos objetos Light por cada elemento.
Recapitulación: $.fn.bind y $.fn.trigger
En el mundo de los eventos personalizados, existen dos métodos importantes de jQuery: $.fn.bind y $.fn.trigger. En el capítulo dedicado a eventos se explicó la utilización de estos dos métodos para trabajar con eventos del usuario; en este capítulo es importante recordar 2 puntos:
  • El método $.fn.bind toma como argumentos un tipo de evento y una función controladora de evento. Opcionalmente, puede recibir información asociada al evento como segundo argumento, desplazando como tercer argumento a la función controladora de evento. Cualquier información pasada estará disponible a la función controladora a través de la propiedad data del objeto del evento. A su vez, la función controladora recibe el objeto del evento como primer argumento.
  • El método $.fn.trigger toma como argumentos el tipo de evento y opcionalmente, puede tomar un array con valores. Estos valores serán pasados a la función controladora de eventos como argumentos luego del objeto del evento.
A continuación se muestra un ejemplo de utilización de $.fn.bind y $.fn.trigger en donde se utiliza información personalizada en ambos casos:
$(document).bind('myCustomEvent', { foo : 'bar' }, function(e, arg1, arg2) {
    console.log(e.data.foo); // 'bar'
    console.log(arg1); // 'bim'
    console.log(arg2); // 'baz'
});
 
$(document).trigger('myCustomEvent', [ 'bim', 'baz' ]);

11.2. Un ejemplo de aplicación

Para demostrar el poder de los eventos personalizados, se desarrollará una simple herramienta para buscar en Twitter. Dicha herramienta ofrecerá varias maneras para que el usuario realice una búsqueda: ingresando el término a buscar en una caja de texto o consultando los "temas de moda" de Twitter.
Los resultados de cada término se mostrarán en un contenedor de resultados; dichos resultados podrán expandirse, colapsarse, refrescarse y eliminarse, ya sea de forma individual o conjunta.
Aplicación que permite buscar en Twitter
Figura 11.1 Aplicación que permite buscar en Twitter
Se empieza con un HTML básico:
<h1>Twitter Search</h1>
<input type="button" id="get_trends"
    value="Load Trending Terms" />
 
<form>
    <input type="text" class="input_text"
        id="search_term" />
    <input type="submit" class="input_submit"
        value="Add Search Term" />
</form>
 
<div id="twitter">
    <div class="template results">
        <h2>Search Results for
        <span class="search_term"></span></h2>
    </div>
</div>
El HTML posee un contenedor (#twitter) para el widget, una plantilla para los resultados (oculto con CSS) y un simple formulario en donde el usuario puede escribir el término a buscar.
Existen dos tipos de elementos en los cuales actuar: los contenedores de resultados y el contenedor Twitter.
Los contenedores de resultados son el corazón de la aplicación. Se creará una extensión para preparar cada contenedor una vez que éste se agrega al contenedor Twitter. Además, entre otras cosas, la extensión vinculará los eventos personalizados por cada contenedor y añadirá en la parte superior derecha de cada contenedor botones que ejecutarán acciones. Cada contenedor de resultados tendrá los siguientes eventos personalizados:
  • refresh Señala que la información del contenedor se esta actualizando y dispara la petición que busca los datos para el término de búsqueda.
  • populate Recibe la información JSON y la utiliza para rellenar el contenedor.
  • remove Elimina el contenedor de la página luego de que el usuario confirme la acción. Dicha confirmación puede omitirse si se pasa true como segundo argumento del controlador de evento. El evento además elimina el término asociado con el contenedor de resultados del objeto global que contiene los términos de búsqueda.
  • collapse Añade una clase al contenedor, la cual ocultará el resultado a través de CSS. Además cambiará el botón de Colapsar a Expandir.
  • expand Remueve la clase del contenedor que añade el evento collapse. Además cambiará el botón de Expandir a Colapsar.
Además, la extensión es responsable de añadir los botones de acciones al contenedor, vinculando un evento click a cada botón y utilizando la clase de cada ítem para determinar qué evento personalizado será ejecutado en cada contenedor de resultados.
$.fn.twitterResult = function(settings) {
    return this.each(function() {
        var $results = $(this),
            $actions = $.fn.twitterResult.actions =
                $.fn.twitterResult.actions ||
                $.fn.twitterResult.createActions(),
            $a = $actions.clone().prependTo($results),
            term = settings.term;
 
        $results.find('span.search_term').text(term);
 
        $.each(
            ['refresh', 'populate', 'remove', 'collapse', 'expand'],
            function(i, ev) {
                $results.bind(
                    ev,
                    { term : term },
                    $.fn.twitterResult.events[ev]
                );
            }
        );
 
        // utiliza la clase de cada acción para determinar
        // que evento se ejecutará en el panel de resultados
        $a.find('li').click(function() {
            // pasa el elemento <li> clickeado en la función
            // para que se pueda manipular en caso de ser necesario
            $results.trigger($(this).attr('class'), [ $(this) ]);
        });
    });
};
 
$.fn.twitterResult.createActions = function() {
    return $('<ul class="actions" />').append(
        '<li class="refresh">Refresh</li>' +
        '<li class="remove">Remove</li>' +
        '<li class="collapse">Collapse</li>'
    );
};
 
$.fn.twitterResult.events = {
    refresh : function(e) {
           // indica que los resultados se estan actualizando
        var $this = $(this).addClass('refreshing');
 
        $this.find('p.tweet').remove();
        $results.append('<p class="loading">Loading ...</p>');
 
        // obtiene la información de Twitter en formato jsonp
        $.getJSON(
            'http://search.twitter.com/search.json?q=' +
                escape(e.data.term) + '&rpp=5&callback=?',
            function(json) {
                $this.trigger('populate', [ json ]);
            }
        );
    },
 
    populate : function(e, json) {
        var results = json.results;
        var $this = $(this);
 
        $this.find('p.loading').remove();
 
        $.each(results, function(i,result) {
            var tweet = '<p class="tweet">' +
                '<a href="http://twitter.com/' +
                result.from_user +
                '">' +
                result.from_user +
                '</a>: ' +
                result.text +
                ' <span class="date">' +
                result.created_at +
                '</span>' +
            '</p>';
 
            $this.append(tweet);
        });
 
        // indica que los resultados
        // ya se han actualizado
        $this.removeClass('refreshing');
    },
 
 
    remove : function(e, force) {
        if (
            !force &&
            !confirm('Remove panel for term ' + e.data.term + '?')
        ) {
            return;
        }
        $(this).remove();
 
        // indica que ya no se tendrá
        // un panel para el término
        search_terms[e.data.term] = 0;
    },
 
    collapse : function(e) {
        $(this).find('li.collapse').removeClass('collapse')
            .addClass('expand').text('Expand');
 
        $(this).addClass('collapsed');
    },
 
    expand : function(e) {
        $(this).find('li.expand').removeClass('expand')
            .addClass('collapse').text('Collapse');
 
        $(this).removeClass('collapsed');
    }
};
El contenedor Twitter, posee solo dos eventos personalizados:
getResults Recibe un término de búsqueda y comprueba si ya no existe un contenedor de resultados para dicho término. En caso de no existir, añade un contenedor utilizando la plantilla de resultados, lo configura utilizando la extensión $.fn.twitterResult (mostrada anteriormente) y luego ejecuta el evento refresh con el fin de cargar correctamente los resultados. Finalmente, guarda el término buscado para no tener volver a pedir los datos sobre la búsqueda.
getTrends Consulta a Twitter el listado de los 10 primeros "términos de moda", interactúa con ellos y ejecuta el evento getResults por cada uno, de tal modo que añade un contenedor de resultados por cada término.
Vinculaciones en el contenedor Twitter:
$('#twitter')
    .bind('getResults', function(e, term) {
        // se comprueba que ya no exista una caja para el término
        if (!search_terms[term]) {
            var $this = $(this);
            var $template = $this.find('div.template');
 
            // realiza una copia de la plantilla
            // y la inserta como la primera caja de resultados
            $results = $template.clone().
                removeClass('template').
                insertBefore($this.find('div:first')).
                twitterResult({
                    'term' : term
                });
 
            // carga el contenido utilizando el evento personalizado "refresh"
            // vinculado al contenedor de resultados
            $results.trigger('refresh');
            search_terms[term] = 1;
        }
    })
    .bind('getTrends', function(e) {
        var $this = $(this);
        $.getJSON('http://search.twitter.com/trends.json?callback=?', function(json) {
            var trends = json.trends;
            $.each(trends, function(i, trend) {
                $this.trigger('getResults', [ trend.name ]);
            });
        });
    });
Hasta ahora, se ha escrito una gran cantidad de código que no realiza nada, lo cual no esta mal. Se han especificado todos los comportamientos que se desean para los elementos núcleos y se ha creado un sólido marco para la creación rápida de la interfaz.
A continuación, se conecta la caja de búsqueda y el botón para cargar los Temas de moda. En la caja de texto, se captura el término ingresado y se pasa al mismo tiempo que se ejecuta el evento getResults. Por otro lado, haciendo click en el botón para cargar los Temas de moda, se ejecuta el evento getTrends:
$('form').submit(function(e) {
    e.preventDefault();
    var term = $('#search_term').val();
    $('#twitter').trigger('getResults', [ term ]);
});
 
$('#get_trends').click(function() {
    $('#twitter').trigger('getTrends');
});
Añadiendo botones con un ID apropiado, es posible remover, colapsar, expandir y refrescar todos los contenedores de resultados al mismo tiempo. Para el botón que elimina el contenedor, notar que se esta pasando true al controlador del evento como segundo argumento, indicando que no se desea una confirmación del usuario para eliminar el contenedor.
$.each(['refresh', 'expand', 'collapse'], function(i, ev) {
    $('#' + ev).click(function(e) { $('#twitter div.results').trigger(ev); });
});
 
$('#remove').click(function(e) {
    if (confirm('Remove all results?')) {
        $('#twitter div.results').trigger('remove', [ true ]);
    }
});

11.3. Conclusión

Los eventos personalizados ofrecen una nueva manera de pensar el código: ellos ponen el énfasis en el objetivo de un comportamiento, no en el elemento que lo activa. Si se toma el tiempo desde el principio para explicar las piezas de su aplicación, así como los comportamientos que esas piezas necesitan exhibir, los eventos personalizados proveen una manera poderosa para hablar con esas piezas, ya sea de una en una o en masa.
Una vez que los comportamientos se han descripto, se convierte en algo trivial ejecutarlos desde cualquier lugar, lo que permite la rápida creación y experimentación de opciones de interfaz. Finalmente, los eventos personalizados también permiten mejorar la lectura del código y su mantenimiento, haciendo clara la relación entre un elemento y su comportamiento.
Puede ver la aplicación completa en los archivos demos/custom-events/custom-events.html y demos/custom-events/js/custom-events.js del material que componen este libro.

No hay comentarios:

Publicar un comentario