About

lunes, 18 de marzo de 2013

¿Cómo hacer un plugin en jQuery?

Autor: Pablo Betancor Lugo (pbetancor@avantic.net)
Revisor: Yeray Darias  Camacho

¿Cómo hacer un plugin en jQuery?

Introducción

jQuery es un framework de javascript, que entre otras, tiene las siguientes características:
  • Simplificar la interactuación con elementos html.
  • Simplificar la  manipulación del árbol DOM.
  • Simplificar la interacción con Ajax.
  • Simplificar el manejo de eventos.
  • Simplificar el desarrollo de animaciones.
La filosofía de jQuery se basa en "Encuentra algo. Manipúlalo", y para ello utiliza la función $().

La función $()

La forma de interactuar con la página es mediante la función $(), un alias de jQuery(), que recibe como parámetro una expresión CSS o el nombre de una etiqueta HTML y devuelve todos los nodos (elementos) que concuerden con la expresión. Por ejemplo:
  • $('#bitacora') : devuelve el elemento con el id bitácora
  • $('.oculto'): devuelve el listado de elementos de la clase oculto
  • $('p.rojo'): devuelve todos los 'p' de la clase rojo.
Una vez obtenidos los nodos, se les puede aplicar cualquiera de las funciones del framework

$('#bitacora').load('ajax/bitacora.html');

$('.oculto').hide('slow');

$("p.rojo").mouseover(function () {
   $(this).css("color","red");
});

Web de jQuery http://www.jquery.com

Plugins en jQuery

jQuery cuenta con un gran número de plugins que permiten extender su funcionalidad. Además es muy sencillo extender la librería jQuery con nuevos plugins y métodos, ahorrándonos de mucho tiempo de desarrollo.
La web de plugins de jQuery es: http://plugins.jquery.com.

Entorno

Este tutorial se ha realizado en jQuery 1.6.1 y ha sido probado en los navegadores Firefox 10, Internet Explorer 9, Google Chrome 17.0.963.46 m y Opera 11.50.

Realización del plugin

Descripción del plugin a realizar

El plugin que a continuación vamos a realizar lo llamamos autoExpandInput. Es un plugin para auto incrementar el tamaño de los input texts a medida que se va escribiendo. La forma de invocar el plugin es:
$(selector).autoExpandInput(opciones).
Las opciones que se pueden pasar son:
  • maxWidth: Tamaño máximo que podrá alcanzar el input text.
  • minWidth: Tamaño mínimo que podrá alcanzar el input text.
  • comfortZone: Espacio entre el final del texto y el del input.

Primer paso para la creación del plugin

Para la creacion de plugins con jQuery, hay dos objetos básicos que se pueden extender:
  • El objeto jQuery, que se encarga de prácticamente todo el procesamiento interno. 
  • El objeto jQuery.fn, que es el que maneja la interacción con elementos.
Si quisiéramos crear una función general (del tipo $.ajax(...)), usaríamos jQuery, pero como lo que queremos es realizar un plugin que interactue sobre los elementos devueltos por un selector jQuery, usaremos jQuery.fn..
Para empezar añadiremos una nueva función a jQuery.fn., donde el nombre de ésta será el nombre del plugin.
jQuery.fn.autoExpandInput = function() {

  // Código del plugin

};
Como querremos usar el dolar y asegurarnos de que no colisionará con otras librerías, es una buena práctica encapsular la función de la siguiente forma. Así mapeamos la variable jQuery a la variable $ dentro del scope de nuestra función.
(function( $ ){
  $.fn.autoExpandInput = function() {
  
    // Código del plugin
  };
})( jQuery );

Preparar inicialización del plugin

Al plugin se le podría pasar por parámetros todas las opciones mencionadas anteriormente.
$('miSelector').autoExpandInput(maxWidth, minWidth...);
Pero para tener plugins más complejos y personalizables, es una buena práctica tener unos valores por defecto que se podrán extender con los que pase el usuario al invocar el plugin. Y en vez de llamar al plugin con un gran número de argumentos, llamarlo sólo con un argumento, que será un mapa con los valores que el usuario quiera sobrescribir dentro de los valores por defecto.
Una forma elegante de implementar esto es mediante la función jQuery.extend(). Que mezcla el contenido de dos o más objetos dentro del primer objecto.
(function($){

  // añadimos el plugin autoExpandInput al espacio de nombres jQuery
  $.fn.autoExpandInput = function(options) {

    // mezclamos los parámetros que haya pasado el usuario con los parámetros por defecto
    options = $.extend({
      maxWidth: 820,
      minWidth: 200,
      comfortZone: 20
    }, options);
  
    return this;

  };

})(jQuery); 
Como vemos la salida del plugin es this. En this tenemos el objecto jQuery con el que que el plugin fue invocado. Devolviendo éste, permitimos encadenar llamadas a otras funciones/plugins jquery sobre los mismos elementos. Por ejemplo:
$('form input').autoExpandInput(options).css('color', 'red');

