angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Flask con múltiples formularios en una página publicada usando AJAX y devolviendo un resultado de formulario renderizado

Devolver un formulario al cliente significa que el cliente no necesita saber mucho sobre el formulario y reduce la codificación del lado del cliente.

22 febrero 2020
post main image
https://unsplash.com/@emilydafinchy

Mientras trabajaba en el sistema de comentarios por primera vez me encontré con el problema de tener múltiples formularios WTForms en una sola página. En cada página también hay un formulario de búsqueda, pero este no es un formulario POST . Está haciendo un GET. El sistema de comentarios utiliza dos formas:

  • Comment form
  • Comment reply form

El formulario de comentarios se encuentra inmediatamente debajo del elemento de contenido, de la entrada del blog o de la página, el formulario de respuesta a los comentarios se oculta y se inserta inicialmente y se muestra cuando el visitante hace clic en el botón de respuesta. Los formularios son casi idénticos, la principal diferencia es que el formulario de respuesta a los comentarios tiene un parámetro extra, el parent_id. Cuando se muestra el formulario de respuesta a los comentarios, el id_padre se copia en el formulario. Otras diferencias son más textuales. Ambas formas podrían ser encajadas en una sola, pero eso causaría otras dificultades como la adición de código extra para manejar el cambio de forma y la copia de los datos de la forma. Podríamos publicar los formularios sin AJAX pero al final queremos usar AJAX aquí.

Usando un formulario en una plantilla con HTML

La forma tradicional de hacerlo es enviar el formulario y recibir una respuesta (JSON) que consiste sólo en el estado. Tienes que decodificar el estado y en caso de errores, establecer los mensajes de error adecuados en algún lugar de la página.

La solución que quiero usar es diferente y apunta a minimizar el código del lado del cliente, ya usé algo así para el formulario de contacto. La idea es que la página no sabe nada sobre la forma y su respuesta. Para un formulario utilizo una plantilla Jinja que también contiene los mensajes de error y otros mensajes. Uso el macro (Bootstrap) para renderizar todos los formularios del sitio, y esto incluye renderizar los mensajes de error. Otra ventaja de este enfoque es que todas las formas y mensajes de error se ven iguales, no hay duplicación en el cliente.

El título no está contenido en esta plantilla ya que considero que es parte de la página de comentarios. En el servidor usamos el procesamiento estándar WTForms y renderizamos la plantilla del formulario. Entonces la plantilla renderizada se envía de vuelta al cliente. Creo que puedes ver la ventaja aquí. La lógica del lado del cliente es diferente porque en lugar de pinchar los mensajes de alerta y, en caso de error, los mensajes de error, en las posiciones adecuadas (ids), ahora reemplazamos una parte del HTML de la página.

Dos formularios WTForms en una página

Mi primer intento fue poner ambos formularios en la página. Te dije en una entrada anterior del blog que pongo formularios en la página usando una macro:

	{{ bootstrap_form(comment_form, id='comment_form', form_action=url_for('pages.content_item_comment_new')) }}
    ...
	{{ bootstrap_form(comment_reply_form, id='comment_reply_form', form_action=url_for('pages.content_item_comment_new')) }}

Establecí el atributo id de los formularios para poder acceder a los formularios en jQuery. Entonces los mensajes de la consola roja comenzaron a llegar, CSRF token field id estaba duplicado, submit field id estaba duplicado. ¡Esto no está permitido en HTML5! En HTML5 una identificación debe ser única en el documento.

Buscando en Internet y leyendo la documentación de WTForms parecía que la clase de formulario viene con un parámetro 'prefix'. La descripción es:

prefix - Si se proporciona, todos los campos tendrán su nombre prefix con el valor.

Usar esto es muy fácil. Tengo los siguientes formularios:

class ContentItemCommentForm(FlaskForm):

    message = TextAreaWithCounterField(_l('Your message'), 
        validators=[ InputRequired(), Length(min=6, max=1000) ] )

    submit = SubmitField(_l('Add'))


