django-htmx-patterns

repository·master·Indexed 21 days ago

https://github.com/spookylukey/django-htmx-patterns

A collection of patterns for integrating Django forms with HTMX to create interactive user experiences. It covers techniques for inline form validation, AJAX-style submissions, and using the @for_htmx decorator to render specific template blocks. The documentation also provides guidance on using Function-Based Views (FBVs) over Class-Based Views (CBVs), installing HTMX via CDN or static files, and implementing dual-mode testing using django-functest with WebTest and Selenium.

Tokens
13K
Snippets
35
Records
42
Agent score
74%

What's inside django-htmx-patterns

  1. Why use Function-Based Views (FBVs) for HTMX patterns

    master

    When developing HTMX-enabled views, it is strongly recommended to use Function-Based Views (FBVs) instead of Class-Based Views (CBVs).

    FBVs are preferred because they allow you to:

    1. See the complete control flow: HTMX often requires conditional logic to decide whether to return a full page or a partial HTML fragment. FBVs make this flow explicit.
    2. Re-arrange the control flow: It is significantly easier to manipulate the logic required for HTMX responses within a function than within the complex inheritance structures of CBVs.

    Using CBVs can hinder your ability to identify, extract, and implement clean HTMX patterns.

  2. Enable WebTest compatibility for HTMX-enhanced views

    master

    When building applications with HTMX, you can maintain the ability to run fast WebTest-based tests by following this development pattern:

    1. Initial Implementation: Write your Django views without any hx- attributes and write corresponding tests. This ensures the core functionality works as standard HTTP requests.
    2. HTMX Enhancement: Add hx- attributes and necessary template/view modifications to enable partial page loads.
    3. Conditional Branching: If a view needs to behave differently for HTMX requests (e.g., returning a partial vs. a full page redirect), use is_htmx(request) to branch the logic. This allows the view to serve a full page for standard requests (testable via WebTest) and a partial for HTMX requests.

    Example Pattern: In your view, check is_htmx(request) to decide whether to perform a full redirect or a partial response. This ensures that even if JavaScript is ignored, the basic server-side functionality remains accessible and testable.

  3. Requirements for robust real-time form validation

    master

    When implementing real-time field validation with HTMX, ensure your implementation meets these UX and technical requirements to avoid breaking the user experience:

    • Data Integrity: Never overwrite user input with an earlier or blank state during an HTMX swap.
    • Non-Intrusive Feedback: Do not interrupt the user or show errors while they are actively typing. If providing feedback during input, ensure it does not change the entered data or steal focus.
    • Timing of Feedback: Trigger validation as soon as the user is "finished" with a field (e.g., toggling a checkbox or blurring an input).
    • Accessibility & Navigation: Do not break keyboard navigation (Tab), focus management, or standard control manipulation (e.g., Space for checkboxes, Arrow keys for selects).
    • Label Interaction: Handle cases where clicking a <label> changes a control (like a checkbox) without necessarily moving focus.
    • Platform Compatibility: Ensure compatibility with mobile keyboard behaviors and varying browser DOM event implementations.
  4. Use hx-confirm for simple confirmation prompts

    master
    If your requirement is a simple user confirmation (e.g., "Are you sure you want to delete this?") rather than a complex form, do not build a full modal dialog. Instead, use the built-in HTMX hx-confirm attribute or listen for the htmx:confirm event.
  5. Apply Bulma classes to widgets using Sass @extend

    master

    Instead of manually adding classes like class="input" to every HTML element in your templates, you can use the Sass @extend rule. This allows you to target specific input types within a container and automatically apply Bulma's styling rules.

    @import "../vendor/bulma.scss";
    
    .field-body {
        input[type=text], input[type=email], input[type=password], input[type=date] {
            @extend .input;
        }
        input[type=checkbox] {
            @extend .checkbox;
        }
    }
  6. Achieve Locality of Behaviour (LoB) with template-driven block selection

    master

    To prevent the view from needing to know which block to render, you can move the routing logic into the template itself. This makes the template self-contained and easier to understand.

    How to implement:

    1. In the template, use hx-vals to pass the desired block name as a parameter (e.g., hx-vals='{"use_block": "my-block-name"}').
    2. In the Django view, apply the @for_htmx decorator with use_block_from_params=True.

    This pattern allows the template to control its own partial rendering, which is particularly useful for complex pages with multiple HTMX targets.

    <!-- In the template -->
    <a
      href="#"
      hx-get="?page={{ page_obj.next_page_number }}"
      hx-vals='{"use_block": "page-and-paging-controls"}'
      hx-target="#paging-area"
      hx-swap="outerHTML"
    >
      Load more
    </a>
    
    <!-- In the view -->
    @for_htmx(use_block_from_params=True)
    def my_view(request):
        ...
  7. Important: Use hx-preserve to prevent data loss

    master

    When using HTMX to swap form parts, it is critical to use the hx-preserve attribute on input widgets.

    Why? If a server response arrives after a user has already started typing new data into a field, the incoming HTML from the server might overwrite the user's current (unsaved) input with the old value.

    How? Use the attr filter from django-widget-tweaks to add hx-preserve:true to your field widgets in the template.

    {{ field|add_class:error_class|attr:"hx-preserve:true" }}
  8. Set hx-headers via JavaScript for restricted templates

    master

    If you cannot modify the <body> tag directly (e.g., when using a base template you cannot edit), you can inject the hx-headers attribute using JavaScript within an overridden template. This ensures the X-CSRFToken header is set for all subsequent HTMX requests.

    <script>
      document.body.setAttribute('hx-headers', '{"X-CSRFToken": "{{ csrf_token }}"}');
    </script>
  9. Implement Modal Dialogs with HTMX and Django

    master

    To implement a full-featured modal dialog that handles form validation and parent-page updates, follow this pattern:

    1. Trigger the Modal: Use a button that fetches the modal HTML and appends it to the <body>.
    2. The Modal Template: Use the HTML <dialog> element. Include a <form> that posts to the same URL using hx-post="{{ request.get_full_path }}" to ensure validation happens within the same context. Use hx-vals='{"use_block": "dialog-contents"}' to leverage inline partials.
    3. Automatic Display: Add a custom attribute like data-onload-showmodal to the <dialog> and use a global htmx:afterSettle listener to call .showModal().
    4. Server-Side Validation: Use the @for_htmx(use_block_from_params=True) decorator on your Django view. When the form is valid, return an Hx-Trigger header containing events to close the modal and refresh the parent page.
    5. Cleanup and Refresh: Use JavaScript to listen for the custom close event to call .close() and remove the element from the DOM. The parent page should use hx-trigger="event_name from:body" to refresh its content.
    <!-- 1. Trigger Button -->
    <button
      hx-trigger="click"
      hx-get="{% url 'modals_create_monster' %}"
      hx-target="body"
      hx-swap="beforeend"
      >
      Add a monster
    </button>
    
    <!-- 2. Modal Template (modals_create_monster.html) -->
    <dialog id="dialog-main" data-onload-showmodal>
      {% block dialog-contents %}
        <form
            hx-post="{{ request.get_full_path }}"
            hx-target="#dialog-main"
            hx-vals='{"use_block": "dialog-contents"}'
            hx-swap="innerHTML"
        >
          {{ form.as_p }}
          <button type="submit">Add</button>
        </form>
      {% endblock %}
    </dialog>
  10. Install htmx using Django static files

    master

    You can download htmx.min.js and place it within your Django project's static assets directory. Use the {% load static %} tag to reference the file in your base.html. Including the version number in the filename (e.g., htmx.min.1.9.4.js) is a recommended practice for clarity.

    {% load static %}
    <html lang="en">
      <head>
        <script defer src="{% static 'js/htmx.min.js' %}"></script>
      </head>
    </html>
  11. Recommended approach for integrating HTMX with Django

    master

    When integrating HTMX into an existing Django application with server-side rendered HTML, follow this progression to avoid premature abstraction:

    1. Study the fundamentals: Read the htmx documentation.
    2. Implement manually: Work out the simplest way to apply flow control and changes directly within your Django views using basic logic (like if statements).
    3. Repeat and observe: Perform this manual implementation at least 3 or 4 times.
    4. Abstract: Only after repeating the manual process should you look for patterns or abstractions to clean up the code.

    Warning: Abstracting too early often leads to unnecessary complexity and pain.

  12. Bundle htmx with django-compressor

    master

    If you use a bundler like django-compressor, you can wrap your scripts in an {% compress js %} block.

    Best Practices for Bundling:

    • Use defer: Always use the defer attribute on your script tags. By placing your custom scripts after htmx in the bundle, you ensure that the htmx JavaScript API is available before your own code executes.
    • Development Configuration: Set COMPRESS_ENABLED = True in your Django settings for development. To aid debugging, avoid using minify filters and use unminified versions of 3rd party libraries like htmx.
    {% compress js %}
      <script type="text/javascript" defer src="{% static "js/htmx.min.js" %}"></script>
      <script type="text/javascript" defer src="{% static "js/mystuff.js" %}"></script>
    {% endcompress %}