diff --git a/CHANGELOG.md b/CHANGELOG.md index e42041c..e1eda5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [0.5.1](https://github.com/dldevinc/paper-forms/tree/v0.5.1) - 2024-01-03 + +- Now you can set the `error_css_class` and `required_css_class` using Composer. + ## [0.5.0](https://github.com/dldevinc/paper-forms/tree/v0.5.0) - 2024-01-02 ### ⚠ BREAKING CHANGES diff --git a/README.md b/README.md index c8811fe..89b3a7b 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,13 @@ class ExampleForm(forms.Form): Here, the `Composer` class provides labels and help text for the "name" and "age" fields, offering clear instructions to users interacting with your forms. +Additionally, developers can enhance the customization of the form's appearance +by utilizing the `error_css_class` and `required_css_class` attributes +within the `Composer` class. These attributes allow you to define specific CSS classes +for handling errors and indicating required fields, respectively. Notably, any values +set for these attributes in the `Composer` class take precedence over those specified +at the form level. + ### Specifying Custom Template Names When using `paper-forms`, you have the flexibility to create custom templates for diff --git a/paper_forms/__init__.py b/paper_forms/__init__.py index 3d18726..dd9b22c 100644 --- a/paper_forms/__init__.py +++ b/paper_forms/__init__.py @@ -1 +1 @@ -__version__ = "0.5.0" +__version__ = "0.5.1" diff --git a/paper_forms/boundfield.py b/paper_forms/boundfield.py index 7ceaed3..47dd9d2 100644 --- a/paper_forms/boundfield.py +++ b/paper_forms/boundfield.py @@ -100,6 +100,30 @@ def get_context( return context + def css_classes(self, extra_classes=None): + if hasattr(extra_classes, "split"): + extra_classes = extra_classes.split() + + # Remove duplicates while maintaining the order. + seen = set() + extra_classes = [ + class_name + for class_name in (extra_classes or []) + if class_name not in seen and not seen.add(class_name) + ] + + if self.errors: + if self.composer.error_css_class: + extra_classes.append(self.composer.error_css_class) + elif hasattr(self.form, "error_css_class"): + extra_classes.append(self.form.error_css_class) + if self.field.required: + if self.composer.required_css_class: + extra_classes.append(self.composer.required_css_class) + elif hasattr(self.form, "required_css_class"): + extra_classes.append(self.form.required_css_class) + return " ".join(extra_classes) + @cached_property def widget(self) -> Widget: widget = self.composer.get_widget(self.name) diff --git a/paper_forms/composer.py b/paper_forms/composer.py index 1e19519..90bc0d3 100644 --- a/paper_forms/composer.py +++ b/paper_forms/composer.py @@ -1,5 +1,5 @@ import copy -from typing import Any, Optional +from typing import Any, Optional, ClassVar from django.forms import BaseForm from django.forms.renderers import BaseRenderer @@ -23,11 +23,13 @@ def __call__(cls, *args, **kwargs): class BaseComposer(metaclass=SingletonMeta): renderer = None - widgets: dict[str, Any] = None - labels: dict[str, str] = None - help_texts: dict[str, str] = None - css_classes: dict[str, str] = None - template_names: dict[str, str] = None + error_css_class: ClassVar[str] = None + required_css_class: ClassVar[str] = None + widgets: ClassVar[dict[str, Any]] = None + labels: ClassVar[dict[str, str]] = None + help_texts: ClassVar[dict[str, str]] = None + css_classes: ClassVar[dict[str, str]] = None + template_names: ClassVar[dict[str, str]] = None def get_renderer(self, form: BaseForm) -> BaseRenderer: renderer = self.renderer or form.default_renderer or conf.DEFAULT_FORM_RENDERER diff --git a/tests/tests/test_boundfield.py b/tests/tests/test_boundfield.py index 1dcb735..a7d8d0f 100644 --- a/tests/tests/test_boundfield.py +++ b/tests/tests/test_boundfield.py @@ -267,3 +267,41 @@ class MyForm(Form): "css_classes": "", "errors": [], } + + +class TestCssClasses: + def test_get_values_from_composer(self): + class MyForm(Form): + error_css_class = "This will be overriden" + required_css_class = "This will be overriden" + + name = CharField( + max_length=64, + ) + + class Composer(BaseComposer): + error_css_class = "invalid" + required_css_class = "required" + + # Bound form + form = MyForm({}) + + bf = get_boundfield(form, "name", Composer()) + css_classes = bf.css_classes() + assert css_classes == "invalid required" + + def test_get_values_from_form(self): + class MyForm(Form): + error_css_class = "invalid" + required_css_class = "required" + + name = CharField( + max_length=64, + ) + + # Bound form + form = MyForm({}) + + bf = get_boundfield(form, "name", BaseComposer()) + css_classes = bf.css_classes() + assert css_classes == "invalid required"