class ContentItemCommentReplyForm(FlaskForm):

    parent_uid = HiddenField('parent uid')

    message = TextAreaWithCounterField(_l('Your message'), 
        validators=[ InputRequired(), Length(min=6, max=1000) ] )

    submit = SubmitField(_l('Add'))

En la función de vista simplemente añadimos el prefixes:

    ...
    comment_form_prefix  = 'comment_form'
    comment_form = ContentItemCommentForm(prefix=comment_form_prefix)

    comment_reply_form_prefix  = 'comment_reply_form'
    comment_reply_form = ContentItemCommentReplyForm(prefix=comment_reply_form_prefix)

    if request.method == 'POST':

        if comment_form.submit.data and comment_form.validate_on_submit():
            ...
            # message
            message = comment_form.message.data
            ...

        elif comment_reply_form.submit.data and comment_reply_form.validate_on_submit():
            ...
            # parent_id
            parent_id = comment_reply_form.parent_id.data

            # message
            message = comment_reply_form.message.data
            ...

    return  render_template(
        ...
        )

Para poner los formularios en la página hacemos lo mismo que arriba, sin cambios aquí. Acceder a los elementos de la forma de jQuery también es fácil conociendo la forma prefixes. Por ejemplo, para establecer el campo oculto parent_id del formulario de respuesta al comentario podemos hacer algo como:

    $('#comment_reply_form-parent_id').val(parent_comment_id);

Todos los elementos tienen identificaciones únicas y es fácil de codificar y leer. Hasta ahora todo bien.

Publicar los formularios usando AJAX

El código jQuery para publicar un formulario no es realmente difícil. Se pueden encontrar muchos ejemplos en Internet. Como se explicó anteriormente, en un envío el servidor procesa el formulario. En caso de error, el servidor simplemente devuelve el formulario renderizado de nuevo incluyendo los mensajes de alerta y los mensajes de error. La lógica del cliente reemplaza parte del HTML de la página por el HTML devuelto. Si no hay errores, el servidor devuelve un anchor id del comentario que fue agregado. La lógica del cliente entonces hace una redirección a la misma página usando la url de la página con este anchor. Esto puede ser optimizado más tarde.

La función jQuery serialize() no incluye el botón de envío

Utilizo la función jQuery serialize() para obtener los datos de la forma. Desafortunadamente jQuery serialize() no incluye el botón de envío porque puede haber múltiples botones de envío en el formulario. Como quiero utilizar el mecanismo descrito anteriormente (determinar cuál fue el formulario que se presentó marcando el botón de envío) añado el botón de envío a los datos del formulario serializado de otra manera, no realmente difícil. Después de llamar a la función jQuery replace() debemos volver a asignar los eventos.

En la función $(document).ready se adjuntan dos eventos a un formulario:

  • El evento para publicar el formulario
  • El evento para mostrar el número de caracteres restantes

Después de reemplazar el HTML estas ataduras se pierden y deben volver a colocarse. Una vez más, no es realmente difícil. Tal vez también podamos usar la delegación de eventos, pero no investigué esto.

Las plantillas de formularios

A continuación se muestra la plantilla del formulario de comentarios. La plantilla de respuesta a los comentarios es casi idéntica. El servidor puede añadir mensajes de alerta y de error y el cliente no sabe nada de ellos. En caso de error, el elemento con id comment_form-post-container es reemplazado por el formulario renderizado HTML devuelto por la llamada AJAX .

<div id="comment_form-post-container">

    {% if comment_alert %}
    <div class="alert alert-info alert-dismissible fade show" role="alert">
        {{ comment_alert_message }}
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>

    </div>
    {% endif %}

    {% if comment_error and comment_error_type == 2 %}
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
        {{ comment_error_message }}
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>

    </div>
    {% endif %}

    {{ bootstrap_form(comment_form, id='comment_form', form_action=url_for('pages.content_item_comment_new')) }}

