Back to Blog
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

  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!

Comments (0)

Please login to leave a comment.

No comments yet. Be the first to comment!