Scriptia. Javascript y buenas prácticas en español



Scriptia / Etiquetas / jQuery

Saltar a Acerca de Scriptia

Estás viendo la página para la etiqueta (o conjunto de etiquetas) jQuery.

Trabajando con jQuery: timeline arrastrable, del espaguetismo al plugin

En el capítulo de hoy veremos cómo reconvertir un código espagueti en un bonito plugin reutilizable.

Objetivo del widget: permitir desplazar horizontalmente mediante arrastre (pincho, arrastro, suelto) un bloque de contenido. Si tienes prisa por ver en qué acaba la cosa, ve directamente a la demo.

Elementos: un contenedor (con overflow: auto) y su contenido. Para nuestro ejemplo:

<div id="#timeline">
  <ul>
    <li>...</li>
    <li>...</li>
  </ul>
</div>

Con unos estilos tal que:

#timeline {
 width: 600px;
 height: 400px;
 overflow: auto;
}

#timeline ul {
 width: 1000px;
 margin: 0;
 padding: 0;
}

#timeline li {
 display: block;
 width: 200px;
 padding: 10px;
 margin: 0;
 float: left;
}

Sobre estas piedras edificaremos nuestra iglesia. Veamos paso a paso el código necesario para hacer que la cosa se mueva. Pondremos en marcha la maquinaria cuando pinchemos sobre el contenedor:

$('#timeline')
  .bind('mousedown', function(e) {
    // aquí vendrá nuestro código
  });

A fin de evitar el uso de variables globales, utilizarmos data, un método de jQuery que nos permite mantener relaciones entre valores y elementos DOM. Vamos a mantener los siguientes datos: posición actual del scroll, elemento al que afecta y posición del puntero en el momento del mousedown.

$(document)
  .data('timeline', {
    element: this,
    scroll: this.scrollLeft,
    x : e.clientX
  });

Observa que estamos conservando los datos en document. En cada momento existirá un máximo de un elemento en desplazamiento.

Bien, ha empezado la acción… pero todavía no estamos siguiendo los movimientos del puntero. Asignemos un par de manejadores para los eventos mousemove y mouseup de document.

¿Por qué para document y no para el contenedor con el que estamos tratando? Aunque el puntero salga del contenedor durante el arrastre, queremos seguir desplazando el contenido. Y si saliera y no controláramos mouseup a nivel de documento, nuestro script nunca abandonaría el modo “estamos arrastrando cositas”.

Como somos chulos y modernos, aprovecharemos los eventos.con-espacio-de-nombres que jQuery gentilmente pone a nuestra disposición. Para mousemove, recuperamos los datos que hemos almacenado en mousedown y modificamos la propiedad scrollLeft del contenedor sumándole la diferencia entre la posición actual del ratón y la almacenada. En mouseup, limpiamos los datos almacenados y eliminamos los manejadores de eventos del espacio timeline asignados a document.

jQuery(document)
  .bind('mousemove.timeline', function(e) {
    var data = jQuery(this).data('timeline');
    data.element.scrollLeft = data.scroll + data.x - e.clientX;
  })
  .bind('mouseup.timeline', function(e) {
    jQuery(this)
      .removeData('timeline')
      .unbind('.timeline');
  });

Para rematar, cancelaremos la acción por defecto de mousedown para no seleccionar texto mientras arrastramos:

e.preventDefault();

Poniéndolo todo junto y dentro de una llamada a ready, tenemos la cosita funcionando:

jQuery(document).ready(function() {

  jQuery('#timeline')
    .bind('mousedown', function(e) {

      jQuery(document)
        .data('timeline', {
          element: this,
          scroll: this.scrollLeft,
          x : e.clientX
        })
        .bind('mousemove.timeline', function(e) {
          var data = jQuery(this).data('timeline');
          data.element.scrollLeft = data.scroll + data.x - e.clientX;
        })
        .bind('mouseup.timeline', function(e) {
          jQuery(this)
            .removeData('timeline')
            .unbind('.timeline');
        });

      e.preventDefault();

    });
});

