Skip to main content

Reusable Django CRUD views, forms, and template fragments with HTMX, Alpine.js, and Tailwind CSS

Project description

django-crispy-crud

PyPI version License: MIT

Reusable Django CRUD views, forms, and template fragments with HTMX, Alpine.js, and Tailwind CSS.

Installation

With uv:

uv add django-crispy-crud

With pip:

pip install django-crispy-crud

For audit log support (optional):

uv add "django-crispy-crud[audit]"
# or
pip install "django-crispy-crud[audit]"

Setup

Add crispy_crud to your INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "crispy_forms",
    "crispy_tailwind",
    "django_htmx",
    "django_filters",
    "crispy_crud",
]

Add the required middleware:

MIDDLEWARE = [
    # ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django_htmx.middleware.HtmxMiddleware",
]

Ensure the messages context processor is included in your template settings:

TEMPLATES = [
    {
        # ...
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.messages.context_processors.messages",
                # ...
            ],
        },
    },
]

Set crispy forms to use Tailwind:

CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
CRISPY_TEMPLATE_PACK = "tailwind"

Your base template must load Alpine.js and HTMX (not bundled):

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/htmx.org@2.0.0"></script>

Component CSS

The package templates use custom CSS classes (btn-primary, btn-secondary, btn-important, btn-disabled, form-link, readonly-form-field, toggle). A reference stylesheet is provided at static/crispy_crud/css/components.css using Tailwind @apply directives.

Add it to your Tailwind CSS input file:

@import "../../static/crispy_crud/css/components.css";

Or copy the definitions into your own CSS. See the test app's base.html for a working example.

Usage

View mixins

from django.views.generic import CreateView, UpdateView
from django_filters.views import FilterView
from crispy_crud.views import (
    BaseModelAppMixin, CrispyFormArgsMixin,
    FilterFormMixin, HtmxFilterListMixin,
)

class WidgetListView(BaseModelAppMixin, FilterFormMixin, HtmxFilterListMixin, FilterView):
    model = Widget
    filterset_class = WidgetFilter
    template_name = "myapp/widget_list.html"
    partial_template_name = "myapp/fragments/widget_table.html"
    section = "widget-list"
    create_view_name = "widget-create"
    filter_form_class = WidgetFilterForm

class WidgetUpdateView(BaseModelAppMixin, CrispyFormArgsMixin, UpdateView):
    model = Widget
    form_class = WidgetForm
    template_name = "myapp/widget_detail.html"
    section = "widgets"
    delete_view_name = "widget-delete"
    success_url = reverse_lazy("widget-list")

Form helpers

from django import forms
from crispy_crud.forms import CrispyFormMixin

class WidgetForm(CrispyFormMixin, forms.ModelForm):
    cancel_action = "widget-list"

    class Meta:
        model = Widget
        fields = ["name", "description", "is_active"]

CrispyFormMixin provides a crispy FormHelper, cancel link, and Alpine.js dirty-state submit button out of the box. You need to set the layout in your form's __init__ and include self.button_holder at the end:

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.helper.layout = Layout(
        Field("name"),
        Field("description"),
        Field("is_active"),
        self.button_holder,
    )

Filter forms

Create a filter form with a crispy helper using BaseFilterFormHelper:

from crispy_crud.forms import BaseFilterFormHelper
from crispy_forms.layout import Field, Layout

class WidgetFilterFormHelper(BaseFilterFormHelper):
    q_field_placeholder = "Search widgets..."

    def __init__(self):
        super().__init__()
        self.form_class = self.form_css
        self.layout = Layout(
            self.q_field,
            Field("is_active", wrapper_class="w-40"),
            self.button_holder,
        )

class WidgetFilterForm(forms.Form):
    q = forms.CharField(required=False)
    is_active = forms.BooleanField(required=False)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = WidgetFilterFormHelper()

Set filter_form_class on your list view, and include the HTMX filter form fragment in your list template:

{% include 'crispy_crud/fragments/forms/hx_filter_form.html' with view_name="widget-list" %}

This renders a form that auto-submits via HTMX on input changes -- the search field fires after a 500ms debounce, and select/checkbox fields fire on change. The filter_form_fields template tag automatically generates the correct HTMX trigger selectors for your filter fields.

You also need a django-filter FilterSet on your view:

import django_filters

class WidgetFilter(django_filters.FilterSet):
    q = django_filters.CharFilter(field_name="name", lookup_expr="icontains")

    class Meta:
        model = Widget
        fields = ["q", "is_active"]

Template fragments

