Alpha v0.5.0

Full stack reactive component framework for Django using Alpine.js

Tetra is a new full stack component framework for Django, bridging the gap between your server logic and front end presentation. It uses a public shared state and a resumable server state to enable inplace reactive updates. It also encapsulates your Python, HTML, JavaScript and CSS into one file or directory for proximity of related concerns.

See how easy it is to build a todo list

Python: Todo List
from demo.models import ToDo
from tetra import Component, public


class TodoList(Component):
    title = public("")

    def load(self, *args, **kwargs):
        self.todos = ToDo.objects.filter(
            session_key=self.request.session.session_key,
        )

    @public
    def add_todo(self, title: str):
        if self.title:
            todo = ToDo(
                title=title,
                session_key=self.request.session.session_key,
            )
            todo.save()
            self.title = ""
Python: Todo Item
from tetra import Component, public


class TodoItem(Component):
    title = public("")
    done = public(False)

    def load(self, todo, *args, **kwargs):
        self.todo = todo
        self.title = todo.title
        self.done = todo.done

    @public.watch("title", "done")
    @public.debounce(200)
    def save(self, value, old_value, attr):
        self.todo.title = self.title
        self.todo.done = self.done
        self.todo.save()

    @public(update=False)
    def delete_item(self):
        self.todo.delete()
        self.client._removeComponent()
HTML: Todo List
{% load i18n %}
<div>
  <div class="input-group mb-2">
    <input type="text" x-model="title" class="form-control" placeholder="{% translate 'New task...' %}" @keyup.enter="add_todo(title)">
    <button class="btn btn-primary" :class="{'disabled': title == ''}" @click="add_todo(title)">{% translate 'Add' %}</button>
  </div>
  <div class="list-group">
    {% for todo in todos %}
      {% TodoItem todo=todo key=todo.id / %}
    {% endfor %}
  </div>
</div>
HTML: Todo Item
<div class="list-group-item d-flex gap-1 p-1" {% ... attrs %}>
  <label class="align-middle px-2 d-flex">
    <input class="form-check-input m-0 align-self-center" type="checkbox"
           x-model="done">
  </label>
  <input
      type="text"
      class="form-control border-0 p-0 m-0"
      :class="{'text-muted': done, 'todo-strike': done}"
      x-model="title"
      maxlength="80"
      @keydown.backspace="inputDeleteDown()"
      @keyup.backspace="inputDeleteUp()"
  >
  <button @click="delete_item()" class="btn btn-sm">
    <i class="fa-solid fa-trash"></i>
  </button>
</div>
CSS
.todo-strike {
    text-decoration: line-through;
}
Javascript
export default {
    lastTitleValue: "",
    inputDeleteDown() {
        this.lastTitleValue = this.title;
    },
    inputDeleteUp() {
        if (this.title === "" && this.lastTitleValue === "") {
            this.delete_item()
        }
    }
}
Python: Models
from django.db import models

class ToDo(models.Model):
    session_key = models.CharField(max_length=40, db_index=True)
    title = models.CharField(max_length=80)
    done = models.BooleanField(default=False)

Or a reactive server rendered search component:

Python
import itertools
from tetra import Component, public
from demo.movies import movies


class ReactiveSearch(Component):
    query = public("")
    results = []

    @public.watch("query")
    @public.throttle(200, leading=False, trailing=True)
    def watch_query(self, value, old_value, attr):
        if self.query:
            self.results = itertools.islice(
                (movie for movie in movies if self.query.lower() in movie.lower()),
                20,
            )
        else:
            self.results = []
HTML
{% load i18n %}
<div>
  <p>
    <input class="form-control" placeholder="{% translate 'Search for an 80s movie...' %}"
           type="text" x-model="query">
  </p>
  <ul>
    {% for result in results %}
      <li>{{ result }}</li>
    {% endfor %}
  </ul>
</div>

Or a counter, but with multiple nested instances.

Python
from tetra import Component, public


class Counter(Component):
    count = 0
    current_sum = 0

    def load(self, current_sum=None, *args, **kwargs):
        if current_sum is not None:
            self.current_sum = current_sum

    @public
    def increment(self):
        self.count += 1

    @public
    def decrement(self):
        self.count -= 1

    def sum(self):
        return self.count + self.current_sum
Django Template
{% load tetra %}
{% Counter key="counter-1" %}
  {% Counter key="counter-2" current_sum=sum %}
    {% Counter key="counter-3" current_sum=sum / %}
  {% /Counter %}
{% /Counter %}

Count: 0, Sum: 0

Count: 0, Sum: 0

Count: 0, Sum: 0

Or a simple card, with dynamic content.

Python
from sourcetypes import django_html
from django.utils.translation import gettext_lazy as _

from tetra import Component, public


