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

Flask met meerdere formulieren op een pagina geplaatst met behulp van AJAX en het terugsturen van een weergegeven formulierresultaat

Het terugsturen van een gerenderd formulier naar de klant betekent dat de klant niet veel hoeft te weten over het formulier en vermindert de codering aan de klantzijde.

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

Toen ik voor het eerst aan het opmerkingensysteem werkte, stuitte ik op het probleem om meerdere WTForms -formulieren op één pagina te hebben. Op elke pagina staat ook het zoekformulier, maar dit is geen POST formulier. Het doet een GET. Het opmerkingensysteem maakt gebruik van twee vormen:

  • Comment form
  • Comment reply form

Het reactieformulier staat direct onder het inhoudselement, blogbericht of pagina, het reactieformulier wordt in eerste instantie verborgen en ingevoegd en getoond wanneer de bezoeker op de knop Reply klikt. De formulieren zijn vrijwel identiek, het belangrijkste verschil is dat het commentaar-antwoordformulier één extra parameter heeft, de parent_id. Wanneer het commentaar antwoordformulier wordt getoond, wordt de parent_id gekopieerd naar het formulier. Andere verschillen zijn meer tekstueel. Beide formulieren zouden in één formulier kunnen worden samengevoegd, maar dat zou andere problemen opleveren, zoals het toevoegen van extra code voor het verwisselen van formulieren en het kopiëren van formuliergegevens. We zouden de formulieren zonder AJAX kunnen plaatsen, maar uiteindelijk willen we hier AJAX gebruiken.

Gebruik van een formulier in een sjabloon met HTML

De traditionele manier om dit te doen is door het formulier te versturen en een (JSON) antwoord te ontvangen dat alleen bestaat uit status. U dient de status te decoderen en in geval van fouten ergens op de pagina de juiste foutmeldingen in te stellen.

De oplossing die ik wil gebruiken is anders en is gericht op het minimaliseren van client-side code, ik heb zoiets al gebruikt voor het contactformulier. Het idee is dat de pagina niets weet over het formulier en de reactie daarop. Voor een formulier gebruik ik een Jinja sjabloon dat ook de foutmeldingen en andere berichten bevat. Ik gebruik (Bootstrap) macro om alle formulieren op de site weer te geven, inclusief de foutmeldingen. Een ander pluspunt van deze aanpak is dat alle formulieren en foutmeldingen er hetzelfde uitzien, geen duplicatie in de klant.