¡Genial! Con esto ya tenemos el comportamiento deseado, pero… no parece muy reutilizable. Pluguinifiquemos.

Construyendo un plugin

Cuando invocamos la función jQuery (o su alias habitual, $), recuperamos un objeto. Dicho objeto tiene un prototipo. Para añadir métodos al objeto devuelto por jQuery, los añadimos a su prototipo. Así que algo tal que:

jQuery.prototype.miMetodo = function() {

};

Ya nos serviría para ir tirando. Pero hay un idioma más jotacueriesco para decir lo mismo. De paso añadamos un return this a nuestro método para permitir el encadenamiento de llamadas:

jQuery.extend(jQuery.fn, {
  miMetodo : function() {
    return this;
  }
});

Con esto ya podemos darnos el gustazo de escribir:

jQuery('#miCosa')
  .miMetodo()
  .etcetera();

Pero escribir jQuery (que es lo que debemos hacer si queremos asegurarnos de que nuestro plugin es compatible con otras bibliotecas) una y otra vez es escribir mucho. Vamos a meter todo el código de nuestro plugin dentro de una función anónima que nos permita tener un alias seguro para jQuery.

(function($) {

})(jQuery);

Y vamos a rellenarlo poquito a poco.

(function($) {
  $.extend($.fn, {
    timeline : function() {
      this
        .bind('mousedown', handleMouseDown);
      return this;
    }
  });

  function handleMouseDown(e) {
    // bla bla
  }

})(jQuery);

Fíjate: extraemos el código de manejo del evento mousedown. ¿Por qué? No necesitamos ningún dato local y por tanto no necesitamos del cierre funcional para acceder a dicho dato (ni generar la función al vuelo para cada widget). Haremos lo mismo para los manejadores de mousemove y mouseup:

(function($) {
  $.extend($.fn, {
    timeline : function() {
      this
        .bind('mousedown', handleMouseDown);
      return this;
    }
  });

  function handleMouseDown(e) {
    $(document)
      .data('timeline', {
        element: this,
        scroll: this.scrollLeft,
        x : e.clientX
      })
      .bind('mousemove.timeline', handleMouseMove)
      .bind('mouseup.timeline', handleMouseUp);
    e.preventDefault();
  }

  function handleMouseMove(e) {
    var data = $(this).data('timeline');
    data.element.scrollLeft = data.scroll + data.x - e.clientX;
  }

  function handleMouseUp(e) {
    $(this)
      .removeData('timeline')
      .unbind('.timeline');
  }

})(jQuery);

Con esto nuestro código espagueti ya parece algo más ordenadito. Pero un plugin sin opciones sabe a poco. Vamos a considerar la posibilidad de configurar la proporción de desplazamiento/arrastre y de decidir si ocultamos o no la barra de scroll nativa. Por partes:

$.extend($.fn, {
  timeline : function(opts) {
    var config = $.extend({
      speed: 1.5,
      accesible: true
    }, opts || {});

    this
      .bind('mousedown', config, handleMouseDown);

    if (!config.accesible) {
      this.css('overflow', 'hidden');
    }

    return this;
  }
});

Ahora el método timeline acepta un argumento que corresponde a un objeto de configuración. Utilizamos jQuery.extend para aplicar los valores sobre una configuración por defecto. Y al asignar el manejador para mousedown lo pasamos como argumento. Si esto resulta nuevo para ti, lo que te falta saber es que con una asignación de este tipo, el manejador recibirá estos datos extra en la propiedad data del objeto que representa el evento. Modifiquemos el manejador para transmitir la configuración:

function handleMouseDown(e) {
  $(document)
    .data('timeline', {
      element: this,
      scroll: this.scrollLeft,
      x : e.clientX,
      config : e.data
    })
    .bind('mousemove.timeline', handleMouseMove)
    .bind('mouseup.timeline', handleMouseUp);
  e.preventDefault();
}

