Acerca de Scriptia
Saltar a Anotaciones recientes
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.
Saltar a Anotaciones recientes
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.
Ver una vaca encima de un cobertizo no es nada comparado con las cosas que uno se encuentra en los scripts que corren por estos mundos de dios.
Uno de los campos en los que cualquier mindundi es capaz de aplicar al máximo su desconocimiento de javascript es el del cálculo de días en un mes. Utilizar una docena de condicionales es de flojos.
La máxima: cuando tengas que echar cuentas, sobre cualquier cosa, utiliza las herramientas del lenguaje. No hagas cuentas de la vieja.
El código. Función que recibe un mes (en notación numérica humana) y un año (opcional, si no se recibe tomamos el año actual) y retorna el número de días en el mes. Ningún condicional fue dañado durante el experimento.
function daysInMonth(humanMonth, year) {
return new Date(year || new Date().getFullYear(), humanMonth, 0).getDate();
}
daysInMonth(2, 2009); // 28
daysInMonth(2, 2008); // 29
Las claves:
d) corresponderá al día 0 del mes siguiente al que buscamos. El algoritmo de creación de fechas es tolerante (básicamente suma lo que le demos y obtiene un entero, así que el día 0 de un mes es siempre el último día del mes anterior).En fin, que todo esto va de que la máquina es más lista que tú y tus nudillos. Y que todos los blogueros juntos, incluido el menda.
Podemos usar una estratagema similar para saber si el año es bisiesto:
function isLeapYear(year) {
return new Date(year, 0, 366).getFullYear() == year;
}
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.
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 })
});
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.
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.
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.
Hablábamos el otro día de QUnit, hoy hablaremos de JsUnitTest –otro sistema de testing– y de jShoulda, una capa de abstracción que un servidor ha moldeado con sus propias manos y a imagen y semejanza de Shoulda.
Principiemos hablando de JsUnitTest, un port sin dependencias del sistema de testing desarrollado para probar prototype y script.aculo.us.
Algo como esto bastará:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Minimal JsUnitTest</title>
<script src="/js/jsunittest.js" type="text/javascript"></script>
<link rel="stylesheet" href="/css/unittest.css" type="text/css" media="screen"/>
</head>
<body>
<h1>Minimal JsUnitTest</h1>
<div id="testlog">
Los resultados de las pruebas se muestran, por defecto
en el elemento con id=testlog
</div>
<script type="text/javascript">
// aquí irá el código de nuestras pruebas,
// también podemos hacer referencia a un script externo
</script>
</body>
</html>
Para definir y ejecutar las pruebas, creamos una instancia de Test.Unit.Runner:
new Test.Unit.Runner({});
En el objeto de configuración que pasamos como argumento, definimos como propiedades las pruebas a realizar:
new Test.Unit.Runner({
testNombreDelTest : function() {
this.assert(true);
},
testOtroTest : function() {
this.assert(true);
}
});
Los nombres de las propiedades correspondientes a pruebas deben comenzar por “test”. Si deseamos ejecutar funciones de preparación y limpieza antes y después de cada una de las pruebas, utilizamos las propiedades setup y teardown para definirlas:
new Test.Unit.Runner({
setup : function() {
this.foo = 1;
},
testNombreDelTest : function() {
this.assert(true);
},
testFoo : function() {
this.assertEqual(1, this.foo);
}
});
Observa: tanto las funciones de preparación como las de prueba se ejecutan en el contexto de una instancia de Test.Unit.Testcase. Las posibles aserciones son métodos de dicha instancia (por eso usamos this). También podemos utilizar this para mantener variables que vayamos a utilizar en nuestras pruebas.
La mayor parte de los métodos asertivos aceptan tres parámetros: el valor esperado, el valor real y (opcionalmente) un mensaje que el sistema nos mostrará en caso de error.
He aquí una lista deliberadamente incompleta de los métodos de aserción:
Comprueban lo que su nombre parece indicar. Un detalle importante: en JavaScript, dos arrays con el mismo contenido (o dos objetos-usados-como-hashes) no pueden compararse mediante igualdad. Por tanto, si quieres comparar dos arrays necesitarás utilizar assertEnumEqual. Y para comparar dos objetos, assertHashEqual.
drnic, el humano detrás de JsUnitTest, es también padre de newjs, un gem (si no sabes qué es un gem, ignora esta sección hasta que lo averigües) pensada para facilitar el desarrollo de bibliotecas JavaScript. Sus virtudes:
Sin entrar en detalles. O lo ves, o no lo ves:
$ newjs milib
create
create config
create lib
create src
create script
create tasks
create test/assets
create test/unit
create test/functional
create test/assets/unittest.css
create test/assets/jsunittest.js
# etcetera
dependency install_rubigen_scripts
exists script
create script/generate
create script/destroy
$ cd milib
$ script/generate unit_test basic
exists test/unit
create test/unit/basic_test.html
$ rake test_units
(in /Users/choan/Current/tirame/milib)
Started tests in Safari
.
Finished in 2 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Started tests in Firefox
.
Finished in 1 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Skipping Internet Explorer, not supported on this OS
Skipping Konqueror, not supported on this OS
Started tests in Opera
.
Finished in 3 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Sí, amigo mío, mediante la ejecución de una tarea rake hemos ejecutado las pruebas en cada uno de los navegadores disponibles en nuestro sistema y hemos recogido los resultados. Algo que Chuck Norris no es capaz de hacer.
Pero vayamos con jShoulda, que yo he venido aquí a hablar de mi libro.
jShoulda es una capa de abstracción para JsUnitTest que permite escribir nuestros tests al más puro estilo Shoulda. Algo así:
context('A context', {},
should('run a test', function() {
this.assert('Yay!');
}),
)();
¿Y qué ventaja tiene esto? Para un test vulgar, ninguna, francamente. Pero si tus pruebas requieren de inicializaciones complejas, la cosa se pone chula.
context('A context', {
setup: function() {
this.foo = 1;
}
},
should('run its setup function', function() {
this.assertEqual(1, this.foo);
}),
context('which is a "nested" context', {
setup: function() {
this.foo +=1;
}
},
should('run both setup functions', function() {
this.assertEqual(2, this.foo);
})
)
)();
Para utilizar jShoulda necesitas… jshoulda.js. Y un documento HTML como el anterior, al que añadirás la referencia a la biblioteca.
Pero para probarlo no necesitas descargar nada. Pásate por la página del proyecto. Los resultados de pruebas que encontrarás en ella (suponiendo que la visites con JavaScript activado) corresponden al test de ejemplo ejecutado en vivo. Si quieres juguetear, doblecliquea en el código, modifícalo a tu gusto y pulsa el botón. Sugerencia: haz fallar las pruebas. Mola.
Si estás interesado en jShoulda y tu inglés no es ni mejor ni peor que el mío, lee el tutorial de jShoulda, suscríbete a la lista de correo y, si das con algún bug, arréglalo.
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.
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.
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.
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:
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.
¿Diste con un código que te gustaría estudiar y está compactado en una línea? Beautify Javascript reformatea cualquier cosa para convertirla en algo legible.
Visto en Sentido web.
En lo que hace a eventos, los navegadores suelen ir a su bola. Y en lo que hace a eventos de teclado, el caos es absoluto: hay quien notifica las teclas modificadores en keypress, hay quien no; cada navegador es un mundo en cuanto a códigos de tecla… y dos o tres pesadillas más.
En JavaScript Madness: Keyboard Events, Jan Wolter documenta para uso y disfrute público todas las divergencias que se va encontrando.
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.