</div>

El formulario de comentarios se inserta en la página incluyéndolo:

    <div class="row mt-2">
        <div class="col-12 p-0">

            <h2>{{ _('Add comment') }}</h2>

        </div>
    </div>

    <div class="row mb-2">
        <div class="col-12 p-0">

            {%- include "pages/comment_form.html" -%}

        </div>
    </div>

El cliente jQuery código:

$(document).ready(function(){
    ...
    // comment form: post
    $('#comment_form').on('submit', function(event){
        event.preventDefault();
        post_comment_form('comment_form');
        return false;
    });

    // comment form: character count
    update_character_count('comment_form-message');
    $('#comment_form-message').on('change input paste', function(event){
        update_character_count('comment_form-message');
    });
    ...
});

Y el código para enviar el formulario:

function post_comment_form(form_id){

    var form = $('#'  +  form_id)

    // submit button must be added to the form data
    var submit_button = $('input[type=submit]', form);

    // get url 
    var page_url =  window.location.href.replace(/#.*/, '');
    page_url = page_url.replace(/\?.*/,"");

    $.ajax({
        data: $('#'  +  form_id).serialize()   +  '&'  +  encodeURI( $(submit_button).attr('name') )  +  '='  +  encodeURI( $(submit_button).val() ),
        type: 'POST',
        url: url_comment_new,
        contentType: 'application/x-www-form-urlencoded; charset=UTF-8', 
        dataType: 'json'
    })
    .done(function(data){
        if(data.error){

            // replace
            $('#'  +  form_id  +  '-post-container').replaceWith(data.rendered_form);

            // attach submit event again 
            $('#'  +  form_id).on('submit', function(event){
                event.preventDefault();
                post_comment_form(form_id);
                return false;
            });

            // attach character count event again 
            update_character_count(form_id  +  '-message');
            $('#'  +  form_id  +  '-message').on('change input paste', function(event){
                update_character_count(form_id  +  '-message');
            });

        }else{

            var new_url = page_url  +  '?t='  +  (+new Date())  +  '#'  +  data.comment_anchor;
            window.location.href = new_url;

        }
    });
}

Saltando a un comentario insertado usando un anchor

Después de que un comentario ha sido insertado en la base de datos por el servidor debemos mostrar el nuevo árbol de comentarios. Podemos hacer esto quedándonos en el código de cliente de uso pero por el momento elijo devolver una llamada anchor a la AJAX . La página es entonces llamada, refrescada, usando el url de la página y el anchor. Añado una marca de tiempo para evitar problemas con el caching del navegador. En HTML5 el id se usa como anchor. Como uso Bootstrap con una barra de navegación pegajosa también debemos compensar los píxeles extra de la barra de navegación:

.anchor5 {
    display: block;
    position: relative;
    top: -100px;
    visibility: hidden;
}

Resumen

Lo anterior muestra una posible implementación de Flask, WTforms y AJAX. Intenté reducir el código del lado del cliente devolviendo un formulario renderizado que incluía mensajes de alerta y de error en lugar de sólo mensajes de error. La protección CSRF también funciona porque usamos FlaskForm. De la documentación de la extensión Flask_WTF: Cualquier vista que utilice FlaskForm para procesar la solicitud ya está recibiendo la protección CSRF . Hay mucho más trabajo por hacer pero pensé en compartir esto con ustedes.

Enlaces / créditos

Combining multiple forms in Flask-WTForms but validating independently
https://dev.to/sampart/combining-multiple-forms-in-flask-wtforms-but-validating-independently-cbm

flask-bootstrap with two forms in one page
https://stackoverflow.com/questions/39738069/flask-bootstrap-with-two-forms-in-one-page/39739863#39739863

Forms
https://wtforms.readthedocs.io/en/stable/forms.html

Multiple forms in a single page using flask and WTForms
https://stackoverflow.com/questions/18290142/multiple-forms-in-a-single-page-using-flask-and-wtforms

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.