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



Scriptia / artículos

Saltar a Acerca de Scriptia

Keypress, apilado de eventos, control de valores y setTimeout

Algo que me han preguntado hoy:

Choan, cariño mío, guapetón de mi alma… tengo dos campos en un formulario y quiero que uno refleje los cambios en el otro… y controlando keypress no tengo el valor actualizado

Pues no, no lo tienes. Solución: controlar la pila de ejecución. El truco: usar un timeout para que el método que actualiza los valores se ejecute después de acabar con la gestión de eventos. Algo así:

// en el manejador del evento keypress
setTimeout(updateValue, 0); // <-- la clave está en el cero

Ahora bien, lo de recoger el valor actualizado basándonos en keypress no es suficiente, porque hay mil maneras de rellenar el campo sin pulsar una tecla (copipegar por menús, lectores de códigos de barras, yo-que-sés…)

Así que mi consejo es defenderse controlando también el evento blur. Lo pongo en términos de jQuery, suponiendo que el campo que controlamos lleva el identificador source y el de destino es conocido como destination.

(function($) {

  var init_copy_values = function() {
    $('#source').bind('keypress blur', function(e) {
      if (e.type == 'keypress') setTimeout(updateValue, 0);
      else updateValue();
    });
  },
  updateValue = function() {
    $('#destination').val($('#source').val());
  };

  $(init_copy_values);

}(jQuery));

Pues nada, ya está, un post sin insultar a nadie.

Vilaweb y el desarrollador tonto del haba

Me cuentan por ahí que hoy se ha lanzado la nueva versión de Vilaweb. El culmen de las buenas prácticas en marcado HTML (sí, es sarcasmo).

Atención al siguiente fragmento, extraído del tal sitio el 24 de noviembre de 2009:

<div class="zona-imatge">
  <img alt="Detingut el batlle de Polop per la mort del seu predecessor&lt;br/&gt;" src="/media/portada/81/1259046000.jpg" width="299" height="254"/>
  <div class="params" style="display: none">
    <div class="w">299</div>
    <div class="h">254</div>
    <div class="file">/media/portada/81/1259046000.jpg</div>
  </div>
</div>

Mi comentario (versión breve): imbécil.

Mi comentario (versión extendida): eres tan tonto que cada vez que te documentas, en lugar de mejorar empeoras. ¿Qué demonios hacen esos «parámetros» metiendo ruido en el HTML? ¿Se te ha ocurrido, por ventura, aplicar un tipo de «no intrusividad» parida por tu genial cerebro?

Supongo que estarás pensando: ya te vale, Choan, despotricas y no dices nada. Así es, en efecto. Si sabes lo que está mal (fatal, punible con muerte) en ese pedazo de código no hace falta que te cuente nada. Y si no lo sabes, sigue en tu santa ignorancia, busca una multinacional en la que triunfar como un programador mediocre y haz que tu madre se sienta orgullosa de ti. Cretino.

(Hay otras perlas, pero la que más ganas me ha dado de ponerme faltón ha sido esta. Y sí, el diseño también me parece una mierda.)

Número de días en un mes

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:

  • ECMAScript maneja internamente las fechas como datos numéricos (véase Tiempo Unix)
  • en ECMAScript, los meses se numeran del 0 al 11 y los días de 1 a N. Nosotros alimentamos la función con un mes humano. Así que la fecha que se crea (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.

Adendum

Podemos usar una estratagema similar para saber si el año es bisiesto:

function isLeapYear(year) {
  return new Date(year, 0, 366).getFullYear() == year;
}

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

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”).

YUI Compressor comprime tus scripts y tus hojas de estilos

Ya hemos hablado por aquí de cómo comprimir ficheros javascript. Pero aún no está todo dicho. Hoy y aquí, YUI Compressor.

Continúa leyendo YUI Compressor comprime tus scripts y tus hojas de estilos

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.