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
↕
|
|---|---|---|
| 27" Monitor | $399 | 12 |
| Laptop Pro | $1299 | 42 |
| Mechanical Keyboard | $89 | 23 |
| USB-C Hub | $45 | 78 |
| Wireless Mouse | $29 | 156 |
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
507
Revenue
$5968
Orders
64
Uptime
99.23%
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>
Ready to Build Your Own?
Install djust locally to start building real-time applications with these patterns.