Fundamentos de jQuery

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 ]);
    }
});