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

Un textarea avec un compteur de caractères widget pour Flask, WTForms et Bootstrap

Ajouter un WTForms textarea widget semble facile mais les différences entre Linux et Windows causent des problèmes inattendus.

15 février 2020
Dans Flask, WTForms
post main image
https://unsplash.com/@creativegangsters

J'espérais vous dire aujourd'hui que vous pouviez dès à présent commenter les articles de ce site web. Cela aurait signifié que j'aurais achevé la première mise en œuvre du système de commentaires. Malheureusement, je suis tombé sur quelques problèmes, oui bien sûr, je suis un programmeur, et l'un d'entre eux concernait le TextAreaField.

Je voulais juste une version simple et étendue du WTForm TextAreaField, il suffit d'ajouter un champ de compteur de caractères sous le textarea et c'est tout. Je pensais que cela prendrait quelques heures, mais je me suis trompé, j'ai résolu le problème et j'ai pensé à vous en faire part. Faites-moi savoir ce que vous en pensez ... hmmm ... lorsque les commentaires seront en ligne ... :-)

J'utilise Bootstrap et jQuery. Pour la jQuery , la textarea avec les caractères restants a été documentée à de nombreuses reprises. De nombreuses solutions suggèrent de suivre l'exemple de Twitter. Cela signifie que vous continuez à afficher le texte intégral même si le nombre de caractères dépasse le nombre autorisé. Si le nombre de caractères dépasse le nombre autorisé, nous indiquons le nombre de caractères dans la couleur rouge. Pas de problème, je vais mettre en œuvre.

Préciser le nombre maximum de caractères à un endroit

Je veux que le widget soit universel, donc pas de valeurs codées en dur dedans. jQuery est utilisé pour compter le nombre réel de caractères mais comment jQuery sait-il quel est le maximum autorisé ? Nous pouvons définir des constantes et les utiliser partout, mais il est plus souple de mettre en œuvre des attributs de données supplémentaires HTML5 pour le textarea :

  • data-char-count-min
  • data-char-count-max

Nous ajoutons un élément supplémentaire, indiquant le nombre de caractères restants sous le code textarea. Les données peuvent être référencées par le code jQuery et utilisées pour calculer le nombre de caractères restants.

Mise en œuvre

Là encore, nous examinons d'abord le code WTForms pour le TextAreaField. Voir aussi un article précédent. J'ai copié ce code et je l'ai modifié en ceci :

class TextAreaWithCounterWidget(object):
    """
    Renders a multi-line text area.

    `rows` and `cols` ought to be passed as keyword args when rendering.
    """
    def __init__(self):
        pass

    def __call__(self, field, **kwargs):
        fname = 'TextAreaWithCounterWidget - __call__'

         kwargs.setdefault('id', field.id)
        if 'required' not in  kwargs  and 'required' in getattr(field, 'flags', []):
             kwargs['required'] = True

        return  HTMLString('<textarea  %s>%s</textarea>' % (
            html_params(name=field.name, **kwargs),
            escape(text_type(field._value()), quote=False)
        )  +  '<span class="" id="'  +  field.id  +  '-char-count-num'  +  '"></span>' )


class TextAreaWithCounterField(StringField):
    """
    This field represents an  HTML  ``<textarea>`` and can be used to take
    multi-line input.
    """
     widget  = TextAreaWithCounterWidget()

Ensuite, je l'utilise comme suit :

def strip_whitespace(s):
    if isinstance(s, str):
        s = s.strip()
    return s

class ContentItemCommentForm(FlaskForm):

    message = TextAreaWithCounterField(_l('Your message'), 
        render_kw={'data-char-count-min': 0, 'data-char-count-max': 1000, 'rows': 4},
        validators=[ InputRequired(), Length(min=6, max=1000) ],
        filters=[ strip_whitespace ] )

    submit = SubmitField(_l('Add'))

Parfois, les choses sont plus faciles que prévu. Nous pouvons simplement ajouter nos nouveaux paramètres au formulaire en utilisant render_kw. Ensuite, dans le widget , ils sont passés comme attributs au textarea. Si nous le souhaitons, nous pouvons également accéder à ces paramètres dans le widget en utilisant le kwargs. Nous pouvons les référencer comme :

    data_char_count_max =  kwargs['data-char-count-max']

Nous pouvons également les extraire de kwargs, ce qui signifie lire et supprimer. Ils n'apparaîtront alors pas en tant qu'attributs dans le textarea :

    char_count_max =  kwargs.pop('char_count_max',  None)

Mais il n'est pas nécessaire de l'utiliser ici. Dans le widget , nous pouvons également accéder aux valeurs du validateur de longueur, mais là encore, ce n'est pas nécessaire.