Filtrar los inputs sobre los que actuar

Hay que recordar que el plugin que estamos realizando auto incrementa los inputs texts a medida que vamos escribiendo. Por lo que el próximo paso del plugin será filtrar los nodos de texto a partir de los nodos sobre el que fue invocado el scrpit.
(function($){

  // añadimos el plugin autoExpandInput al espacio de nombres jQuery
  $.fn.autoExpandInput = function(options) {

    ...   

    this.filter('input:text').each(function(){
      var inputText = $(this);
   
    });

    return this;

  };

})(jQuery); 

Ahora podemos asociar que cuando se produzcan los eventos keypress y blur (es decir al apretar una tecla en el input text o al abandonar éste) sobre cualquiera de los input texts, se llame a la función que autoincrementa éste, sólo en el caso que sea necesario.
(function($){

  // añadimos el plugin autoExpandInput al espacio de nombres jQuery
  $.fn.autoExpandInput = function(options) {

    ...

    this.filter('input:text').each(function(){
      var inputText = $(this);
   
      inputText.bind('keypress blur', function(){extendInputIfNecessary(inputText, options);});

    });

    return this;

  };

})(jQuery); 

Nota: Tener en cuenta que la función extendInputIfNecessary la asociamos a la función bind de jQuery envuelta en una función anónima. El por qué de esto no entra en este tutorial. Para desarrolladores poco experimentados con javascript se recomienda leer http://docs.jquery.com/Tutorials:How_jQuery_Works#Callback_and_Functions para entenderlo.

Función de autoincremento de un input text.

Para saber cuanto tenemos que incrementar el input sobre el que estamos escribiendo vamos a seguir la siguiente aproximación:
  • Hacemos un elemento html de prueba (no existente dentro de los tags html) con las mismas características de fuente que el input (fontSize, fontFamily, fontWeight y letterSpacing).
  • Hacemos que el width de este elemento de prueba sea auto.
  • Insertamos el elemento de prueba tras el input que queremos expandir (al no existir dentro de los tags de html, éste no se mostrará en pantalla).
  • Insertamos el texto del input dentro del elemento de prueba. Al tener el width a auto, el elemento de prueba tendrá el tamaño del texto que insertamos.
    El código de la función que nos devuelve este elemento de prueba es la siguiente:
    /*
     * Devuelve un elemento html con el width auto a partir de las propiedades
     * de las fuentes del input pasado.
     */
    function getHtmlFixedTesterElement(inputText) {
     return $('<tester/>').css({
      position: 'absolute',
      top: -9999,
      left: -9999,
      width: 'auto',
      fontSize: inputText.css('fontSize'),
      fontFamily: inputText.css('fontFamily'),
      fontWeight: inputText.css('fontWeight'),
      letterSpacing: inputText.css('letterSpacing'),
      whiteSpace: 'nowrap'
     });
    } 
    
