En el anterior artículo vimos algunas problemáticas de combinar controles JavaScript con Ajax y JSF 2. Hoy os presento una versión que soluciona los problemas que describí allí (que por supuesto no es ni la mejor ni la única, simplemente a la mejor que he llegado yo).

El cronómetro Javascript

var Cronometro = Class.create({
  initialize: function(id, opciones) {
    this.el = $(id);
    this.id = this.el.identify();
    this.opciones = Object.extend({
                                    hiddenField : null
                                  }, opciones || {});
    this.t0 = new Date().getTime();
    if (Cronometro.cronos[this.id])
      Cronometro.cronos[this.id].parar();
    Cronometro.cronos[this.id] = this;
    this.pintaCrono();
    this.temporizador = new PeriodicalExecuter(this.pintaCrono.bind(this), 1);
  },
  parar: function() {
    this.temporizador.stop();
  },
  pintaCrono: function() {
    if (this.opciones.hiddenField) Form.Element.setValue(this.opciones.hiddenField,new Date().getTime() - this.t0);
    this.el.update(this.getCronoString());
  },
  getCronoString: function() {
    var t = (new Date().getTime() - this.t0) / 1000;
    var m = Math.floor(t / 60);
    var s = Math.floor(t % 60);
    var mm = m < 10 ? "0" + m.toString() : m.toString();
    var ss = s < 10 ? "0" + s.toString() : s.toString();
    return mm + ":" + ss;
  }
});

El problema de parar el anterior cronómetro si creo otro encima intenté solucionarlo primero metiendo una referencia al cronómetro dentro del nodo DOM al que está asociado (this.el). De esa manera pretendía, al crear un cronómetro, mirar primero si ya existía otro en ese mismo elemento y pararlo primero. Pero no funcionaba ya que no pensé que el elemento del DOM también se sustituye así que nunca encontraba la referencia.

La siguiente idea era capturar el evento (si existía, que no lo tenía claro) de eliminación de un nodo DOM. Existe, pero no se aconseja su uso y, sobre todo, sólo es compatible con algunos de los últimos navegadores. Parece que sólo me quedaba una solución aunque no es muy elegante.

Necesitaba algún otro lugar donde guardar el cronómetro y al final he optado por la vía fácil de la variable global. Es el Cronometro.cronos que se puede ver al final. Con eso, como se puede ver en el constructor del objeto, puedo controlar y evitar que se creen cronómetros sin destruir el anterior.

La integración en la página

<p>Tiempo: <span id="tiempo">--:--</span>
<h:inputHidden id="iTiempo" value="#{juego.tiempo}"/></p>
<script>var crono = new Cronometro('tiempo', {hiddenField:'form:iTiempo'})</script>

<h:panelGroup rendered="#{juego.respuestaCorrecta}">
 <script>Cronometro.cronos['tiempo'].parar()</script>

Lo siguiente que necesitaba era poder parar el cronómetro ante determinadas acciones. Concretamente, como ya comenté, al responder correctamente. Para ello he optado por enviar el fragmento javascript que se ve al final del anterior listado.