The package provides reusable template fragments under crispy_crud/fragments/. Include them in your own templates:

{% include 'crispy_crud/fragments/common/messages.html' %}
{% include 'crispy_crud/fragments/common/page_heading.html' %}
{% include 'crispy_crud/fragments/table/table_heading.html' with text="Name" first_column=True %}
{% include 'crispy_crud/fragments/table/table_cell.html' with text=widget.name first_column=True %}
{% include 'crispy_crud/fragments/table/hx_pagination.html' with page=page_obj %}
{% include 'crispy_crud/fragments/details/delete_action.html' %}
{% include 'crispy_crud/fragments/loaders/loading_spinner.html' %}

Static files

Include the Alpine.js components for form state tracking and pagination:

<script src="{% static 'crispy_crud/js/formState.js' %}"></script>
<script src="{% static 'crispy_crud/js/pagination.js' %}"></script>

Configuration

All settings are optional and have sensible defaults:

Setting Default Purpose
CRISPY_CRUD_PAGINATE_BY 25 Default page size
CRISPY_CRUD_PAGE_SIZE_OPTIONS [10, 25, 50, 100] Page-size pills shown in pagination
CRISPY_CRUD_AUDIT_PAGE_SIZE 25 Rows per page in audit history table

Running the test app

The test app in tests/testapp/ is a working example you can clone as a starting point. To run it locally:

git clone https://github.com/howieweiner/django-crispy-crud.git
cd django-crispy-crud
uv run python manage.py migrate
uv run python manage.py runserver

Then visit http://localhost:8000/widgets/ to see the list view with filtering, pagination, create/update forms, and delete with confirmation modal.

Development

uv run pytest                    # run tests
uv run ruff check src/ tests/    # lint
uv run ruff format src/ tests/   # format

What's included

View mixins (crispy_crud.views)

  • PaginationMixin -- page size from query param, cookie, or config
  • HtmxPaginationMixin -- extends PaginationMixin with HTMX URL push
  • CrispyFormArgsMixin -- injects request into form kwargs
  • BaseAppMixin -- section/heading context
  • BaseModelAppMixin -- auto-generated headings, create/delete actions, related-object protection
  • HtmxFilterListMixin -- switches to partial template on HTMX requests
  • FilterFormMixin -- populates filter form in context
  • AuditLogMixin -- paginated audit history (requires django-auditlog)

Form helpers (crispy_crud.forms)

  • CrispyFormMixin -- crispy FormHelper with cancel link and Alpine.js dirty-state
  • AlpineSubmit -- submit button with Alpine.js attributes
  • BaseFilterFormHelper -- filter form helper with search field and reset link

Model mixins (crispy_crud.models)

  • AuditOnUpdateOnly -- suppresses auditlog on creation, only logs updates

Template fragments (crispy_crud/fragments/)

  • common/ -- messages, alerts, page heading
  • table/ -- pagination, table heading/cell, OOB add button
  • details/ -- delete action with confirmation modal, audit history table
  • forms/ -- filter form, field errors, submit/cancel, read-only fields
  • loaders/ -- HTMX loading spinner and busy indicator

Template tags (crispy_crud_tags)

  • filter_form_fields -- generates HTMX trigger selectors for filter form fields

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_crispy_crud-1.0.0.tar.gz (26.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_crispy_crud-1.0.0-py3-none-any.whl (27.4 kB view details)

Uploaded Python 3

File details

Details for the file django_crispy_crud-1.0.0.tar.gz.

File metadata

  • Download URL: django_crispy_crud-1.0.0.tar.gz
  • Upload date:
  • Size: 26.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_crispy_crud-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d25b9694d0e6caefdbd2f3d6c10a15c409926ddf65799bafbabd57e0ceb29e14
MD5 0b0c001549dac8732ecc253dbdbf8478
BLAKE2b-256 47f2822c26641089b298b5ce6c71c1ada675ed6903bba0f54e1fd9d19c201909

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_crispy_crud-1.0.0.tar.gz:

Publisher: publish.yml on howieweiner/django-crispy-crud

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_crispy_crud-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_crispy_crud-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5a73cf3b10f6e894252039b2bd70399bde8911789377010c7435537754278f9e
MD5 368874d6de53fc62ac92bd58f3204d47
BLAKE2b-256 4ade70d504cad4a3b5bfe6108e414325ad7d769b6ce0ad26a76fdad2f1b022d7

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_crispy_crud-1.0.0-py3-none-any.whl:

Publisher: publish.yml on howieweiner/django-crispy-crud

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page