La función extendInputIfNecessary obtendrá el tamaño del input de la siguiente manera:
function extendInputIfNecessary(inputText,  options) {
 
 var testElement = getHtmlFixedTesterElement(inputText);
 
 testElement.insertAfter(inputText);
 
 var minWidth = options.minWidth;

 // escapeamos el contenido conflictivo a pasar a html y añadimos el html al testElement
 var escaped = inputText.val().replace(/&/g, '&amp;').replace(/\s/g,'&nbsp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 testElement.html(escaped);

 var testerWidth = testElement.width();
 
}
Nota: El texto del input sobre el que obtenemos el elemento de prueba, lo escapeamos ante posible contenido conflictivo.
Ahora que ya tenemos el ancho que ocupa el texto en el input (testerWidth), simplemente nos falta:
  • Sumarle el confortZone de las opciones del plugin.
  • Mirar que no sobrepase el valor mínimo ni el máximo de las opciones del plugin.
  • Establecer en el input text el nuevo tamaño.
  • Eliminar el elemento de prueba del DOM. Si no lo hacemos estaremos llenando el documento html de elementos de prueba.
El código quedaría:
function extendInputIfNecessary(inputText,  options) {
 
 var testElement = getHtmlFixedTesterElement(inputText);
 
 testElement.insertAfter(inputText);
 
 var minWidth = options.minWidth;

 // escapeamos el contenido conflictivo a pasar a html y añadimos el html al testElement
 var escaped = inputText.val().replace(/&/g, '&amp;').replace(/\s/g,'&nbsp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 testElement.html(escaped);

 var testerWidth = testElement.width();
 
 var proposedSize = testerWidth + options.comfortZone;
 
 var newWidth = getFixedProposedSize(proposedSize, options);
 
 inputText.width(newWidth);
 
 // como ya no necesitamos testElement lo borramos del DOM
 testElement.detach();
}

function getFixedProposedSize(proposedSize, options) {
 if (proposedSize < options.minWidth)
       return options.minWidth;
 else if (proposedSize > options.maxWidth)
       return options.maxWidth;
 else
       return proposedSize;
}

Código completo del plugin

(function($){

  // añadimos el plugin autoExpandInput al espacio de nombres jQuery
  $.fn.autoExpandInput = function(options) {

    // mezclamos los parámetros que haya pasado el usuario con los parámetros por defecto
    options = $.extend({
      maxWidth: 820,
      minWidth: 200,
      comfortZone: 20
    }, options);
  
    this.filter('input:text').each(function(){
      var inputText = $(this);
   
      inputText.bind('keypress blur', function(){extendInputIfNecessary(inputText, options);});

    });

    return this;

  };

})(jQuery); 

function extendInputIfNecessary(inputText,  options) {
 
 var testElement = getHtmlFixedTesterElement(inputText);
 
 testElement.insertAfter(inputText);
 
 var minWidth = options.minWidth;

 // escapeamos el contenido conflictivo a pasar a html y añadimos el html al testElement
 var escaped = inputText.val().replace(/&/g, '&amp;').replace(/\s/g,'&nbsp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 testElement.html(escaped);

 var testerWidth = testElement.width();
 
 var proposedSize = testerWidth + options.comfortZone;
 
 var newWidth = getFixedProposedSize(proposedSize, options);
 
 inputText.width(newWidth);
 
 // como ya no necesitamos testElement lo borramos del DOM
 testElement.detach();
}

/*
 * Devuelve un elemento html con el width auto a partir de las propiedades
 * de las fuentes del input pasado.
 */
function getHtmlFixedTesterElement(inputText) {
 return $('<tester/>').css({
  position: 'absolute',
  top: -9999,
  left: -9999,
  width: 'auto',
  fontSize: inputText.css('fontSize'),
  fontFamily: inputText.css('fontFamily'),
  fontWeight: inputText.css('fontWeight'),
  letterSpacing: inputText.css('letterSpacing'),
  whiteSpace: 'nowrap'
 });
} 

function getFixedProposedSize(proposedSize, options) {
 if (proposedSize < options.minWidth)
       return options.minWidth;
 else if (proposedSize > options.maxWidth)
       return options.maxWidth;
 else
       return proposedSize;
}

Ejercicios y mejoras en el plugin.

Sobre el plugin realizado, se proponen las siguiente mejoras/ampliaciones:
  • Ahora mismo el plugin hace crecer o encoger los inputs siempre que se pulsa una tecla. Se propone añadir la opción growthChars, que indique cada cuántos caracteres se quiere hacer crecer los inputs. Es decir, los inputs crecerán en bloques de growthChars y no siempre que se escriba.
  • Añadir como segundo parámetro del plugin un callback (opcional) que se ejecute cada vez que se haga crecer un input text.

Referencias

Obtener productos y tecnologías

Acerca del autor

Pablo Betancor Lugo, empleado de Avantic Estudio de Ingenieros desde 2008. Desarrollador de aplicaciones Web en general en diferentes tecnologías como JSF, Spring MVC y Grails entre otras. Entre las diferentes aplicaciones desarrolladas se encuentran desde las aplicaciones empresariales más comunes, hasta sistemas basados en Web Semántica, Arquitecturas SOA o aplicaciones de escritorio. Programador sobre todo en lenguaje Java aunque a partir de 2010 empieza a abrirse a otros lenguajes basados en Java Virtual Machine como Groovy y Scala.
Gran admirador del mundo de Spring en general (asistente a la SpringIO 2010 en Madrid) y participante en el grupo Agile Canarias, donde tiene la oportunidad compartir y aprender inquietudes, dudas y metodologías de diferentes profesionales del mundo del desarrollo de software de Canarias.
En julio de 2011 participa como ponente en un taller de jQuery en la Tenerife Lan Party 2k11 junto a Yeray Darias Camacho, compañero de Avantic Estudio de Ingenieros.
Pablo Betancor Lugo
pbetancor@avantic.net
@diyipol


0 comentarios:

Publicar un comentario

Share

Twitter Delicious Facebook Digg Stumbleupon Favorites More