Utilicemos la configuración en handleMouseMove:

function handleMouseMove(e) {
  var data = $(this).data('timeline');
  data.element.scrollLeft = data.scroll + (data.x - e.clientX) * data.config.speed;
}

Y listo. Ya tenemos un plugin bastante decente. Para ponerlo en marcha:

jQuery(document)
  .ready(function() {
    $('el.selector-de-dios')
      .timeline({ speed: 2, accesible: false })
  });

Bola extra 1: configuración por defecto

Si nuestro usuario siempre va a preferir la opción no accesible del plugin, no por ser idiota (u obedecer órdenes) le vamos a obligar a pasar la configuración una y otra vez. Apañemos:

$.extend($.fn, {
  timeline : function(opts) {
    var config = $.extend({}, $.fn.timeline.defaults, opts || {});
    // ...
    return this;
  }
});

$.fn.timeline.defaults = {
  speed: 1.5,
  accesible: true
};

De esta manera permitimos al usuario configurar las opciones de manera global manipulando jQuery.fn.timeline.defaults a su antojo.

Bola extra 2: mousewheel

El plugin mousewheel permite controlar los movimientos de la ruedita del ratón. Aprovechémoslo:

$.extend($.fn, {
    timeline : function(opts) {
      // ...
      if ($.fn.mousewheel) {
        this
          .mousewheel(function(e, delta) {
            this.scrollLeft -= delta * config.mousewheelSpeed;
            e.preventDefault();
          });
      }
      return this;
    }
  });

Puedes probar la combinación de todos estos elementos en la demo correspondiente.

Coda

Choan no inventa nada nuevo, lo tratado aquí viene a ser una combinación de mis conocimientos, gustos y experiencia y lo expuesto en estos dos artículos:

Ten presente que la arquitectura propuesta para este plugin no siempre es la más adecuada —ya hablaremos de ello— y que el plugin de ejemplo no está publicado ni oficial ni extra oficialmente y nadie te dará soporte.

Pruebas unitarias con QUnit

Las pruebas unitarias (unit testing) son necesarias y convenientes, ya programes en Ruby, en PHP, en JavaScript o en Cuenca. En esta notita veremos cómo utilizar QUnit –la biblioteca creada para el testeo del núcleo de jQuery– para testear nuestros propios proyectos.

Continúa leyendo Pruebas unitarias con QUnit

Eventos en jQuery 1.2

Días ha que escribí una nota sobre los eventos en jQuery. Y hora es de ampliar dicho artículo con las novedades que la serie 1.2 de jQuery añade al respecto.

Continúa leyendo Eventos en jQuery 1.2

Ajax, eventos y jQuery

Un «problema» con el que todo novato de la programación con jQuery se encuentra tarde o temprano (y las listas de correo lo demuestran) es que el contenido cargado (o generado) dinámicamente no dispara los manejadores de eventos asignados en $(document).ready.

Continúa leyendo Ajax, eventos y jQuery

Taller de jQuery en The Cocktail el 29 de mayo

Los muchachos de The Cocktail me han invitado a impartir un taller de jQuery en The Cocktail Academy.

Será el jueves 29 de mayo de 2008, a partir de las las 19.30h en los locales de Salamanca, 17 (Madrid). Si piensas asistir (es gratis), inscríbete cuanto antes en el wiki de los talleres.

Una pista de por donde irán los tiros. Hablaremos de:

  • selección de elementos, filtrado, etc.;
  • eventos a fondo, incluyendo delegación de eventos y sus ventajas;
  • navegación por el DOM;
  • algún que otro truquito de lo más chachipiruli.

Y por supuesto, de algunas razones para usar (o no usar) jQuery.

Después del taller, estáis todos invitados a pagarme unas cañas.

Un patrón de desarrollo de plugins para jQuery