J'ai également spécifié le nombre de lignes dans render_kw. Il est transmis au champ textarea . Le HTML généré est ajouté avec l'élément supplémentaire indiquant le nombre de caractères restants. L'identifiant de cet élément est construit à partir de l'identifiant textarea :

    field.id  +  '-char-count-num'

Le filtre strip_whitespace est appelé à couper l'espace blanc avant et arrière.

Le code jQuery n'est pas si difficile. J'utilise Bootstrap, les classes changent la couleur et fixent le rembourrage :

function update_character_count(textarea_id){

	var char_count_num_id =  textarea_id  +  '-char-count-num';

	if( ($("#"  +   textarea_id).length == 0) || ($("#"  +  char_count_num_id).length == 0) ){
		// must exist
		return;
	}

	var char_count_min = parseInt( $('#'  +   textarea_id).data('char-count-min'), 10 );
	var char_count_max = parseInt( $('#'  +   textarea_id).data('char-count-max'), 10 );

	var remaining = char_count_max - $('#'  +   textarea_id).val().length;
	$('#'  +  char_count_num_id).html( ''  +  remaining );

	if(remaining >= 0){
		$('#'  +  char_count_num_id).removeClass('text-danger');
		$('#'  +  char_count_num_id).addClass('pl-2 text-secondary');
	}else{
		$('#'  +  char_count_num_id).removeClass('text-secondary');
		$('#'  +  char_count_num_id).addClass('pl-2 text-danger');
	}
}

et :

$(document).ready(function(){
	...
	// 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');
	});
}

Test et décalage dans le décompte des caractères

Nous pouvons maintenant commencer à tester le nouveau TextAreaWithCounterField. Tout avait l'air bien jusqu'à ce que je commence à entrer dans les nouvelles lignes. La longueur a été déclarée correcte par jQuery , ce qui signifie qu'ils ont été comptés comme un seul caractère. Mais le validateur WTForms a déclaré que la longueur maximale était dépassée. Encore une fois, au débogage, dans le widget , j'ai imprimé les caractères reçus par le WTForms :

    message = field.data
    if message is  None:
         current_app.logger.debug(fname  +  ': message is  None')
    else:
         current_app.logger.debug(fname  +  ': message = {}, len(message) = {}, list(message) = {}'.format(message, len(message), list(message)))

Cela m'a donné le résultat suivant :

    len(message) = 4, list(message) = ['a', '\r', '\n', 'b']

jQuery compte \r\n comme un seul caractère mais WTForms le compte comme deux caractères. En creusant dans le code WTForms , validators.py, on voit qu'il utilise la fonction len Python pour déterminer la longueur :

    l = field.data and len(field.data) or 0

Compréhensible mais dans ce cas, c'est faux ! Que faire ? Je ne voulais pas passer outre la fonction de validation par défaut WTForms . Heureusement, nous avons le champ de filtres qui sont appelés avant ( !) la validation. J'utilisais déjà strip_whitespace et j'ai ajouté un nouveau filtre :

def compress_newline(s):
    if isinstance(s, str):
        s = s.replace("\r\n", "\n")
        s = re.sub(r'\n+', '\n', s)
    return s

Ce filtre remplace \r\n par un seul \n. En outre, il remplace plusieurs caractères \n par un seul \n. Ce dernier n'est qu'une protection très primitive contre les inévitables soumissions erronées (et folles). La ligne de filtrage devient alors :

    filters=[ strip_whitespace, compress_newline ] )

Maintenant, cela a fonctionné comme prévu.

Résumé

L'utilisation de render_kw est un moyen facile de passer des paramètres au widget. J'ai cherché sur internet le problème \r\n avec le WTForms TextAreaField mais je n'ai trouvé aucune référence. Ne me dites pas que je suis le seul. Le problème est causé par une inadéquation entre les systèmes Windows et Linux . En outre, le codage Javascript/jQuery prend du temps si vous ne le faites pas tout le temps.

Liens / crédits

How to specify rows and columns of a <textarea > tag using wtforms
https://stackoverflow.com/questions/4930747/how-to-specify-rows-and-columns-of-a-textarea-tag-using-wtforms

New line in text area
https://stackoverflow.com/questions/8627902/new-line-in-text-area

Using data attributes
https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes

WTForms - Fields
https://wtforms.readthedocs.io/en/stable/fields.html

WTForms - Widgets
https://wtforms.readthedocs.io/en/stable/widgets.html

En savoir plus...

Flask WTForms

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires (1)

Laissez une réponse

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

avatar

Thanks for sharing! I wrote long comment but token has expired since I was reading the other tabs and I lost my comment :/. In short - great article and real demo, which I can see here, while typing this one again.