Un textarea con un contador de caracteres widget para Flask, WTForms y Bootstrap
Añadir un WTForms textarea widget parece fácil pero las diferencias entre Linux y Windows causan problemas inesperados.
Esperaba decirles hoy que ahora pueden comentar las entradas del blog de este sitio web. Eso habría significado que yo completara la primera aplicación del sistema de comentarios. Desafortunadamente me tropecé con algunos problemas, sí por supuesto, soy un programador, y uno de ellos involucraba el TextAreaField.
Sólo quería una versión extendida simple del WTForm TextAreaField, sólo añadir un campo de contador de caracteres debajo del textarea y eso es todo. Pensé que esto tomaría unas pocas horas pero estaba totalmente equivocado, pero de alguna manera lo resolví y pensé en compartirlo contigo. Hazme saber lo que piensas... hmmm... cuando los comentarios estén en línea... :-)
Estoy usando Bootstrap y jQuery. Para jQuery el textarea con los caracteres restantes ha sido documentado muchas veces. Muchas soluciones sugieren seguir el ejemplo de Twitter. Esto significa que sigues mostrando el texto completo aunque el número de caracteres exceda el número permitido. Si el número de caracteres excede el número permitido, mostramos el número de caracteres en el color rojo. No hay problema, lo implementaré.
Especificando el número máximo de caracteres en un lugar
Quiero que el widget sea universal, por lo que no hay valores codificados en él. jQuery se utiliza para contar el número real de caracteres pero, ¿cómo sabe jQuery cuál es el máximo permitido? Podemos definir constantes y usarlas en todas partes, pero es más flexible implementar atributos de datos extra HTML5 para el textarea:
- data-char-count-min
- data-char-count-max
Añadimos un elemento extra, mostrando el número de caracteres restantes debajo del textarea. Los datos de los atributos pueden ser referenciados por el código jQuery y usados para calcular el número de caracteres restantes.
Implementación
De nuevo, primero miramos el código WTForms para el TextAreaField. Ver también un post anterior. Copié este código y lo modifiqué en esto:
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()
Entonces lo uso de la siguiente manera:
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'))
A veces las cosas son más fáciles de lo esperado. Podemos simplemente añadir nuestros nuevos parámetros al formulario usando render_kw. Luego en el widget se pasan como atributos al textarea. Si queremos, también podemos acceder a estos parámetros en el widget usando kwargs. Podemos referirnos a ellos como:
data_char_count_max = kwargs['data-char-count-max']
También podemos hacerlos estallar desde kwargs, que significa leer y eliminar. Entonces no aparecerán como atributos en el textarea:
char_count_max = kwargs.pop('char_count_max', None)
Pero no hay necesidad de usar esto aquí. Dentro del widget también podemos acceder a los valores del validador de longitud, pero de nuevo, no es necesario hacerlo aquí.
También especifiqué el número de filas en render_kw. Esto se pasa al campo textarea . El HTML generado se añade con el elemento extra que muestra el número de caracteres restantes. El id de este elemento se construye a partir del id de textarea :
field.id + '-char-count-num'
La tira de filtro del espacio blanco es llamada para recortar el espacio blanco que va adelante y atrás.
El código jQuery no es tan difícil. Estoy usando Bootstrap, las clases cambian el color y establecen el relleno:
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');
}
}
y:
$(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');
});
}
Las pruebas y la falta de coincidencia en el recuento de caracteres
Ahora podemos empezar a probar el nuevo TextAreaWithCounterField. Todo parecía estar bien hasta que empecé a entrar en las nuevas líneas. La longitud fue reportada como correcta por jQuery , lo que significa que fueron contados como un solo carácter. Pero el validador WTForms dijo que la longitud máxima fue excedida. Una vez más, en el widget imprimí los caracteres recibidos por 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)))
Esto me dio el siguiente resultado:
len(message) = 4, list(message) = ['a', '\r', '\n', 'b']
jQuery cuenta \r\n como un solo carácter, pero WTForms lo cuenta como dos caracteres. Excavando en el código WTForms , validadores.py, vemos que utiliza la función len Python para determinar la longitud:
l = field.data and len(field.data) or 0
Es comprensible, pero en este caso está mal. ¿Qué hacer? No quería anular la función de validación WTForms por defecto. Afortunadamente tenemos el campo de filtros que se llaman antes (!) de la validación. Ya estaba usando el espacio en blanco y añadí un nuevo filtro:
def compress_newline(s):
if isinstance(s, str):
s = s.replace("\r\n", "\n")
s = re.sub(r'\n+', '\n', s)
return s
Este filtro sustituye a \r\n por un solo \n. Además, reemplaza varios caracteres \n por un solo \n. Esta última es sólo una protección muy primitiva contra los inevitables envíos erróneos (y locos). La línea de filtros se convierte entonces:
filters=[ strip_whitespace, compress_newline ] )
Ahora funcionó como estaba previsto.
Resumen
Usar render_kw es una forma fácil de pasar parámetros al widget. Busqué en Internet el problema de \r\n con el WTForms TextAreaField pero no pude encontrar ninguna referencia. Por favor, no me digas que soy el único. El problema es causado por un desajuste entre los sistemas Windows y Linux . Además, la codificación de Javascript/jQuery toma tiempo si no se hace todo el tiempo.
Enlaces / créditos
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
Deje un comentario
Comente de forma anónima o inicie sesión para comentar.
Comentarios (1)
Deje una respuesta.
Responda de forma anónima o inicie sesión para responder.
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.
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow