angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Flask avec plusieurs formulaires sur une page affichée à l'aide de AJAX et renvoyant un résultat de formulaire rendu

22 février 2020 à côté de Peter

Le fait de renvoyer un formulaire rendu au client signifie que ce dernier n'a pas besoin d'en savoir beaucoup sur le formulaire et réduit le codage du côté client.

post main image
https://unsplash.com/@emilydafinchy

En travaillant sur le système de commentaires pour la première fois, je me suis heurté au problème d'avoir plusieurs formulaires WTForms sur une seule page. Sur chaque page se trouve également le formulaire de recherche, mais il ne s'agit pas d'un formulaire POST . Il fait un GET. Le système de commentaires utilise deux formulaires :

  • Comment form
  • Comment reply form

Le formulaire de commentaire se trouve immédiatement sous l'élément de contenu, l'article de blog ou la page, le formulaire de réponse au commentaire est initialement caché et inséré et s'affiche lorsque le visiteur clique sur le bouton Répondre. Les formulaires sont presque identiques, la principale différence est que le formulaire de réponse aux commentaires comporte un paramètre supplémentaire, le parent_id. Lorsque le formulaire de réponse aux commentaires est affiché, le parent_id est copié dans le formulaire. D'autres différences sont plus textuelles. Les deux formulaires pourraient être regroupés en un seul, mais cela entraînerait d'autres difficultés, comme l'ajout d'un code supplémentaire pour gérer le changement de formulaire et la copie des données du formulaire. Nous pourrions poster les formulaires sans AJAX mais au final nous voulons utiliser AJAX ici.

Utilisation d'un formulaire dans un modèle avec HTML

La façon traditionnelle de procéder consiste à envoyer le formulaire et à recevoir une réponse (JSON) qui ne comporte que le statut. Vous devez décoder le statut et, en cas d'erreur, définir les messages d'erreur appropriés quelque part sur la page.

La solution que je veux utiliser est différente et vise à minimiser le code côté client, j'ai déjà utilisé quelque chose comme ça pour le formulaire de contact. L'idée est que la page ne sait rien du formulaire et de sa réponse. Pour un formulaire, j'utilise un modèle Jinja qui contient également les messages d'erreur et autres messages. J'utilise la macro (Bootstrap) pour rendre tous les formulaires sur le site, et cela inclut le rendu des messages d'erreur. Un autre avantage de cette approche est que tous les formulaires et les messages d'erreur se ressemblent, sans duplication chez le client.

Le titre ne figure pas dans ce modèle car je considère cette partie de la page des commentaires. Sur le serveur, nous utilisons le traitement standard WTForms et rendons le modèle de formulaire. Ensuite, le modèle rendu est renvoyé au client. Je pense que vous pouvez voir l'avantage ici. La logique côté client est différente car au lieu d'insérer des messages d'alerte et, en cas d'erreur, des messages d'erreur, aux bonnes positions (ids), nous remplaçons maintenant une partie du HTML sur la page.

Deux formulaires WTForms sur une page

Ma première tentative a été de mettre les deux formulaires sur la page. Je vous ai dit dans un précédent billet de blog que je mettais des formulaires sur la page en utilisant une 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')) }}

J'ai défini l'attribut id des formulaires afin de pouvoir accéder aux formulaires dans jQuery. Puis les messages rouges de la console ont commencé à arriver, l'identifiant du champ du jeton CSRF était en double, l'identifiant du champ submit était en double. Cela n'est pas autorisé au HTML5 ! Dans HTML5, un identifiant doit être unique dans le document.

En cherchant sur Internet et en lisant la documentation WTForms , il est apparu que la classe de formulaire est fournie avec un paramètre "prefix". La description est :

prefix - Si cette valeur est fournie, le nom de tous les champs sera prefix avec la valeur.

Il est très facile à utiliser. Je dispose des formulaires suivants :

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'))

Dans la fonction de visualisation, nous ajoutons simplement les 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(
        ...
        )