Mike Alsup, autor de jQuery form plugin y otras delicias, nos explica cómo crear un plugin para jQuery que cumpla con las condiciones de: no contaminar el espacio de nombres, acepte opciones (y las extienda), mantenga los límites adecuados entre lo público y lo privado y saque provecho del plugin de metadatos. Ahí es nada: A Plugin Development Pattern.

jQuery UI y jQuery 1.2.1

Llega jQuery UI, una colección de componentes reusables y de alta calidad: jQuery UI: Interactions and Widgets.

Y también la versión 1.2.1 de jQuery, con algunas correcciones de bugs sobre la 1.2.

jQuery 1.2

El equipo de desarrollo de jQuery ha publicado la versión 1.2 de la biblioteca. Incorpora algunas novedades que justifican sobradamente el cambio de minor version.

Selectores

Se incorporan :has(), :header y :animated. Desaparecen los selectores XPath (si los necesitas, puedes usar el plugin de compatibilidad con XPath) y, aprovechando la ocasión, la sintaxis para los selectores por atributo usa sintaxis CSS. Así, a[@class=jfgi] se convierte en a[class=jfgi].

Atributos

El método val() ha sido mejorado y ahora permite recuperar el valor de elementos SELECT y marcar y desmarcar checkboxes.

Navegación por el DOM

Nuevos métodos. map() permite la transmutación alquímica de la colección. prevAll() y nextAll() recuperan, respectivamente, los hermanos (siblings) mayores y menores (o anteriores y siguientes, como se prefiera). slice() corta la colección a gusto del consumidor. hasClass('una-clase‘) nos dice si el elemento tiene o no asignada una-clase. andSelf() combina dos colecciones apiladas. contents() recupera los nodos hijos, incluidos los nodos de tipo texto.

Manipulación

Llegan wrapAll() y wrapInner(). clone() trae una gran novedad: usando clone(true) los elementos clonados mantienen los manejadores de eventos del original.

Posición

Aterriza offset(), que nos devuelve las coordenadas de un elemento tomando como origen la esquina superior izquierda del viewport. height() y width() también sirven ahora para obtener el tamaño de la ventana y el documento.

AJAX

Ahora load() permite cargar de modo muy sencillo pedazos de HTML. Usa un selector a continuación de la URL para indicar el filtro: $('#links').load('/Main_Page #p-Getting-Started li'). Con getScript() podemos cargar scripts desde otros dominios, lo que autoriza a getJSON() a utilizar servicios web basados en JSONP. El método serialize() ha sido reescrito para permitir la serialización sencilla de formularios. Se ha incorporado a $.ajax() la opción cache que fuerza el refresco de los datos solicitados.

Efectos

Ya podemos utilizar valores en em o porcentajes en las animaciones. El plugin (oficial) Color Animations permite realizar animaciones de colorines. stop() detiene las animaciones. Llegan stop(), queue(), dequeue(), las animaciones relativas, las personalizadas y otras maravillas.

Eventos

El nuevo método triggerHandler() dispara los manejadores de eventos asignados a un elemento sin activar el comportamiento por defecto del elemento. Llegan los eventos con espacio de nombres.

Todos los detalles en jQuery 1.2: jQuery.extend(”Awesome”).

Chuleta de jQuery

Adrien Gibrat ha tenido a bien crear y compartir una chuletita de jQuery (PDF).

Novedades en jQuery 1.1.4

La versión 1.1.4 de jQuery, publicada a finales de agosto, incluye, como es costumbre, algunas mejoras en el rendimiento, pero también (y esto no es tan habitual) algunas novedades interesantes que merece la pena conocer.

Continúa leyendo Novedades en jQuery 1.1.4

Acerca de Scriptia

Saltar a la caja de búsqueda

Scriptia forma parte del PDM de Choan C. Gálvez, desarrollador web residente en Barcelona. Scriptia pretende mejorar la calidad de la documentación acerca de javascript disponible en español.