De titel staat niet in dit sjabloon, omdat ik dit deel van de commentaarpagina beschouw. Op de server gebruiken we de standaard WTForms verwerking en geven we het formuliersjabloon weer. Vervolgens wordt het gerenderde sjabloon teruggestuurd naar de klant. Ik denk dat je hier het voordeel ziet. De client-side logica is anders, want in plaats van waarschuwingsberichten en, in geval van een fout, foutmeldingen, op de juiste posities (id's), vervangen we nu een deel van de HTML op de pagina.

Twee WTForms formulieren op een pagina

Mijn eerste poging was gewoon om beide formulieren op de pagina te zetten. Ik heb je in een vorige blogpost verteld dat ik formulieren op de pagina heb gezet met behulp van een 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')) }}

Ik stel het id-attribuut van de formulieren in zodat ik de formulieren in jQuery kan benaderen. Toen begonnen de rode console berichten te komen, CSRF token veld id was duplicaat, submit veld id was duplicaat. Dit is niet toegestaan in HTML5! In HTML5 moet een id uniek zijn in het document.

Bij het zoeken op internet en het lezen van de WTForms documentatie bleek dat de formulierklasse wordt geleverd met een 'prefix' parameter. De beschrijving is:

prefix - Indien voorzien, zullen alle velden hun naam prefix hebben met de waarde.

Het gebruik hiervan is zeer eenvoudig. Ik heb de volgende formulieren:

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

In de weergavefunctie voegen we eenvoudigweg de prefixes toe:

    ...
    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(
        ...
        )

Om de formulieren op de pagina te zetten doen we hetzelfde als hierboven, geen wijzigingen hier. De toegang tot formulierelementen uit jQuery is ook gemakkelijk te weten de vorm prefixes. Om bijvoorbeeld het parent_id verborgen veld van het commentaar-antwoordformulier in te stellen kunnen we zoiets doen:

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

Alle elementen hebben unieke id's en het is gemakkelijk te coderen en te lezen. Tot zover is het goed.

Het plaatsen van de formulieren met behulp van AJAX

De jQuery code om een formulier te posten is niet echt moeilijk. Er zijn vele voorbeelden te vinden op het internet. Zoals hierboven uitgelegd, verwerkt de server het formulier op een verzendingsformulier. In het geval van een fout stuurt de server gewoon weer het gerenderde formulier terug, inclusief waarschuwingsberichten en foutmeldingen. De cliëntlogica vervangt een deel van de HTML op de pagina door de HTML terug. Als er geen fouten zijn, geeft de server een anchor id terug van het commentaar dat is toegevoegd. De client logica doet dan een redirect naar dezelfde pagina met behulp van de pagina url met deze anchor. Dit kan later worden geoptimaliseerd.

De functie jQuery serialize() bevat niet de knop "submit".

Ik gebruik de functie jQuery serialize() om de gegevens van het formulier te verkrijgen. Helaas bevat jQuery serialize() niet de verzendknop omdat er meerdere verzendingsknoppen op het formulier kunnen staan. Omdat ik het hierboven beschreven mechanisme wil gebruiken (bepaal welk formulier werd ingediend door de verzendknop aan te vinken) voeg ik de verzendknop op een andere manier toe aan de geserialiseerde formuliergegevens, niet echt moeilijk. Na het oproepen van de jQuery replace() functie moeten we de gebeurtenissen opnieuw koppelen.

In de $(document).ready functie zijn twee gebeurtenissen aan een formulier gekoppeld:

  • Het evenement om het formulier te posten
  • Het evenement om het aantal resterende karakters te tonen

Na het vervangen van de HTML gaan deze bindingen verloren en moeten ze opnieuw worden bevestigd. Opnieuw niet echt moeilijk. Misschien kunnen we ook gebruik maken van evenementdelegatie, maar ik heb dit niet onderzocht.

De formuliersjablonen

Hieronder staat het sjabloon van het commentaarformulier. Het antwoordsjabloon voor opmerkingen is vrijwel identiek. De server kan waarschuwingsberichten en foutmeldingen toevoegen en de client weet daar niets van. In geval van een fout wordt het element met id comment_form-post-container vervangen door het weergegeven formulier HTML teruggestuurd door de AJAX oproep.

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

Het commentaarformulier wordt in de pagina ingevoegd door het op te nemen:

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

De cliënt jQuery code:

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

En de code om het formulier in te dienen:

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;

        }
    });
}

Springen naar een ingevoegde opmerking met behulp van een anchor

Nadat een commentaar is ingevoegd in de database door de server moeten we de nieuwe commentaarboom laten zien. We kunnen dit doen door op de gebruikscode te blijven, maar op dit moment kies ik ervoor om een anchor terug te sturen naar de AJAX oproep. De pagina wordt dan aangeroepen, ververst, met behulp van de pagina url en de anchor. Ik voeg een tijdstempel toe om problemen met de browser caching te voorkomen. In HTML5 wordt de id gebruikt als een anchor. Omdat ik Bootstrap gebruik met een kleverige navbar moeten we ook de extra navbarpixels compenseren:

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

Samenvatting

Het bovenstaande toont een mogelijke implementatie van Flask, WTforms en AJAX. Ik heb geprobeerd de code van de klant te verminderen door een gerenderd formulier met waarschuwingsberichten en foutmeldingen terug te sturen in plaats van alleen maar foutmeldingen. CSRF bescherming werkt ook omdat we FlaskForm gebruiken. Uit de Flask_WTF uitbreidingsdocumentatie: Elke view die FlaskForm gebruikt om de aanvraag te verwerken, krijgt al CSRF bescherming. Er is nog veel meer werk te doen, maar ik dacht om dit met u te delen.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.