Pour mettre les formulaires sur la page nous faisons la même chose que ci-dessus, aucun changement ici. Il est également facile d'accéder aux éléments du formulaire jQuery en connaissant le formulaire prefixes. Par exemple, pour définir le champ caché parent_id du formulaire de réponse aux commentaires, nous pouvons faire quelque chose comme

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

Tous les éléments ont des identifiants uniques et il est facile à coder et à lire. Jusqu'à présent, tout va bien.

Envoi des formulaires à l'aide de AJAX

Le code jQuery pour poster un formulaire n'est pas vraiment difficile. De nombreux exemples peuvent être trouvés sur Internet. Comme expliqué ci-dessus, le serveur traite le formulaire lors d'une soumission. En cas d'erreur, le serveur renvoie simplement le formulaire rendu, y compris les messages d'alerte et les messages d'erreur. La logique du client remplace une partie de la HTML de la page par la HTML retournée. S'il n'y a pas d'erreurs, le serveur renvoie un identifiant anchor du commentaire qui a été ajouté. La logique du client effectue alors une redirection vers la même page en utilisant l'url de la page avec ce anchor. Cela peut être optimisé ultérieurement.

La fonction jQuery serialize() n'inclut pas le bouton "submit

J'utilise la fonction jQuery serialize() pour obtenir les données du formulaire. Malheureusement, le formulaire jQuery serialize() ne comporte pas de bouton d'envoi car il peut y avoir plusieurs boutons d'envoi sur le formulaire. Comme je veux utiliser le mécanisme décrit ci-dessus (déterminer quel formulaire a été soumis en cochant le bouton "soumettre"), j'ajoute le bouton "soumettre" aux données sérialisées du formulaire d'une autre manière, pas vraiment difficile. Après avoir appelé la fonction jQuery replace(), nous devons rattacher les événements.

Dans la fonction $(document).ready , deux événements sont joints à un formulaire :

  • L'événement pour poster le formulaire
  • L'événement pour montrer le nombre de caractères restants

Après avoir remplacé le HTML , ces reliures se perdent et doivent être à nouveau attachées. Là encore, ce n'est pas vraiment difficile. Peut-être que nous pouvons aussi utiliser la délégation d'événements, mais je n'ai pas fait de recherches à ce sujet.

Les modèles de formulaires

Vous trouverez ci-dessous le modèle du formulaire de commentaires. Le modèle de réponse aux commentaires est presque identique. Le serveur peut ajouter des messages d'alerte et des messages d'erreur et le client n'en sait rien. En cas d'erreur, l'élément avec l'identifiant comment_form-post-container est remplacé par le formulaire rendu HTML renvoyé par l'appel 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>

Le formulaire de commentaire est inséré dans la page en l'incluant :

    <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>

Le code du client jQuery :

$(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');
    });
    ...
});

Et le code pour soumettre le formulaire :

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;

        }
    });
}

Passer à un commentaire inséré en utilisant un anchor

Après qu'un commentaire ait été inséré dans la base de données par le serveur, nous devons montrer le nouvel arbre de commentaires. Nous pouvons le faire en restant sur le code client d'utilisation mais pour l'instant je choisis de renvoyer un anchor à l'appel AJAX . La page est alors appelée, rafraîchie, en utilisant l'url de la page et le anchor. J'ajoute un horodatage pour éviter les problèmes de mise en cache du navigateur. Dans HTML5, l'identifiant est utilisé comme un anchor. Comme j'utilise Bootstrap avec une barre de navigation collante, nous devons également compenser les pixels supplémentaires de la barre de navigation :

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

Résumé

Ce qui précède montre une possible mise en œuvre de Flask, WTforms et AJAX. J'ai essayé de réduire le code côté client en renvoyant un formulaire rendu comprenant des messages d'alerte et des messages d'erreur au lieu de simples messages d'erreur. La protection CSRF fonctionne également parce que nous utilisons FlaskForm. Extrait de la documentation de l'extension Flask_WTF : Toute vue utilisant FlaskForm pour traiter la demande obtient déjà la protection CSRF . Il y a encore beaucoup de travail à faire, mais j'ai pensé à vous en faire part.

Liens / crédits

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

En savoir plus...:
AJAX WTForms

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.