class InfoCard(Component):
    title: str = _("I'm so excited!")
    content: str = _("We got news for you.")
    name: str = public("")

    @public
    def close(self):
        self.client._removeComponent()

    @public
    def done(self):
        print("User clicked on OK, username:", self.name)
        self.content = _("Hi {name}! No further news.").format(name=self.name)

    # language=html
    template: django_html = """
    {% load i18n %}
    <div class="card text-white bg-secondary mb-3" style="max-width: 18rem;">
      <div class="card-header d-flex justify-content-between">
        <h3>{% translate "Information" %}</h3>
        <button class="btn btn-sm btn-warning" @click="_removeComponent(
        )"><i class="fa fa-x"></i></button>
      </div>
      
      <div class="card-body">
        <h5 class="card-title">{{ title }}</h5>
        <p class="card-text">
          {{ content }}
        </p>
        <p x-show="!name">
          {% translate "Enter your name below!" %}
        </p>
        <p x-show="name">
            {% translate "Thanks," %} {% livevar name %}
        </p>
        <div class="input-group mb-3">

          <input 
            type="text" 
            class="form-control" 
            placeholder="{% translate 'Your name' %}"
            @keyup.enter="done()"
            x-model="name">
        </div>
        <button 
            class="btn btn-primary" 
            @click="done()" 
            :disabled="name == ''">
            {% translate "Ok" %}
        </button>
      </div>
      
    </div>
    """
Django Template

Information

I'm so excited!

We got news for you.

Enter your name below!

Thanks,

Not convinced? How about a reactive news ticker with push notifications?

Python
import random
from demo.models import BreakingNews
from tetra import public, ReactiveComponent


class NewsTicker(ReactiveComponent):
    headline: str = public("")
    # could be a fixed subscription too:
    # subscribe = ["notifications.news.headline"]

    def load(self, *args, **kwargs) -> None:
        # Fetch the latest news headline from database
        self.breaking_news = BreakingNews.objects.all()
        # get random item from BreakingNews
        self.headline = random.choice(self.breaking_news).title
HTML
<div class="card mt-2 ">
  <div class="card-body fs-5">
    {% livevar headline %}
  </div>
</div>

Django Template
{% NewsTicker subscribe="notifications.news.headline" %}
Async server code
while True:
    count = await BreakingNews.objects.acount()
    offset = random.randint(0, count - 1)
    news = await BreakingNews.objects.all()[offset : offset + 1].aget()

    await ComponentDispatcher.update_data(
    "notifications.news.headline",
    {
    "headline": news.title,
    },
    )
    await asyncio.sleep(5)
    
The following (partly fake) news line is not polled, but pushed from the server to all connected clients simultaneously each 10 seconds.
This might look trivial, you could do that with polling too. But Tetra is smarter than that.
Open multiple browsers/tabs with this very page, even on your phone, and watch the news change simultaneously.
An async django task worker is pushing the news to all connected clients.

What does Tetra do?

Django on the backend, Alpine.js in the browser

Tetra combines the power of Django with Alpine.js to make development easier and quicker.

Component encapsulation

Each component combines its Python, HTML, CSS and JavaScript in one place for close proximity of related code.

Async HTTP calls

Server calls are asynchronous, ensuring the frontend remains responsive. Multiple, fast clicks on the same button are not dropped between HTTP calls.

Resumable server state

The components' full server state is saved between public method calls. This state is encrypted for security.

Public server methods

Methods can be made public, allowing you to easily call them from JS on the front end, resuming the component's state.

Shared public state

Attributes can be decorated to indicate they should be available in the browser as Alpine.js data objects.

Server "watcher" methods

Public methods can be instructed to watch a public attribute, enabling reactive re-rendering on the server.

Inplace updating from the server

Server methods can update the rendered component in place. Powered by the Alpine.js morph plugin.

Component library packaging

Every component belongs to a "library"; their JS & CSS is packed together for quicker browser downloads.

Components with overridable slots

Components can have multiple {% slot(s) %} which can be overridden when used.

JS/CSS builds using esbuild

Both for development (built into runserver) and production your JS & CSS is built with esbuild.

Source Maps

Source maps are generated during development so that you can track down errors to the original Python files.

Syntax highlighting with type annotations

Tetra uses type annotations to syntax highlight your JS, CSS & HTML in your Python files with a VS Code plugin

Form components

A simple replacement for Django's FormView, but due to Tetra's dynamic nature, e.g. a field can change its value or disappear depending on other fields' values.

Event subscriptions

Both frontend and backend components can subscribe to JavaScript events, enabling seamless event-driven programming across the full stack.

File upload/downloads

Whenever a form contains a FileField, Tetra makes sure the uploading process works smooth, even with page reloads.

Integration with Django Messages

Django's messaging framework is deeply integrated into Tetra: Whenever a new message occurs, it is transformed into a JavaScript object and sent as an event that can be subscribed to by any component.

Loading indicators

While an AJAX request is in flight, you can show a loading indicator, globally, per component, or per call.

Reactive components / realtime updates

Component data can be updated from the server using websockets/push notification, even by async Django background tasks, Celery, signals etc.

More features are planned, including:

• ModelComponent for bootstrapping standard CRUD interactions.   • Python type annotations   • Integration with Django Validators   • Alpine.js directives for component state (loading ✅, offline etc.)   • Routing and query strings ✅ for "whole page" components.   • Page title and metadata in header.   • Pagination and Infinity Scroll components.   • PostCSS/SASS/etc. support.   • CSS scoped to a component.   • Redirect responses.   • Additional authentication tools.   • Integration with UI & component toolkits.   • Bundling of esbuild to be npm free.