Building Dynamic UIs with AJAX in Django
# Building Dynamic UIs with AJAX in Django
Modern web applications need smooth, instant interactions. Here's how we use AJAX in DjangoZen for a seamless user experience.
## Why AJAX?
- **No page reloads**: Instant feedback
- **Better UX**: Smooth interactions
- **Reduced server load**: Only fetch what's needed
- **Progressive enhancement**: Works without JS too
## CSRF Token Handling
Django requires CSRF tokens for POST requests:
```javascript
// Get CSRF token from cookie
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// Setup for fetch requests
const fetchConfig = {
headers: {
'X-CSRFToken': csrftoken,
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json',
}
};
```
## Wishlist Toggle
One-click add/remove from wishlist:
```javascript
// Event delegation for wishlist buttons
document.addEventListener('click', function(e) {
if (e.target.closest('.wishlist-btn')) {
e.preventDefault();
const btn = e.target.closest('.wishlist-btn');
const productId = btn.dataset.productId;
const icon = btn.querySelector('i');
// Optimistic UI update
icon.classList.toggle('far');
icon.classList.toggle('fas');
fetch(`/wishlist/toggle/${productId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken,
'X-Requested-With': 'XMLHttpRequest',
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update navbar badge
updateWishlistBadge(data.wishlist_count);
// Show toast notification
showToast(data.message, 'success');
} else {
// Revert on failure
icon.classList.toggle('far');
icon.classList.toggle('fas');
showToast(data.message, 'error');
}
})
.catch(error => {
// Revert on error
icon.classList.toggle('far');
icon.classList.toggle('fas');
console.error('Error:', error);
});
}
});
function updateWishlistBadge(count) {
const badge = document.querySelector('.wishlist-badge');
if (count > 0) {
if (badge) {
badge.textContent = count;
} else {
// Create badge if doesn't exist
const wishlistLink = document.querySelector('a[href*="/wishlist/"]');
if (wishlistLink) {
const newBadge = document.createElement('span');
newBadge.className = 'badge rounded-pill bg-danger wishlist-badge';
newBadge.textContent = count;
wishlistLink.querySelector('span').appendChild(newBadge);
}
}
} else if (badge) {
badge.remove();
}
}
```
Django view:
```python
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
@login_required
@require_POST
def toggle_wishlist(request, product_id):
product = get_object_or_404(DigitalItem, id=product_id)
wishlist, _ = Wishlist.objects.get_or_create(user=request.user)
if product in wishlist.products.all():
wishlist.products.remove(product)
in_wishlist = False
message = 'Removed from wishlist'
else:
wishlist.products.add(product)
in_wishlist = True
message = 'Added to wishlist'
return JsonResponse({
'success': True,
'in_wishlist': in_wishlist,
'message': message,
'wishlist_count': wishlist.products.count(),
})
```
## Live Search with Autocomplete
Real-time search suggestions:
```javascript
const searchInput = document.getElementById('searchInput');
const autocomplete = document.getElementById('searchAutocomplete');
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value.trim();
if (query.length < 2) {
autocomplete.style.display = 'none';
return;
}
debounceTimer = setTimeout(() => {
fetch(`/api/search/suggestions/?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.suggestions.length > 0) {
renderSuggestions(data.suggestions);
autocomplete.style.display = 'block';
} else {
autocomplete.style.display = 'none';
}
});
}, 300); // 300ms debounce
});
function renderSuggestions(suggestions) {
let html = '';
// Products
if (suggestions.products.length > 0) {
html += '<div class="p-2 bg-light border-bottom"><small class="text-muted fw-bold">PRODUCTS</small></div>';
suggestions.products.forEach(product => {
html += `
<a href="/product/${product.slug}/"
class="d-flex align-items-center px-3 py-2 text-decoration-none text-dark suggestion-item">
<img src="${product.image}" alt="" class="me-3" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;">
<div>
<div class="fw-medium">${product.name}</div>
<small class="text-success">€${product.price}</small>
</div>
</a>
`;
});
}
// Categories
if (suggestions.categories.length > 0) {
html += '<div class="p-2 bg-light border-bottom"><small class="text-muted fw-bold">CATEGORIES</small></div>';
suggestions.categories.forEach(category => {
html += `
<a href="/category/${category.slug}/"
class="d-block px-3 py-2 text-decoration-none text-dark suggestion-item">
<i class="${category.icon} me-2" style="color: ${category.color}"></i>
${category.name}
</a>
`;
});
}
autocomplete.innerHTML = html;
}
// Close autocomplete when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('#searchForm')) {
autocomplete.style.display = 'none';
}
});
// Keyboard navigation
searchInput.addEventListener('keydown', function(e) {
const items = autocomplete.querySelectorAll('.suggestion-item');
const current = autocomplete.querySelector('.suggestion-item.active');
if (e.key === 'ArrowDown') {
e.preventDefault();
if (!current) {
items[0]?.classList.add('active');
} else {
const next = current.nextElementSibling;
if (next?.classList.contains('suggestion-item')) {
current.classList.remove('active');
next.classList.add('active');
}
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (current) {
const prev = current.previousElementSibling;
current.classList.remove('active');
if (prev?.classList.contains('suggestion-item')) {
prev.classList.add('active');
}
}
} else if (e.key === 'Enter' && current) {
e.preventDefault();
window.location.href = current.href;
}
});
```
## Cart Quantity Updates
Update cart without page reload:
```javascript
document.querySelectorAll('.quantity-input').forEach(input => {
input.addEventListener('change', async function() {
const itemId = this.dataset.itemId;
const quantity = parseInt(this.value);
if (quantity < 1) {
this.value = 1;
return;
}
try {
const response = await fetch('/cart/update/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken,
},
body: JSON.stringify({
item_id: itemId,
quantity: quantity
})
});
const data = await response.json();
if (data.success) {
// Update item subtotal
document.querySelector(`#item-${itemId} .item-subtotal`).textContent =
`€${data.item_subtotal.toFixed(2)}`;
// Update cart total
document.querySelector('.cart-total').textContent =
`€${data.cart_total.toFixed(2)}`;
// Update navbar badge
document.querySelector('.cart-badge').textContent = data.item_count;
}
} catch (error) {
console.error('Error updating cart:', error);
showToast('Failed to update cart', 'error');
}
});
});
```
## Newsletter Subscription
```javascript
document.getElementById('newsletterForm').addEventListener('submit', async function(e) {
e.preventDefault();
const email = this.querySelector('input[name="email"]').value;
const button = this.querySelector('button[type="submit"]');
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch('/newsletter/subscribe/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrftoken,
},
body: `email=${encodeURIComponent(email)}`
});
const data = await response.json();
const messageEl = document.getElementById('newsletterMessage');
messageEl.textContent = data.message;
messageEl.className = data.success ? 'text-success' : 'text-warning';
messageEl.style.display = 'block';
if (data.success) {
this.reset();
}
} catch (error) {
console.error('Error:', error);
} finally {
button.disabled = false;
button.innerHTML = originalText;
}
});
```
## Toast Notifications
```javascript
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container') || createToastContainer();
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'info'} border-0`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'times-circle' : 'info-circle'} me-2"></i>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toast-container';
container.className = 'toast-container position-fixed bottom-0 end-0 p-3';
container.style.zIndex = '1100';
document.body.appendChild(container);
return container;
}
```
## Best Practices
1. **Debounce inputs** - Prevent excessive requests
2. **Optimistic updates** - Update UI immediately, revert on error
3. **Loading states** - Show spinners during requests
4. **Error handling** - Always catch and display errors
5. **Graceful degradation** - Works without JavaScript
AJAX transforms your Django app into a modern, responsive experience!