Examples & Demos.
Explore real djust applications. Each demo is fully interactive with complete source code.
100% Server-Side Execution
Every click, input, and interaction you see below runs Python code on the server. No client-side JavaScript logic required. It just feels instant thanks to Rust-powered sub-millisecond VDOM diffing.
Real-Time Counter
Server-SideBasic reactivity with server-side state management
Each click executes increment() Python method on server
1from djust import LiveView, event_handler
2
3class CounterView(LiveView):
4 template_name = 'counter.html'
5
6 def mount(self, request):
7 """Initialize counter state"""
8 self.count = 0
9
10 @event_handler
11 def increment(self):
12 """Increment counter by 1"""
13 self.count += 1
14
15 @event_handler
16 def decrement(self):
17 """Decrement counter by 1"""
18 self.count -= 1
19
20 @event_handler
21 def reset(self):
22 """Reset counter to 0"""
23 self.count = 0
24
25 def get_context_data(self, **kwargs):
26 return {'count': self.count}
1<div class="counter-app">
2 <h2>Counter Example</h2>
3
4 <div class="count-display">
5 {{ count }}
6 </div>
7
8 <div class="button-group">
9 <button dj-click="decrement" class="btn-danger">
10 Decrement
11 </button>
12 <button dj-click="reset" class="btn-secondary">
13 Reset
14 </button>
15 <button dj-click="increment" class="btn-success">
16 Increment
17 </button>
18 </div>
19</div>
Todo List (CRUD)
Server-SideFull create, read, update, delete operations
Add, toggle, delete → all Python methods on server
1from djust import LiveView, event_handler
2
3class TodoListView(LiveView):
4 template_name = 'todo.html'
5
6 def mount(self, request):
7 """Initialize todo list"""
8 self.todos = []
9 self.next_id = 1
10
11 @event_handler
12 def add_todo(self, text: str = "", **kwargs):
13 """Add new todo item"""
14 if text.strip():
15 self.todos.append({
16 'id': self.next_id,
17 'text': text,
18 'completed': False
19 })
20 self.next_id += 1
21
22 @event_handler
23 def toggle_todo(self, id: int = None, **kwargs):
24 """Toggle todo completion status"""
25 todo = next((t for t in self.todos if t['id'] == id), None)
26 if todo:
27 todo['completed'] = not todo['completed']
28
29 @event_handler
30 def delete_todo(self, id: int = None, **kwargs):
31 """Delete todo item"""
32 self.todos = [t for t in self.todos if t['id'] != id]
33
34 def get_context_data(self, **kwargs):
35 completed = sum(1 for t in self.todos if t['completed'])
36 return {
37 'todos': self.todos,
38 'completed_count': completed,
39 'total_count': len(self.todos)
40 }
1<div class="todo-app">
2 <h2>Todo List</h2>
3
4 <form dj-submit="add_todo" class="add-form">
5 <input type="text"
6 name="text"
7 placeholder="What needs to be done?" />
8 <button type="submit">Add</button>
9 </form>
10
11 <div class="todo-list">
12 {% for todo in todos %}
13 <div class="todo-item">
14 <input type="checkbox"
15 dj-change="toggle_todo"
16 data-id="{{ todo.id }}"
17 {% if todo.completed %}checked{% endif %} />
18 <span class="{% if todo.completed %}completed{% endif %}">
19 {{ todo.text }}
20 </span>
21 <button dj-click="delete_todo"
22 data-id="{{ todo.id }}"
23 class="delete-btn">
24 Delete
25 </button>
26 </div>
27 {% endfor %}
28 </div>
29
30 <div class="stats">
31 <span>{{ completed_count }} completed</span>
32 <span>{{ total_count }} total</span>
33 </div>
34</div>
Real-Time Form Validation
Server-SideInstant feedback with Django Forms integration
Validation runs validate_field() on every keystroke
1from djust import LiveView, event_handler
2from djust.forms import FormMixin
3from .forms import SignupForm
4
5class SignupView(FormMixin, LiveView):
6 form_class = SignupForm
7 template_name = 'signup.html'
8
9 def mount(self, request):
10 """Initialize form view"""
11 self.success_message = None
12
13 @event_handler
14 def validate_field(self, field: str = None, value: str = "", **kwargs):
15 """Real-time field validation"""
16 # Validation happens automatically via FormMixin
17 # Errors are displayed in real-time
18 pass
19
20 def form_valid(self, form):
21 """Handle valid form submission"""
22 # Save the user
23 user = form.save()
24 self.success_message = "Account created successfully!"
25 # Reset form
26 self.form = self.form_class()
27
28 def form_invalid(self, form):
29 """Handle invalid form submission"""
30 # Errors displayed automatically
31 pass
1from django import forms
2from django.contrib.auth.models import User
3
4class SignupForm(forms.ModelForm):
5 """User signup form with validation"""
6
7 password = forms.CharField(
8 widget=forms.PasswordInput,
9 min_length=8,
10 help_text="At least 8 characters"
11 )
12
13 class Meta:
14 model = User
15 fields = ['email', 'password']
16
17 def clean_email(self):
18 """Validate email is unique"""
19 email = self.cleaned_data.get('email')
20 if User.objects.filter(email=email).exists():
21 raise forms.ValidationError(
22 "Email already registered"
23 )
24 return email
25
26 def clean_password(self):
27 """Validate password strength"""
28 password = self.cleaned_data.get('password')
29 if len(password) < 8:
30 raise forms.ValidationError(
31 "Password must be at least 8 characters"
32 )
33 return password
1<div class="signup-form">
2 <h2>Sign Up</h2>
3
4 <form dj-submit="submit_form">
5 <div class="form-field">
6 <label for="email">Email</label>
7 <input type="email"
8 name="email"
9 dj-change="validate_field"
10 value="{{ form.email.value|default:'' }}" />
11 {% if form.email.errors %}
12 <p class="error">{{ form.email.errors.0 }}</p>
13 {% endif %}
14 </div>
15
16 <div class="form-field">
17 <label for="password">Password</label>
18 <input type="password"
19 name="password"
20 dj-change="validate_field" />
21 {% if form.password.errors %}
22 <p class="error">{{ form.password.errors.0 }}</p>
23 {% endif %}
24 <p class="help">At least 8 characters</p>
25 </div>
26
27 <button type="submit">Sign Up</button>
28
29 {% if success_message %}
30 <p class="success">✓ {{ success_message }}</p>
31 {% endif %}
32 </form>
33</div>
Sortable Data Table
Server-SideInteractive table with search and sorting
|
Name
↑
|
Price
↕
|
Stock
↕
|
|---|---|---|
| Laptop Pro | $1299 | 42 |
| Wireless Mouse | $29 | 156 |
| Mechanical Keyboard | $89 | 23 |
| USB-C Hub | $45 | 78 |
| 27" Monitor | $399 | 12 |
Search & sort execute search() and sort() on server
1from djust import LiveView, event_handler
2from .models import Product
3
4class ProductTableView(LiveView):
5 template_name = 'product_table.html'
6
7 def mount(self, request):
8 """Initialize table view"""
9 self._products = Product.objects.all()
10 self.search_query = ""
11 self.sort_by = "name"
12 self.sort_order = "asc"
13
14 @event_handler
15 def search(self, value: str = "", **kwargs):
16 """Search products by name"""
17 self.search_query = value
18 self._refresh_products()
19
20 @event_handler
21 def sort(self, field: str = None, **kwargs):
22 """Sort products by field"""
23 if field == self.sort_by:
24 # Toggle order
25 self.sort_order = "desc" if self.sort_order == "asc" else "asc"
26 else:
27 self.sort_by = field
28 self.sort_order = "asc"
29 self._refresh_products()
30
31 def _refresh_products(self):
32 """Refresh product list with filters"""
33 queryset = Product.objects.all()
34
35 # Apply search filter
36 if self.search_query:
37 queryset = queryset.filter(
38 name__icontains=self.search_query
39 )
40
41 # Apply sorting
42 order_prefix = "-" if self.sort_order == "desc" else ""
43 queryset = queryset.order_by(f"{order_prefix}{self.sort_by}")
44
45 self._products = queryset
46
47 def get_context_data(self, **kwargs):
48 self.products = self._products # JIT serialization
49 context = super().get_context_data(**kwargs)
50 context.update({
51 'search_query': self.search_query,
52 'sort_by': self.sort_by,
53 'sort_order': self.sort_order
54 })
55 return context
1<div class="product-table">
2 <h2>Products</h2>
3
4 <input type="text"
5 dj-input="search"
6 value="{{ search_query }}"
7 placeholder="Search products..." />
8
9 <table>
10 <thead>
11 <tr>
12 <th dj-click="sort" data-field="name">
13 Name
14 {% if sort_by == 'name' %}
15 {% if sort_order == 'asc' %}↑{% else %}↓{% endif %}
16 {% endif %}
17 </th>
18 <th dj-click="sort" data-field="price">
19 Price
20 {% if sort_by == 'price' %}
21 {% if sort_order == 'asc' %}↑{% else %}↓{% endif %}
22 {% endif %}
23 </th>
24 <th dj-click="sort" data-field="stock">
25 Stock
26 {% if sort_by == 'stock' %}
27 {% if sort_order == 'asc' %}↑{% else %}↓{% endif %}
28 {% endif %}
29 </th>
30 </tr>
31 </thead>
32 <tbody>
33 {% for product in products %}
34 <tr>
35 <td>{{ product.name }}</td>
36 <td>${{ product.price }}</td>
37 <td>{{ product.stock }}</td>
38 </tr>
39 {% endfor %}
40 </tbody>
41 </table>
42</div>
Live Search with Debounce
Server-SideInstant search filtering with debounced input
Alice Johnson
alice@example.com
Bob Smith
bob@example.com
Carol White
carol@example.com
Dave Brown
dave@example.com
Eve Davis
eve@example.com
Frank Miller
frank@example.com
Grace Lee
grace@example.com
Hank Wilson
hank@example.com
dj-debounce="300" waits 300ms before calling server
1from djust import LiveView, event_handler
2
3class SearchView(LiveView):
4 template_name = 'search.html'
5
6 USERS = [
7 {"name": "Alice Johnson", "email": "alice@example.com", "role": "Admin"},
8 {"name": "Bob Smith", "email": "bob@example.com", "role": "Editor"},
9 {"name": "Carol White", "email": "carol@example.com", "role": "Viewer"},
10 {"name": "Dave Brown", "email": "dave@example.com", "role": "Editor"},
11 {"name": "Eve Davis", "email": "eve@example.com", "role": "Admin"},
12 ]
13
14 def mount(self, request):
15 self.query = ""
16 self.filtered_users = self.USERS
17
18 @event_handler
19 def search_users(self, value: str = "", **kwargs):
20 """Filter users by name or email"""
21 self.query = value
22 q = value.lower()
23 self.filtered_users = [
24 u for u in self.USERS
25 if q in u["name"].lower() or q in u["email"].lower()
26 ] if q else self.USERS
27
28 def get_context_data(self, **kwargs):
29 return {
30 'query': self.query,
31 'filtered_users': self.filtered_users,
32 }
1<div class="search-app">
2 <input type="text"
3 dj-input="search_users"
4 dj-debounce="300"
5 value="{{ query }}"
6 placeholder="Search users..." />
7
8 <div class="results">
9 {% for user in filtered_users %}
10 <div class="user-card">
11 <strong>{{ user.name }}</strong>
12 <span>{{ user.email }}</span>
13 <span class="role">{{ user.role }}</span>
14 </div>
15 {% empty %}
16 <p>No users found.</p>
17 {% endfor %}
18 </div>
19</div>
Chat Room (Simulated)
Server-SideReal-time message list with server-side state
Chat Room
Messages stored in server state via send_message()
1from djust import LiveView, event_handler
2from datetime import datetime
3
4class ChatView(LiveView):
5 template_name = 'chat.html'
6
7 def mount(self, request):
8 self.messages = []
9 self.username = "You"
10
11 @event_handler
12 def send_message(self, text: str = "", **kwargs):
13 """Add a message to the chat"""
14 if text.strip():
15 self.messages.append({
16 'user': self.username,
17 'text': text,
18 'time': datetime.now().strftime('%H:%M'),
19 })
20
21 @event_handler
22 def clear_chat(self, **kwargs):
23 """Clear all messages"""
24 self.messages = []
25
26 def get_context_data(self, **kwargs):
27 return {'messages': self.messages}
1<div class="chat-app">
2 <div class="messages">
3 {% for msg in messages %}
4 <div class="message">
5 <strong>{{ msg.user }}</strong>
6 <span class="time">{{ msg.time }}</span>
7 <p>{{ msg.text }}</p>
8 </div>
9 {% empty %}
10 <p class="empty">No messages yet.</p>
11 {% endfor %}
12 </div>
13
14 <form dj-submit="send_message">
15 <input type="text" name="text"
16 placeholder="Type a message..." />
17 <button type="submit">Send</button>
18 </form>
19 <button dj-click="clear_chat">Clear</button>
20</div>
Drag-and-Drop Kanban
Server-SideDrag cards between columns — HTML5 drag API + server state
todo
2Design homepage
Write unit tests
doing
2Build REST API
Set up CI pipeline
done
1Project scaffolding
Drag cards between columns — sendEvent() bridges HTML5 drag & drop to server
1from djust import LiveView, event_handler
2
3class KanbanView(LiveView):
4 template_name = 'kanban.html'
5
6 def mount(self, request):
7 self.columns = {
8 'todo': [
9 {'id': 1, 'text': 'Design homepage'},
10 {'id': 2, 'text': 'Write tests'},
11 ],
12 'doing': [
13 {'id': 3, 'text': 'Build API'},
14 ],
15 'done': [],
16 }
17 self.next_id = 4
18
19 @event_handler
20 def move_card(self, card_id: int = None,
21 target: str = "", **kwargs):
22 """Move card to another column"""
23 for col, cards in self.columns.items():
24 card = next((c for c in cards if c['id'] == card_id), None)
25 if card:
26 cards.remove(card)
27 self.columns[target].append(card)
28 break
29
30 @event_handler
31 def add_card(self, column: str = "", text: str = "", **kwargs):
32 """Add new card to column"""
33 if text.strip():
34 self.columns[column].append({
35 'id': self.next_id, 'text': text
36 })
37 self.next_id += 1
38
39 def get_context_data(self, **kwargs):
40 return {'columns': self.columns}
1<div class="kanban-board">
2 {% for col in columns %}
3 <div class="dropzone" data-col="{{ col.name }}">
4 <h3>{{ col.name|title }}</h3>
5 {% for card in col.cards %}
6 <div class="card" draggable="true"
7 data-card-id="{{ card.id }}">
8 {{ card.text }}
9 </div>
10 {% endfor %}
11 </div>
12 {% endfor %}
13</div>
14<script>
15// HTML5 drag & drop → server event
16zone.addEventListener('drop', (e) => {
17 const cardId = e.dataTransfer.getData('text');
18 djust.liveViewInstance.sendEvent('move_card', {
19 card_id: parseInt(cardId),
20 target: zone.dataset.col
21 });
22});
23</script>
Infinite Scroll Feed
Server-SideLoad more items on demand with server-side pagination
Post #1
This is the excerpt for post number 1. It contains interesting content worth reading.
Post #2
This is the excerpt for post number 2. It contains interesting content worth reading.
Post #3
This is the excerpt for post number 3. It contains interesting content worth reading.
Post #4
This is the excerpt for post number 4. It contains interesting content worth reading.
Post #5
This is the excerpt for post number 5. It contains interesting content worth reading.
load_more() increments page counter on server
1from djust import LiveView, event_handler
2
3class FeedView(LiveView):
4 template_name = 'feed.html'
5 PAGE_SIZE = 5
6
7 ITEMS = [f"Post #{i}: Lorem ipsum content..." for i in range(1, 51)]
8
9 def mount(self, request):
10 self.page = 1
11 self.feed_items = self.ITEMS[:self.PAGE_SIZE]
12 self.has_more = len(self.ITEMS) > self.PAGE_SIZE
13
14 @event_handler
15 def load_more(self, **kwargs):
16 """Load next page of items"""
17 self.page += 1
18 end = self.page * self.PAGE_SIZE
19 self.feed_items = self.ITEMS[:end]
20 self.has_more = end < len(self.ITEMS)
21
22 def get_context_data(self, **kwargs):
23 return {
24 'feed_items': self.feed_items,
25 'has_more': self.has_more,
26 }
1<div class="feed">
2 {% for item in feed_items %}
3 <div class="feed-item">
4 <p>{{ item }}</p>
5 </div>
6 {% endfor %}
7
8 {% if has_more %}
9 <button dj-click="load_more" class="load-more">
10 Load More
11 </button>
12 {% else %}
13 <p class="end">You've reached the end!</p>
14 {% endif %}
15</div>
Multi-Step Wizard Form
Server-SideStep-by-step form with validation and navigation
Step state + validation all managed server-side
1from djust import LiveView, event_handler
2
3class WizardView(LiveView):
4 template_name = 'wizard.html'
5
6 def mount(self, request):
7 self.step = 1
8 self.data = {'name': '', 'email': '', 'plan': ''}
9 self.errors = {}
10 self.submitted = False
11
12 @event_handler
13 def wizard_next(self, **kwargs):
14 """Validate and advance to next step"""
15 if self.step == 1 and not kwargs.get('name', '').strip():
16 self.errors = {'name': 'Name is required'}
17 return
18 if self.step == 2 and '@' not in kwargs.get('email', ''):
19 self.errors = {'email': 'Valid email required'}
20 return
21 self.data.update(kwargs)
22 self.errors = {}
23 self.step += 1
24
25 @event_handler
26 def wizard_prev(self, **kwargs):
27 """Go back one step"""
28 if self.step > 1:
29 self.step -= 1
30
31 @event_handler
32 def wizard_submit(self, **kwargs):
33 """Submit the wizard form"""
34 self.data.update(kwargs)
35 self.submitted = True
36
37 def get_context_data(self, **kwargs):
38 return {
39 'step': self.step,
40 'wizard_data': self.data,
41 'wizard_errors': self.errors,
42 'submitted': self.submitted,
43 }
1<div class="wizard">
2 <div class="steps">
3 Step {{ step }} of 3
4 </div>
5
6 {% if submitted %}
7 <div class="success">
8 <p>Submitted! Name: {{ wizard_data.name }}</p>
9 </div>
10 {% elif step == 1 %}
11 <form dj-submit="wizard_next">
12 <input name="name" placeholder="Your name"
13 value="{{ wizard_data.name }}" />
14 {% if wizard_errors.name %}
15 <p class="error">{{ wizard_errors.name }}</p>
16 {% endif %}
17 <button type="submit">Next →</button>
18 </form>
19 {% elif step == 2 %}
20 <form dj-submit="wizard_next">
21 <input name="email" placeholder="Your email"
22 value="{{ wizard_data.email }}" />
23 {% if wizard_errors.email %}
24 <p class="error">{{ wizard_errors.email }}</p>
25 {% endif %}
26 <button dj-click="wizard_prev">← Back</button>
27 <button type="submit">Next →</button>
28 </form>
29 {% elif step == 3 %}
30 <form dj-submit="wizard_submit">
31 <select name="plan">
32 <option value="free">Free</option>
33 <option value="pro">Pro</option>
34 </select>
35 <button dj-click="wizard_prev">← Back</button>
36 <button type="submit">Submit</button>
37 </form>
38 {% endif %}
39</div>
Sortable Table + Pagination
Server-SidePaginated data table with configurable page size
| Name | Category | Value |
|---|---|---|
| Item 1 | Books | 17 |
| Item 2 | Clothing | 34 |
| Item 3 | Electronics | 51 |
| Item 4 | Books | 68 |
| Item 5 | Clothing | 85 |
Pagination slicing computed server-side on each request
1from djust import LiveView, event_handler
2
3class PaginatedTableView(LiveView):
4 template_name = 'paginated_table.html'
5
6 DATA = [
7 {"name": f"Item {i}", "category": ["A","B","C"][i%3],
8 "value": i * 17 % 100}
9 for i in range(1, 31)
10 ]
11
12 def mount(self, request):
13 self.page = 1
14 self.per_page = 5
15
16 @event_handler
17 def change_page(self, page: int = 1, **kwargs):
18 self.page = max(1, page)
19
20 @event_handler
21 def change_per_page(self, size: int = 5, **kwargs):
22 self.per_page = size
23 self.page = 1
24
25 def get_context_data(self, **kwargs):
26 start = (self.page - 1) * self.per_page
27 end = start + self.per_page
28 total_pages = -(-len(self.DATA) // self.per_page)
29 return {
30 'rows': self.DATA[start:end],
31 'page': self.page,
32 'total_pages': total_pages,
33 'per_page': self.per_page,
34 }
1<div class="paginated-table">
2 <table>
3 <thead>
4 <tr>
5 <th>Name</th>
6 <th>Category</th>
7 <th>Value</th>
8 </tr>
9 </thead>
10 <tbody>
11 {% for row in rows %}
12 <tr>
13 <td>{{ row.name }}</td>
14 <td>{{ row.category }}</td>
15 <td>{{ row.value }}</td>
16 </tr>
17 {% endfor %}
18 </tbody>
19 </table>
20 <div class="pagination">
21 <button dj-click="change_page"
22 data-page="{{ page|add:-1 }}"
23 {% if page <= 1 %}disabled{% endif %}>
24 Prev
25 </button>
26 <span>Page {{ page }} of {{ total_pages }}</span>
27 <button dj-click="change_page"
28 data-page="{{ page|add:1 }}"
29 {% if page >= total_pages %}disabled{% endif %}>
30 Next
31 </button>
32 </div>
33</div>
Temperature Converter
Server-SideTwo-way reactive binding between Celsius and Fahrenheit
°C
°F
Two-way binding: each input calls a server handler that updates both values
1from djust import LiveView, event_handler
2
3class ConverterView(LiveView):
4 template_name = 'converter.html'
5
6 def mount(self, request):
7 self.celsius = 0
8 self.fahrenheit = 32
9
10 @event_handler
11 def update_celsius(self, value: str = "0", **kwargs):
12 try:
13 c = float(value)
14 self.celsius = c
15 self.fahrenheit = round(c * 9/5 + 32, 1)
16 except ValueError:
17 pass
18
19 @event_handler
20 def update_fahrenheit(self, value: str = "32", **kwargs):
21 try:
22 f = float(value)
23 self.fahrenheit = f
24 self.celsius = round((f - 32) * 5/9, 1)
25 except ValueError:
26 pass
27
28 def get_context_data(self, **kwargs):
29 return {
30 'celsius': self.celsius,
31 'fahrenheit': self.fahrenheit,
32 }
1<div class="converter">
2 <div class="field">
3 <label>Celsius</label>
4 <input type="number"
5 dj-change="update_celsius"
6 value="{{ celsius }}" />
7 </div>
8
9 <span class="arrow">↔</span>
10
11 <div class="field">
12 <label>Fahrenheit</label>
13 <input type="number"
14 dj-change="update_fahrenheit"
15 value="{{ fahrenheit }}" />
16 </div>
17</div>
Polling Dashboard (Simulated)
Server-SideDashboard cards with randomized metrics on refresh
Users
789
Revenue
$8602
Orders
94
Uptime
99.77%
refresh_metrics() generates new random data on server
1from djust import LiveView, event_handler
2import random
3
4class DashboardView(LiveView):
5 template_name = 'dashboard.html'
6
7 def mount(self, request):
8 self._generate_metrics()
9
10 def _generate_metrics(self):
11 self.metrics = {
12 'users': random.randint(100, 999),
13 'revenue': random.randint(1000, 9999),
14 'orders': random.randint(10, 200),
15 'uptime': round(random.uniform(99.0, 99.99), 2),
16 }
17
18 @event_handler
19 def refresh_metrics(self, **kwargs):
20 """Simulate polling for new data"""
21 self._generate_metrics()
22
23 def get_context_data(self, **kwargs):
24 return {'metrics': self.metrics}
1<div class="dashboard">
2 <div class="grid">
3 <div class="card">
4 <h4>Users</h4>
5 <p class="metric">{{ metrics.users }}</p>
6 </div>
7 <div class="card">
8 <h4>Revenue</h4>
9 <p class="metric">${{ metrics.revenue }}</p>
10 </div>
11 <div class="card">
12 <h4>Orders</h4>
13 <p class="metric">{{ metrics.orders }}</p>
14 </div>
15 <div class="card">
16 <h4>Uptime</h4>
17 <p class="metric">{{ metrics.uptime }}%</p>
18 </div>
19 </div>
20 <button dj-click="refresh_metrics">
21 Refresh Data
22 </button>
23</div>
LLM Streaming
Server-SideSimulated token-by-token AI response with StreamingMixin
start_stream() simulates token-by-token LLM output
1from djust import LiveView, event_handler
2from djust.mixins import StreamingMixin
3
4class StreamingView(StreamingMixin, LiveView):
5 template_name = 'streaming.html'
6
7 SAMPLE_RESPONSE = (
8 "Django is a high-level Python web framework "
9 "that encourages rapid development and clean, "
10 "pragmatic design. Built by experienced developers, "
11 "it takes care of much of the hassle of web "
12 "development, so you can focus on writing your "
13 "app without needing to reinvent the wheel."
14 )
15
16 def mount(self, request):
17 self.tokens = []
18 self.streaming_active = False
19
20 @event_handler
21 def start_stream(self, prompt: str = "", **kwargs):
22 """Begin streaming a simulated LLM response"""
23 self.tokens = []
24 self.streaming_active = True
25 # Stream tokens one at a time
26 for word in self.SAMPLE_RESPONSE.split():
27 self.stream_token("response", word + " ")
28 self.streaming_active = False
29
30 def get_context_data(self, **kwargs):
31 return {
32 'tokens': self.tokens,
33 'streaming_active': self.streaming_active,
34 }
1<div class="streaming-app">
2 <h2>LLM Streaming</h2>
3
4 <form dj-submit="start_stream">
5 <input type="text" name="prompt"
6 placeholder="Ask something..." />
7 <button type="submit"
8 {% if streaming_active %}disabled{% endif %}>
9 {% if streaming_active %}Streaming...{% else %}Send{% endif %}
10 </button>
11 </form>
12
13 <div class="response" dj-stream="response">
14 {% for token in tokens %}{{ token }}{% endfor %}
15 </div>
16</div>
Who's Online (Presence)
Server-SideLive user presence tracking with automatic join/leave
Simulated PresenceMixin — toggle status, add/remove users
1from djust import LiveView, event_handler
2from djust.mixins import PresenceMixin
3
4class PresenceView(PresenceMixin, LiveView):
5 template_name = 'presence.html'
6 presence_channel = "lobby"
7
8 def mount(self, request):
9 self.track_presence(
10 channel=self.presence_channel,
11 user_info={"name": request.user.username}
12 )
13
14 def on_presence_join(self, user_info):
15 """Called when a user joins"""
16 pass # UI updates automatically
17
18 def on_presence_leave(self, user_info):
19 """Called when a user leaves"""
20 pass # UI updates automatically
21
22 def get_context_data(self, **kwargs):
23 return {
24 'online_users': self.get_presences(),
25 }
1<div class="presence-app">
2 <h2>Who's Online</h2>
3
4 <div class="user-list">
5 {% for user in online_users %}
6 <div class="user-entry">
7 <span class="status-dot
8 {% if user.status == 'online' %}online
9 {% else %}away{% endif %}">
10 </span>
11 <span class="name">{{ user.name }}</span>
12 <span class="status-label">{{ user.status }}</span>
13 </div>
14 {% empty %}
15 <p>No users online.</p>
16 {% endfor %}
17 </div>
18</div>
Server Push Notifications
Server-SideBackground tasks pushing updates to connected clients
push_to_view() pushes notifications from background tasks
1from djust import LiveView, event_handler
2from djust.push import push_to_view
3
4class NotificationsView(LiveView):
5 template_name = 'notifications.html'
6
7 def mount(self, request):
8 self.notifications = []
9 self.next_id = 1
10
11 @event_handler
12 def trigger_task(self, **kwargs):
13 """Simulate a background task completing"""
14 # In production, called from Celery/RQ:
15 push_to_view(
16 self.view_id,
17 "on_task_complete",
18 {"message": "Deployment finished!"}
19 )
20
21 @event_handler
22 def on_task_complete(self, message: str = "", **kwargs):
23 """Receive push from background task"""
24 self.notifications.append({
25 'id': self.next_id,
26 'message': message,
27 'type': 'success',
28 })
29 self.next_id += 1
30
31 @event_handler
32 def dismiss(self, id: int = None, **kwargs):
33 """Dismiss a notification"""
34 self.notifications = [
35 n for n in self.notifications if n['id'] != id
36 ]
37
38 def get_context_data(self, **kwargs):
39 return {'notifications': self.notifications}
1<div class="push-app">
2 <h2>Server Push Notifications</h2>
3
4 <button dj-click="trigger_task" class="btn-primary">
5 Simulate Background Task
6 </button>
7
8 <div class="toast-container">
9 {% for note in notifications %}
10 <div class="toast toast-{{ note.type }}">
11 <span>{{ note.message }}</span>
12 <button dj-click="dismiss"
13 data-id="{{ note.id }}"
14 class="close-btn">
15 ×
16 </button>
17 </div>
18 {% empty %}
19 <p class="empty">No notifications yet.</p>
20 {% endfor %}
21 </div>
22</div>
Two-Way Data Binding
Server-SideLive form binding with dj-model — no event handlers needed
Live Preview
Name: —
Email: —
Bio: —
dj-model auto-syncs fields to server state
1from djust import LiveView
2from djust.mixins import ModelBindingMixin
3
4class ProfileView(ModelBindingMixin, LiveView):
5 template_name = 'profile.html'
6
7 # Fields auto-synced via dj-model
8 bound_fields = ['name', 'email', 'bio']
9
10 def mount(self, request):
11 self.name = ""
12 self.email = ""
13 self.bio = ""
14
15 def get_context_data(self, **kwargs):
16 return {
17 'name': self.name,
18 'email': self.email,
19 'bio': self.bio,
20 }
1<div class="binding-app">
2 <h2>Profile Editor</h2>
3
4 <div class="form-fields">
5 <div class="field">
6 <label>Name</label>
7 <input type="text" dj-model="name"
8 placeholder="Your name" />
9 </div>
10 <div class="field">
11 <label>Email</label>
12 <input type="email" dj-model="email"
13 placeholder="you@example.com" />
14 </div>
15 <div class="field">
16 <label>Bio</label>
17 <textarea dj-model="bio"
18 placeholder="Tell us about yourself...">
19 </textarea>
20 </div>
21 </div>
22
23 <div class="preview">
24 <h3>Live Preview</h3>
25 <p><strong>Name:</strong> {{ name|default:"—" }}</p>
26 <p><strong>Email:</strong> {{ email|default:"—" }}</p>
27 <p><strong>Bio:</strong> {{ bio|default:"—" }}</p>
28 </div>
29</div>
Chunked File Upload
Server-SideDrag-and-drop upload with progress via UploadMixin
UploadMixin handles chunked uploads with progress
1from djust import LiveView, event_handler
2from djust.mixins import UploadMixin
3
4class ChunkedUploadView(UploadMixin, LiveView):
5 template_name = 'upload_chunked.html'
6
7 def mount(self, request):
8 self.files = []
9 self.progress = 0
10 self.uploading = False
11
12 def allow_upload(self):
13 """Configure upload constraints"""
14 return {
15 'max_size': '50MB',
16 'accept': ['.pdf', '.png', '.jpg', '.zip'],
17 'chunked': True,
18 'chunk_size': '1MB',
19 }
20
21 def on_upload_progress(self, filename, percent):
22 """Called as chunks arrive"""
23 self.progress = percent
24
25 def on_upload_complete(self, upload):
26 """Called when upload finishes"""
27 self.files.append({
28 'name': upload.filename,
29 'size': upload.size_display,
30 })
31 self.uploading = False
32 self.progress = 0
33
34 def get_context_data(self, **kwargs):
35 return {
36 'files': self.files,
37 'progress': self.progress,
38 'uploading': self.uploading,
39 }
1<div class="upload-app">
2 <h2>Chunked File Upload</h2>
3
4 <div dj-upload-drop class="dropzone"
5 {% if uploading %}disabled{% endif %}>
6 <p>Drag & drop files here</p>
7 <p class="hint">PDF, PNG, JPG, ZIP up to 50MB</p>
8 </div>
9
10 {% if uploading %}
11 <div class="progress-bar">
12 <div class="fill" style="width: {{ progress }}%">
13 {{ progress }}%
14 </div>
15 </div>
16 {% endif %}
17
18 {% if files %}
19 <div class="file-list">
20 <h3>Uploaded Files</h3>
21 {% for file in files %}
22 <div class="file-entry">
23 <span>{{ file.name }}</span>
24 <span class="size">{{ file.size }}</span>
25 </div>
26 {% endfor %}
27 </div>
28 {% endif %}
29</div>
Ready to Build Your Own?
Install djust locally to start building real-time applications with these patterns.