django-unicorn Documentation

repository·main·Indexed 25 days ago

https://github.com/django-commons/django-unicorn

A reactive component framework for Django that adds modern front-end interactivity to templates without requiring complex JavaScript frameworks. It allows developers to define component logic in Python classes inheriting from UnicornView and bind them to HTML templates using unicorn: attributes for data binding and action triggering.

Tokens
28.7K
Snippets
107
Records
170
Agent score
79%

What's inside django-unicorn

  1. Understand Unicorn model and action behavior

    main

    Unicorn components interact with the server using specific patterns for data binding and method calls:

    Models (Data Binding)

    Elements with a :model attribute (or u:model) are bound to component attributes.

    • Event Listeners: Unicorn attaches change or blur listeners.
    • Lazy Modifier: Using the lazy modifier changes the listener type.
    • Defer Modifier: The defer modifier stores the action to be bundled with a subsequent action event.
    • Workflow: When a model event fires, a JSON payload is sent to the AJAX endpoint. The component is re-instantiated, data is updated, and the component is re-rendered.

    Actions (Method Calls)

    Actions are triggered via event listeners (e.g., unicorn:click).

    • Argument Parsing: Arguments and keyword arguments passed from the front-end are parsed using ast.parse and ast.literal_eval to ensure Python types (like converting the string "1" to the integer 1) are preserved.
    • Workflow: The component is re-initialized, the method is called with the parsed arguments/kwargs, and the component is re-rendered.
  2. Use component sub-folders

    main

    Components can be organized into sub-folders within your app's components/ directory. To reference a component in a sub-folder, use dot notation in the {% unicorn %} tag.

    Structure Example:

    • myapp/components/hello/world.py
    • myapp/templates/myapp/hello/world.html
    <!-- index.html -->
    {% load unicorn %}
    {% csrf_token %}
    
    {% unicorn 'hello.world' %}
  3. Bind Django QuerySets using dot notation

    main

    Django models within a QuerySet can be accessed in a unicorn:model binding using "dot notation" (e.g., queryset_name.index.field_name), similar to how you would access elements in a list.

    To ensure type safety for component fields holding QuerySets, use the QuerySetType type hint.

    # queryset.py
    from django_unicorn.components import QuerySetType, UnicornView
    from books.models import Book
    
    class QuerysetView(UnicornView):
        # Use QuerySetType[Model] for type hinting
        books: QuerySetType[Book] = None
    
        def mount(self):
            self.books = Book.objects.all().order_by("-id")[:5]
    
        def save(self, book_idx: int):
            self.books[book_idx].save()
    <!-- Accessing a specific item in the queryset via index -->
    <div>
      {% for book in books %}
      <div>
        <input unicorn:model.defer="books.{{ forloop.counter0 }}.title" type="text" id="title" />
        <button unicorn:click="save({{ forloop.counter0 }})">Save</button>
      </div>
      {% endfor %}
    </div>
  4. Handle mutable class variables in UnicornView

    main

    Avoid defining mutable default values (like list or dict) directly on class variables, as all component instances will share the same reference in memory.

    Instead, declare the type hint and initialize the mutable object inside the mount() method to ensure every instance gets its own unique object.

    from django_unicorn.components import UnicornView
    
    class SentenceView(UnicornView):
        words: list[str]  # Declare type hint without default
        word_counts: dict[str, int] = None  # Or use None as default
    
        def mount(self):
            # Initialize new objects every time the component is mounted
            self.words = []
            self.word_counts = {}
    
        def add_word(self, word: str):
            ...
  5. Call JavaScript from a UnicornView

    main

    You can execute JavaScript functions from your Python component methods using self.call().

    For security, django-unicorn uses a strict server-side allowlist. The easiest way to call custom logic is to attach your functions to the window.Unicorn object in your JavaScript code. This requires no extra configuration.

    Option B: Add a Custom Namespace

    To call third-party libraries (e.g., Swal.fire()) or use a custom namespace, you must add the namespace to the ALLOWED_JS_CALL_LIST in your settings.py under the UNICORN dictionary.

    WARNING

    Never add functions like eval or setTimeout to your allowlist. The frontend parser blocks them, but allowing them server-side creates XSS vulnerabilities.

    # settings.py
    UNICORN = {
        # Allow functions starting with Unicorn. and Swal.
        "ALLOWED_JS_CALL_LIST": ["Unicorn", "Swal"],
    }
    
    # call_javascript.py
    from django_unicorn.components import UnicornView
    
    class CallJavascriptView(UnicornView):
        def show_alert(self):
            # Now permitted since 'Swal' is in the allowlist
            self.call("Swal.fire", "Success!")
  6. Move modals outside the table to fix reactivity

    main

    The recommended way to handle modals or overlays triggered from within a table is to manage the state in the parent component and render the modal outside the <table> element. This prevents invalid DOM structures and ensures stable reactivity.

    Implementation pattern:

    1. In the child component, call a method on the parent to trigger the modal.
    2. In the parent component, update state to show the modal.
    3. In the parent template, render the modal after the </table> tag.
    # Child component
    def request_delete(self):
        self.parent.confirm_delete(self.item.id)
    # Parent component
    selected_id = None
    show_modal = False
    
    def confirm_delete(self, item_id):
        self.selected_id = item_id
        self.show_modal = True
    <!-- Parent template -->
    <table>
        ...contents...
    </table>
    
    {% if show_modal %}
        <div class="modal">...</div>
    {% endif %}
  7. Use type hints for field serialization

    main

    Adding type hints to class variables helps Unicorn ensure that fields are correctly serialized/deserialized to the appropriate Python types. For example, without a float hint, a numeric field might be treated as a str during AJAX interactions.

    from django_unicorn.components import UnicornView
    
    class RatingView(UnicornView):
        rating: float = 0
    
        def calculate_percentage(self):
            # The type hint ensures rating is a float, not a str
            print(self.rating / 100.0)
  8. Set up local development environment

    main

    To develop django-unicorn locally, fork the repository, clone it, and set up the environment using uv. You must install pre-commit hooks and sync dependencies with the minify and docs extras to ensure all development tools are available.

    git clone <your-forked-repo>
    cd django-unicorn
    just install-pre-commit
    # or
    uv run pre-commit install
    
    uv sync --extra minify --extra docs
    just runserver
  9. Adopt Unicorn incrementally in existing Django projects

    main
    Unicorn is designed for progressive enhancement. You do not need to migrate your entire application or move to an API-driven architecture. You can introduce Unicorn into an existing Django project one small component at a time by replacing specific interactive elements (like search inputs, dropdowns, or modals) while keeping the rest of your page unchanged.
  10. Refresh components automatically with unicorn:poll

    main

    Use the unicorn:poll attribute to automatically refresh a component at a regular interval (defaulting to every 2 seconds). This is useful for real-time dashboards or chat interfaces.

    Customizing Polling: To specify a specific method to be called during the poll, pass the method name as a string to the attribute: unicorn:poll="method_name".

    # polling.py
    from django.utils.timezone import now
    from django_unicorn.components import UnicornView
    
    class PollingView(UnicornView):
        current_time = now()
    <!-- polling.html -->
    <!-- Default polling (every 2 seconds) -->
    <div unicorn:poll>
        Current time: {{ current_time }}
    </div>
    
    <!-- Polling with a specific method call -->
    <div unicorn:poll="get_updates">
        Current time: {{ current_time }}
    </div>