Tutorials
Featured
Building Dynamic UIs with AJAX in Django
admin
November 28, 2025
•
5 min read
157 views
Create smooth, interactive user experiences with AJAX for wishlist toggles, cart updates, and live search without page reloads.
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:
// 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:
// 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:
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:
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:
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
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
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
- Debounce inputs - Prevent excessive requests
- Optimistic updates - Update UI immediately, revert on error
- Loading states - Show spinners during requests
- Error handling - Always catch and display errors
- Graceful degradation - Works without JavaScript
AJAX transforms your Django app into a modern, responsive experience!