25 de mayo de 2016

Problema de jQuery con formularios anidados

Vamos a presentar un problema que sucede con la librería jQuery con los formularios anidados.

No es recomendable usar formularios anidados, pero es posible encontrar esa situación en código heredado.

Es muy cómodo y fácil usar jQuery para hacerle un lavado de cara a antiguas aplicaciones web, ya sea para incorporar alguna funcionalidad Ajax o simplemente mejorar la experiencia de usurio (UX).

En ese sentido, la facilidad de jQuery o mejor dicho de Javascript es hacer que todo esto suceda sólo a partir de uno o dos enlaces desde la página HTML y sin modificar el resto de su contenido:

<script src="js/jquery.js"></script>
<script src="my_script.js"></script>

Incluso, si la página ya tuviera un enlace a un archivo Javascript, por ejemplo <script src="js/main.js"></script> podríamos fusionar todo el código en un sólo archivo Javascript, y el código HTML de la página quedaría inalterable, aunque no parece una alternativa elegante.

Sin embargo, hay un problema de usar jQuery con formularios HTML anidados como veremos más adelante.

Enviando formularios con jQuery

Enviar formularios (de la manera tradicional) usando jQuery es muy simple. Sólo necesitamos determinar cual será el elemento disparador y seleccionar el formulario a enviar.
Por ejemplo usando el siguiente snippet HTML:

<form name="ordenar" action="action_1.php" method="post">
    ....
</form>
<form name="asignar" action="action_1.php" method="post">
    ....
</form>
...
<a href="alguna_url">algún texto</a>
...
<a href="alguna_url">otro texto</a>

Podríamos despachar los formularios sin tocar el botón submit, usando sólo un par de enlaces y empleando el método Submit del DOM HTML de Javascipt, por ejemplo:

$(document).on('ready', function() {


    // agregando el comportamiento al primer enlace:
    $('a[href="alguna_url"]').on('click', function(e) {

        e.preventDefault();
        $('form')[0].submit();

    });

    // agregando el comportamiento al segundo enlace:
    $('a[href="otra_url"]').on('click', function(e) {

        e.preventDefault();
        $('form')[1].submit();

    });

});

Naturalmente, se puede usar Ajax pero en este ejemplo estamos usando el método de envío tradicional usando jQuery.

Ahora veamos que sucede en el caso de formularios anidados.

Problema con los formularios anidados

Tomemos como ejemplo una estructura exageradamente caotica de formularios (HTML), casi el peor de los casos y veamos como se comporta jQuery:

<!-- nested forms -->
<form action="#" id="11">

    <form action="#" id="21"></form>
    <form action="#" id="22"></form>
    <form action="#" id="23">

        <form action="#" id="31"></form>
        <form action="#" id="32">

            <form action="#" id="41"></form>
            <form action="#" id="42"></form>
        </form>

    </form>

    <form action="#" id="24"></form>
</form>

En este ejemplo, poner especial atención en el primer elemento anidado, estos formularios serían:

  • formulario 21: es el primer elemento anidado del formulario 11.
  • formulario 31: es el primer elemento anidado del formulario 23.
  • formulario 41: es el primer elemento anidado del formulario 32.

La detección de los formularios en jQuery se efectuaría del siguiente modo:

$(document).on('ready', function() {

    $('form').on('click', function(e) {

        var target_form = this.id;

        // mostrando en consola el formulario seleccionado:
        console.log(target_form);

        e.stopPropagation();

    });
});

Entonces si abrimos la consola del navegador podemos ver el resultado de la ejecución del script que nos entrega información acerca del elemento seleccionado.

Para una mejor experiencia visual, podemos sustituir el código HTML anterior con uno equivalente junto al CSS y apreciar lo que sucede:

Código HTML:

<form action="#" id="11">
    <fieldset>

        <form action="#" id="21">
            <fieldset></fieldset>
        </form>

        <form action="#" id="22">
            <fieldset></fieldset>
        </form>

        <form action="#" id="23">
            <fieldset>

                <form action="#" id="31">
                    <fieldset></fieldset>
                </form>

                <form action="#" id="32">
                    <fieldset>

                        <form action="#" id="41">
                            <fieldset></fieldset>
                        </form>

                        <form action="#" id="42">
                            <fieldset></fieldset>
                        </form>

                    </fieldset>
                </form>

            </fieldset>
        </form>

        <form action="#" id="24">
            <fieldset></fieldset>
        </form>

    </fieldset>
</form>

Código CSS en el head del HTML:

<style>

    fieldset {
        padding: 8px;
        margin-bottom: 10px;
    }

</style>

Al ejecutar la página, apreciaremos (desde consola del navegador) que el primer formulario anidado es inaccesible desde jQuery, es decir, no tenemos acceso a sus propiedades.

Aunque el primer formulario anidado, cualquiera sea su nivel o profundidad de anidamiento es innaccesible, no sucede lo mismo con sus elementos internos, es decir: cuadros de texto, botones de selección u otros.

Conclusión

Este comportamiento fue observado usando las últimas versiones de jQuery a la fecha. El problema de este asunto, en primer lugar que si desconocemos esta anomalía podría conducirnos a problemas y en segundo lugar, es que se nos hace impsible asociar correctamente los campos (elementos) pertenecientes al formulario y manejer el despacho de la info desde jQuery.

Claro se podría agregar antes de cada uno de los primeros formularios anidados el código: <form></form> pero ya deberíamos modificar el HTML, incluso si se nos pasó la idea de incluir desde el Javascipt aún así no funcionará.

$(document).on('ready', function() {

    ...

    $('form').prepend('<form></form>');
    ...


});

Y tampoco funcionará agregándolo desde CSS, por ejemplo:

form::before {
    content: "<form>";
}
form::after {
    content: "</form>";
}