返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
DjangoStarter

Use django-crispy-forms to improve form UI

Heh, another fluff piece on beautifying Django forms. I've already encapsulated Tailwind styles myself and still find it insufficient, so I insist on tinkering with ancient relics like django-crispy-forms—Bootstrap is already outdated, hello! Though crispy-tailwind is passable, at least a bit more elegant than writing templates manually. Anyway, it'll all get rewritten in React eventually, so why bother wasting time?

曦远 (DealiAxy)约 18 分钟
目录11 个章节

Foreword

Alright, let's continue with this series of articles derived from the middle-end project.

Today, I'd like to introduce a library that can beautify the UI.

Django development emphasizes speed—"all martial arts in the world, only speed is unbreakable."

The forms feature is naturally essential; it comes with built-in form UI generation and validation, making it very convenient for building demos.

I previously encapsulated a set of forms behaviors and styles in the DjangoStarter framework, located in src/django_starter/contrib/forms.

That set is already quite good and uses TailwindCSS for styling, making it look nice.

However, while developing the middle-end project, I discovered the django-crispy-forms library, which offers more flexibility for beautifying form UI.

Implementation in DjangoStarter

Let's first examine the implementation in the DjangoStarter framework.

 forms
 ├─ widgets
 │  ├─ __init__.py
 │  ├─ multiple_file.py
 │  └─ flowbite_date_picker.py
 ├─ templates
 │  └─ django_starter
 │     └─ forms
 │        ├─ widgets
 │        └─ form_template.html
 ├─ __init__.py
 ├─ widget_classes.py
 ├─ mixins.py
 └─ base.py

template

src/django_starter/contrib/forms/templates/django_starter/forms/form_template.html

{% for field in form %}
    <div>
        <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
               for="{{ field.id_for_label }}">{{ field.label }}</label>
        {{ field }}
        <div class="mt-2 text-sm text-gray-500 dark:text-gray-200">
            {{ field.help_text }}
        </div>
        <div class="mt-2 text-sm text-red-600 dark:text-red-500">
            {{ field.errors }}
        </div>
    </div>
{% endfor %}

Widgets

src/django_starter/contrib/forms/widgets/flowbite_date_picker.py

from django import forms

class FlowbiteDatePickerWidget(forms.DateInput):
    template_name = 'django_starter/forms/widgets/flowbite_date_picker.html'

    def __init__(self, attrs=None, custom_class=''):
        final_attrs = {'class': custom_class}
        if attrs:
            final_attrs.update(attrs)
        super(FlowbiteDatePickerWidget, self).__init__(attrs=final_attrs)

src/django_starter/contrib/forms/widgets/multiple_file.py

from django import forms


class MultipleFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class MultipleFileField(forms.FileField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("widget", MultipleFileInput())
        super().__init__(*args, **kwargs)

    def clean(self, data, initial=None):
        single_file_clean = super().clean
        if isinstance(data, (list, tuple)):
            result = [single_file_clean(d, initial) for d in data]
        else:
            result = [single_file_clean(data, initial)]
        return result

Forms Code

src/django_starter/contrib/forms/mixins.py

from django import forms
from .widget_classes import *


class BaseFormMixin:
    """A generic mixin class for providing form styling"""

    widget_classes = {
        forms.TextInput: TEXT_INPUT_CLASS,
        forms.Textarea: TEXT_AREA_CLASS,
        forms.EmailInput: TEXT_INPUT_CLASS,
        forms.PasswordInput: PASSWORD_INPUT_CLASS,
        forms.Select: SELECT_CLASS,
        forms.DateInput: DATE_INPUT_CLASS,
        forms.NumberInput: NUMBER_INPUT_CLASS,
    }

    def apply_widget_classes(self):
        """Applies styling to form fields based on the widget type"""
        for field_name, field in self.fields.items():
            widget_class = self.widget_classes.get(type(field.widget))
            if widget_class:
                field.widget.attrs.update({'class': widget_class})

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.apply_widget_classes()

The code for widget_classes.py is not included here.

It uses Tailwind CSS styling.

How does crispy-forms work?

After installation, you can use it with just a simple configuration.

Usage looks something like this:

{% load crispy_forms_tags %}

<div class="sm:col-span-6">
    {{ form.name|as_crispy_field }}
</div>

<div class="sm:col-span-3">
    {{ form.dvr_brand|as_crispy_field }}
</div>

<div class="sm:col-span-3">
    {{ form.dvr_model|as_crispy_field }}
</div>

<div class="sm:col-span-3">
    {{ form.dvr_ip|as_crispy_field }}
</div>

<div class="sm:col-span-3">
    {{ form.status|as_crispy_field }}
</div>

Installation

Install dependencies

pdm install django-crispy-forms

Then add to

INSTALLED_APPS = (
    ...
    'crispy_forms',
)

Template packs

Then you need to install the Template packs; otherwise, it's just an empty shell.

The officially supported ones are only the Bootstrap series, which is a bit outdated.

Fortunately, the community also provides many options. Here I'm focusing only on the Tailwind CSS library, named crispy-tailwind.

pdm add crispy-tailwind

There are many other UI libraries. For those who are interested, you can find them on the official website: https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs

This one also needs to be added.

INSTALLED_APPS = (
    ...
    'crispy_forms',
    'crispy_tailwind',
)

Configuration

Modify the default Template Packs in the configuration file

src/config/settings/components/crispy_forms.py

CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
CRISPY_TEMPLATE_PACK = "tailwind"

Usage

The simplest way to use this is by only modifying the template file.

{% load crispy_forms_tags %}

<form method="post" class="my-class">
    {{ my_formset|crispy }}
</form>

Currently, the mid-end project only uses this method.

Combined with django-select2, it can implement a dropdown search box.

However, the styling is difficult to modify; I spent some time trying but couldn't get it right, so I decided to leave it for now. After all, this is just a quickly implemented DEMO.

If there's a need later, we can rewrite the page using React.

Summary

I found that Django-related technology is still relatively niche.

Generally, writing this kind of article doesn't attract many readers.

I've been working with Django for several years. I've studied the framework's source code, worked with scaffolding tools, and it's become very comfortable to use.

Although there are many Python web frameworks now, all projects eventually end up looking like Django 🤣

That's about it. For Python projects, I will continue to stick with Django because it is truly convenient and easy to use.

Of course, I'm not opposed to trying new things. For example, recently there's Litestar which claims to dethrone FastAPI—it seems quite interesting, and I might give it a try when I have time.