sábado, 27 de febrero de 2010

Event Delegation en Javascript

Hace unos días haciendo una página que básicamente es un listado con 3 radio buttons por fila (si, porras, aunque en tu charla del madrid-rb del jueves dijeras que no sabes para que se usan, a veces son útiles), quería que se enviara el formulario al seleccionar uno de los radio-buttons sin tener que pulsar en submit.

Para hacerlo un poco limpio, lo hice con javascript no intrusivo y prototype de la siguiente forma, donde cada uno de los input tiene definida la clase radio_submit

document.observe("dom:loaded", function() {
    $$('input.radio_submit').each(function(item) {
        item.observe('click', function() {
            item.parentNode.submit();
        });
    });
});

Pero no me gustaba esa solución donde tengo 3 function anidados, así que le pedí consejo a Paco a ver si se le ocurría una mejor solución.

Y evidentemente había una solución mejor, basada en Event Delegation

document.observe("dom:loaded", function() {
  $('user_source_list').observe('click', function(event){
    var element = event.target;
    if (element.match('input.radio_submit')){
      element.up('form').submit();
    }
  });
});


La idea es que en lugar de crear un evento en cada uno de los elementos, se crea sólo un evento en un objeto que los contiene a todos (en este caso es un div con el id 'user_source_list'), y capturamos el click sobre ese objeto.

Otra mejora que me propuso es que en lugar de usar parentNode para ir al elemento padre usara up('form'), de esa forma, si cambia el diseño de la página y se introduce algún elemento intermedio en el dom de la página, el código seguirá funcionando.

Paco tambien me pasó este enlace donde se ve como en Rails 3 se utilizará la misma técnica de javascript no intrusivo con delegación de eventos.

8 comentarios:

  1. Usa jQuery! Mucho más sencillo:

    $(document).ready(function() {
    $('input.radio_submit').click(function() {
    $(this).parents('form').submit();
    });
    });


    sam

    ResponderEliminar
  2. A mí hay una cosa que no me gusta en esa solución. Estamos usando javascript no molesto/obstructor -obstrusivo significa obstructor, no intrusivo ;) - y para evitarlo ¿pretendes añadirle más marcado?

    No me parece limpio. Creo que debemos usar tan poco marcado como sea posible. En este caso, bastaría con darle un ID al form y usar el form como el objeto al que delegarle los eventos.

    En cualquier caso, no termino de verle la ventaja a la delegación de eventos en este caso concreto. Me parece que es complicar innecesariamente el asunto asignándole responsabilidad al padre de algo que depende en exclusiva del hijo y teniendo que preguntar por el target.

    Usando prototype puedes conseguir de forma limpia lo mismo usando directamente el elemento destino. Además si en vez de usar parentNode usas up como recomendaba @pacoguzman, tienes la ventaja de que te pueden cambiar el html tanto como quieran y tus eventos seguirán funcionando.

    document.observe("dom:loaded", function() {
    $$('input.radio_submit').invoke('observe','click', function() {
    this.up('form').submit();
    });
    });

    Igual me he perdido algo de por qué es mejor delegar, pero en este caso no lo veo nada claro.

    Si me convences compro tu solución (refactorizada con el ID del form), pero si no, me gusta más la mía :)

    ResponderEliminar
  3. Sam, en este caso la solución de prototype se parece bastante a la de jQuery. De acuerdo que la de jQuery es ligeramente menos verbosa, pero no justificaría cambiar a jQuery si tu proyecto está todo montado con prototype.

    De todos modos, estoy contigo en que jQuery es más mono ;)

    ResponderEliminar
  4. No conocía el invoke de prototype

    Utilizando invoke queda más sencillo lo que quería hacer Diego, con este método no hubieramos llegado al uso de delegación de eventos.

    ResponderEliminar
  5. Sam, como dice Javi, el tema de hacerlo en prototype ha sido simplemente porque el proyecto está en prototype. Está claro que la solución más sencilla es con jquery.

    Javi, lo que comentas de añadir más marcado, no se ha añadido más marcado, simplemente se ha utilizado el contenedor de los formularios para delegar los eventos.

    Usando delegación de eventos, creo tiene más sentido utilizar el contenedor, que crear un evento por formulario. Otra cosa es que realmente tenga sentido delegar en este caso.... no tengo claro si es más eficiente crear 12 eventos (en mi caso eran 4 formularios con 3 radio button por formulario) o un único evento más complejo.

    Como dice Paco, si se me hubiera ocurrido la solución con invoke, probablemente no habríamos usado delegación.

    Por último, como curiosidad, en rails 3 con prototype, todos los eventos se delegan desde el document.body

    Gracias por los comentarios :-)

    ResponderEliminar
  6. Diego, te entendí mal.. pensaba que habías añadido específicamente un div solamente para los radio buttons, por eso decía lo de añadir marcado de más y sugería usar mejor el ID del Form directamente.

    Esto te pasa por no pegar el html ;)

    ResponderEliminar
  7. men... mira quiero abrir mi blog pero al momento de poner mi codigo queda un desparramo horrible, como lo as echo tu mi correo es cod.matriz@gmail.com

    ResponderEliminar
  8. El método para implementar Event Delegation en jQuery es el siguiente: http://api.jquery.com/live/

    ResponderEliminar