From d7b0faead30328e9d6d519ec77ec2c932564577f Mon Sep 17 00:00:00 2001
From: maxim-f1 <“maxconal228@gmail.com”>
Date: Fri, 10 Jul 2026 16:17:59 +0300
Subject: [PATCH 1/4] Add auto-resizing textarea widget on input.
---
sqladmin/fields.py | 13 +++++++++++++
sqladmin/forms.py | 2 +-
sqladmin/statics/css/main.css | 9 +++++++++
sqladmin/statics/js/main.js | 30 ++++++++++++++++++++++++++++++
sqladmin/widgets.py | 29 ++++++++++++++++++++++++++++-
tests/test_fields.py | 10 ++++++++++
6 files changed, 91 insertions(+), 2 deletions(-)
diff --git a/sqladmin/fields.py b/sqladmin/fields.py
index fcea7d90..4347638f 100644
--- a/sqladmin/fields.py
+++ b/sqladmin/fields.py
@@ -25,6 +25,10 @@
"QuerySelectMultipleField",
"SelectField",
"Select2TagsField",
+ "BooleanField",
+ "FileField",
+ "UuidField",
+ "TextAreaField",
]
@@ -439,3 +443,12 @@ def process_data(self, value: str | UUID | None) -> None:
self.data = None
else:
self.data = None
+
+
+class TextAreaField(fields.StringField):
+ """
+ This field represents an HTML `textarea` and can be used to take
+ multi-line input.
+ """
+
+ widget = sqladmin_widgets.TextAreaWidget()
diff --git a/sqladmin/forms.py b/sqladmin/forms.py
index 302037a0..53b144b5 100644
--- a/sqladmin/forms.py
+++ b/sqladmin/forms.py
@@ -29,7 +29,6 @@
Form,
IntegerField,
StringField,
- TextAreaField,
TimeField,
validators,
)
@@ -57,6 +56,7 @@
QuerySelectMultipleField,
Select2TagsField,
SelectField,
+ TextAreaField,
UuidField,
)
from sqladmin.helpers import (
diff --git a/sqladmin/statics/css/main.css b/sqladmin/statics/css/main.css
index 49cad898..4903eeb2 100644
--- a/sqladmin/statics/css/main.css
+++ b/sqladmin/statics/css/main.css
@@ -8,3 +8,12 @@
margin-right: 5px;
}
+.autoresize-textarea {
+ min-height: 25px;
+ overflow-y: hidden;
+ height: 25px;
+}
+
+.chars-count-label.warning {
+ color: red;
+}
diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js
index 34e59178..d71f43b0 100644
--- a/sqladmin/statics/js/main.js
+++ b/sqladmin/statics/js/main.js
@@ -164,3 +164,33 @@ $(':input[data-role="select2-tags"]').each(function () {
$(this).append(option).trigger('change');
}
});
+
+// Automatically resize textarea on input events.
+$('.autoresize-textarea').each(function() {
+ const $textarea = $(this);
+ const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0;
+
+ const $charsCountLabel = $textarea.next('.chars-count-label');
+ const $charsCountElement = $charsCountLabel.length ? $charsCountLabel : $charsCountLabel.first();
+
+ if (!$charsCountElement.length) return;
+
+ const updateTextareaHeight = () => {
+ $textarea.css('height', 'auto');
+ $textarea.css('height', $textarea[0].scrollHeight + 'px');
+ };
+
+ const updateCharsCount = () => {
+ const $currentLength = $textarea.val().length;
+ $charsCountElement.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`);
+ $charsCountElement.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength);
+ };
+
+ $textarea.on('input propertychange', function() {
+ updateTextareaHeight();
+ updateCharsCount();
+ });
+
+ updateTextareaHeight(); // Resize on start
+ updateCharsCount(); // Resize on start
+});
diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py
index f5c10f3b..3402cf72 100644
--- a/sqladmin/widgets.py
+++ b/sqladmin/widgets.py
@@ -3,7 +3,7 @@
import json
from typing import TYPE_CHECKING, Any
-from markupsafe import Markup
+from markupsafe import Markup, escape
from wtforms import Field, SelectFieldBase, widgets
from wtforms.widgets import html_params
@@ -122,3 +122,30 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup:
return Markup(
'
%s
'
) % Markup.escape(super().__call__(field, **kwargs))
+
+
+class TextAreaWidget(widgets.Input):
+ """
+ Render a textarea that automatically resizes on input.
+ """
+
+ validation_attrs = ["required", "disabled", "readonly", "maxlength", "minlength"]
+
+ def __call__(self, field: Field, **kwargs: Any) -> Markup:
+ kwargs.setdefault("id", field.id)
+ flags = getattr(field, "flags", {})
+ for k in dir(flags):
+ if k in self.validation_attrs and k not in kwargs:
+ kwargs[k] = getattr(flags, k)
+
+ class_ = " ".join(filter(None, (kwargs.get("class"), "autoresize-textarea")))
+ kwargs["class"] = class_
+ kwargs["rows"] = "1"
+ if hasattr(field, "_value") and callable(field._value):
+ kwargs["value"] = field._value()
+
+ return Markup(
+ ""
+ % (html_params(name=field.name, **kwargs), escape(kwargs.get("value", "")))
+ + ''
+ ) # nosec: markupsafe_markup_xss
diff --git a/tests/test_fields.py b/tests/test_fields.py
index bc368a57..c92296c7 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -16,6 +16,7 @@
QuerySelectMultipleField,
Select2TagsField,
SelectField,
+ TextAreaField,
UuidField,
)
from tests.common import DummyData
@@ -202,3 +203,12 @@ class F(Form):
form = F(DummyData(uuid=["00000000-0000-000000000001"]))
assert form.validate() is False
+
+
+def test_textarea_field() -> None:
+ class F(Form):
+ text = TextAreaField()
+
+ form = F()
+ assert "autoresize-textarea" in form.text()
+ assert "chars-count-label" in form.text()
From 66f30877f702d70a3322adc885348fbcaa2e764f Mon Sep 17 00:00:00 2001
From: maxim-f1 <“maxconal228@gmail.com”>
Date: Fri, 10 Jul 2026 18:01:24 +0300
Subject: [PATCH 2/4] Add `show_chars_count` field to disable chars count
---
sqladmin/fields.py | 22 ++++++++++++++++++++++
sqladmin/statics/js/main.js | 6 ++++--
sqladmin/widgets.py | 13 +++++++++----
3 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/sqladmin/fields.py b/sqladmin/fields.py
index e987bb7a..d0dee9f2 100644
--- a/sqladmin/fields.py
+++ b/sqladmin/fields.py
@@ -449,6 +449,28 @@ class TextAreaField(fields.StringField):
"""
This field represents an HTML `textarea` and can be used to take
multi-line input.
+
+ To disable the display of the number of characters,
+ you need to pass the value show_chars_count = False to the field arguments.
+
+ ```python
+ form_args = {
+ 'field_name': {
+ 'show_chars_count': False
+ }
+ }
+ ```
"""
+ def __init__(
+ self,
+ label: str | None = None,
+ validators: list | None = None,
+ *,
+ show_chars_count: bool = True,
+ **kwargs: Any,
+ ):
+ super().__init__(label, validators, **kwargs)
+ self.show_chars_count = show_chars_count
+
widget = sqladmin_widgets.TextAreaWidget()
diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js
index 03e1695d..602b5577 100644
--- a/sqladmin/statics/js/main.js
+++ b/sqladmin/statics/js/main.js
@@ -210,19 +210,21 @@ $(':input[data-role="select2-tags"]').each(function () {
// Automatically resize textarea on input events.
$('.autoresize-textarea').each(function() {
const $textarea = $(this);
+ if (!$textarea) return;
+
const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0;
const $charsCountLabel = $textarea.next('.chars-count-label');
const $charsCountElement = $charsCountLabel.length ? $charsCountLabel : $charsCountLabel.first();
- if (!$charsCountElement.length) return;
-
const updateTextareaHeight = () => {
$textarea.css('height', 'auto');
$textarea.css('height', $textarea[0].scrollHeight + 'px');
};
const updateCharsCount = () => {
+ if (!$charsCountElement.length || !$charsCountLabel) return;
+
const $currentLength = $textarea.val().length;
$charsCountElement.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`);
$charsCountElement.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength);
diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py
index 4db2398b..15726049 100644
--- a/sqladmin/widgets.py
+++ b/sqladmin/widgets.py
@@ -138,16 +138,21 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup:
flags = getattr(field, "flags", {})
for k in dir(flags):
if k in self.validation_attrs and k not in kwargs:
- kwargs.setdefault(k, getattr(flags, k))
+ kwargs[k] = getattr(flags, k)
class_ = " ".join(filter(None, (kwargs.get("class"), "autoresize-textarea")))
- kwargs.setdefault("class", class_)
+ kwargs["class"] = class_
kwargs.setdefault("rows", "1")
if hasattr(field, "_value") and callable(field._value):
- kwargs.setdefault("value", field._value())
+ kwargs["value"] = field._value()
+
+ if getattr(field, "show_chars_count", True) is True:
+ chars_count = ''
+ else:
+ chars_count = ""
return Markup(
""
% (html_params(name=field.name, **kwargs), escape(kwargs.get("value", "")))
- + ''
+ + chars_count
) # nosec: markupsafe_markup_xss
From b6445fe7b28803e9ea83ce830def85d1d5bbd9e3 Mon Sep 17 00:00:00 2001
From: maxim-f1 <“maxconal228@gmail.com”>
Date: Fri, 10 Jul 2026 18:32:03 +0300
Subject: [PATCH 3/4] Added `enable_autoresize` field to control resizing
behavior. Now the parameters are independent of each other
---
sqladmin/fields.py | 12 ++++++++++++
sqladmin/statics/js/main.js | 37 ++++++++++++++++++++++++-------------
sqladmin/widgets.py | 8 ++++++--
3 files changed, 42 insertions(+), 15 deletions(-)
diff --git a/sqladmin/fields.py b/sqladmin/fields.py
index d0dee9f2..9c34cbde 100644
--- a/sqladmin/fields.py
+++ b/sqladmin/fields.py
@@ -450,6 +450,16 @@ class TextAreaField(fields.StringField):
This field represents an HTML `textarea` and can be used to take
multi-line input.
+ To disable automatic updating of the input size,
+ you need to pass enable_autoresize = False to the field arguments.
+ ```python
+ form_args = {
+ 'field_name': {
+ 'enable_autoresize': False
+ }
+ }
+ ```
+
To disable the display of the number of characters,
you need to pass the value show_chars_count = False to the field arguments.
@@ -467,10 +477,12 @@ def __init__(
label: str | None = None,
validators: list | None = None,
*,
+ enable_autoresize: bool = True,
show_chars_count: bool = True,
**kwargs: Any,
):
super().__init__(label, validators, **kwargs)
+ self.enable_autoresize = enable_autoresize
self.show_chars_count = show_chars_count
widget = sqladmin_widgets.TextAreaWidget()
diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js
index 602b5577..0052e21a 100644
--- a/sqladmin/statics/js/main.js
+++ b/sqladmin/statics/js/main.js
@@ -212,29 +212,40 @@ $('.autoresize-textarea').each(function() {
const $textarea = $(this);
if (!$textarea) return;
- const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0;
-
- const $charsCountLabel = $textarea.next('.chars-count-label');
- const $charsCountElement = $charsCountLabel.length ? $charsCountLabel : $charsCountLabel.first();
-
const updateTextareaHeight = () => {
$textarea.css('height', 'auto');
$textarea.css('height', $textarea[0].scrollHeight + 'px');
};
- const updateCharsCount = () => {
- if (!$charsCountElement.length || !$charsCountLabel) return;
+ $textarea.on('input propertychange', function() {
+ updateTextareaHeight();
+ });
+
+ updateTextareaHeight(); // Resize on start
+});
+
+
+// Shows the number of characters in a textarea..
+$('.chars-count-label').each(function() {
+ const $charsCountLabel = $(this);
+ if (!$charsCountLabel) return;
+
+ const $textarea = $charsCountLabel.prev('textarea');
+ if (!$textarea.length) return;
+
+ const $maxLength = parseInt($textarea.attr('maxlength'), 10) || 0;
+
+ const updateCharsCountLabel = () => {
+ if (!$charsCountLabel.length) return;
const $currentLength = $textarea.val().length;
- $charsCountElement.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`);
- $charsCountElement.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength);
+ $charsCountLabel.text(`Number of characters: ${$currentLength}${$maxLength ? ` / ${$maxLength}` : ''}`);
+ $charsCountLabel.toggleClass('warning', $maxLength > 0 && $currentLength >= $maxLength);
};
$textarea.on('input propertychange', function() {
- updateTextareaHeight();
- updateCharsCount();
+ updateCharsCountLabel();
});
- updateTextareaHeight(); // Resize on start
- updateCharsCount(); // Resize on start
+ updateCharsCountLabel(); // Show on start
});
diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py
index 15726049..87a3be6d 100644
--- a/sqladmin/widgets.py
+++ b/sqladmin/widgets.py
@@ -140,9 +140,13 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup:
if k in self.validation_attrs and k not in kwargs:
kwargs[k] = getattr(flags, k)
- class_ = " ".join(filter(None, (kwargs.get("class"), "autoresize-textarea")))
+ class_ = kwargs.get("class")
+ if getattr(field, "enable_autoresize", True) is True:
+ class_ = " ".join(filter(None, (class_, "autoresize-textarea")))
+ kwargs.setdefault("rows", "1")
+
kwargs["class"] = class_
- kwargs.setdefault("rows", "1")
+
if hasattr(field, "_value") and callable(field._value):
kwargs["value"] = field._value()
From c6771171ed865df8d0705d6b8d2898ba6ccffc1f Mon Sep 17 00:00:00 2001
From: maxim-f1 <“maxconal228@gmail.com”>
Date: Mon, 13 Jul 2026 11:50:24 +0300
Subject: [PATCH 4/4] Fix value
---
sqladmin/statics/js/main.js | 2 +-
sqladmin/widgets.py | 9 ++++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/sqladmin/statics/js/main.js b/sqladmin/statics/js/main.js
index 0052e21a..e93cbb13 100644
--- a/sqladmin/statics/js/main.js
+++ b/sqladmin/statics/js/main.js
@@ -225,7 +225,7 @@ $('.autoresize-textarea').each(function() {
});
-// Shows the number of characters in a textarea..
+// Displays the number of characters in the text field.
$('.chars-count-label').each(function() {
const $charsCountLabel = $(this);
if (!$charsCountLabel) return;
diff --git a/sqladmin/widgets.py b/sqladmin/widgets.py
index 87a3be6d..8741b750 100644
--- a/sqladmin/widgets.py
+++ b/sqladmin/widgets.py
@@ -126,7 +126,7 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup:
)
-class TextAreaWidget(widgets.Input):
+class TextAreaWidget(widgets.TextArea):
"""
Render a textarea that automatically resizes on input.
"""
@@ -145,7 +145,8 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup:
class_ = " ".join(filter(None, (class_, "autoresize-textarea")))
kwargs.setdefault("rows", "1")
- kwargs["class"] = class_
+ if class_:
+ kwargs["class"] = class_
if hasattr(field, "_value") and callable(field._value):
kwargs["value"] = field._value()
@@ -155,8 +156,10 @@ def __call__(self, field: Field, **kwargs: Any) -> Markup:
else:
chars_count = ""
+ value = kwargs.pop("value", "")
+
return Markup(
""
- % (html_params(name=field.name, **kwargs), escape(kwargs.get("value", "")))
+ % (html_params(name=field.name, **kwargs), escape(value))
+ chars_count
) # nosec: markupsafe_markup_xss