Instruction file imported from dtebar-10010/tq_v02 (
.github/instructions/ajax-patterns.instructions.md). Copyright stays with the author.
AJAX Patterns - Agent Instructions
Detection Pattern
All AJAX endpoints use this detection:
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JsonResponse({...})
return redirect("tqv02_app:basket") # fallback for non-JS
The header is set automatically by jQuery $.ajax(). Do NOT switch to fetch() - front.js uses jQuery throughout.
Response Shape - Canonical Format
Every AJAX endpoint MUST return this shape on success:
return JsonResponse(
{
"success": True,
"message": "Human-readable status message",
"cart_count": int, # total items in cart
"cart_total": "$123.45", # pre-formatted with $ sign
# ... endpoint-specific fields
}
)
On error:
return JsonResponse(
{
"success": False,
"error": "Human-readable error message",
},
status=400,
)
Endpoint-Specific Fields
| Endpoint | Extra fields |
|---|---|
add_to_cart |
created (bool), quantity (int), product (str name), wishlist_removed (bool) |
remove_from_cart |
- |
update_cart_quantity |
quantity (int), item_subtotal (str), removed (bool, if qty dropped to 0) |
basket_mini |
html (rendered HTML), count (int), total (str), items (list of dicts) |
add_to_wishlist |
created (bool) |
CartManager - Single Source of Truth
front.js contains the CartManager class. This is the ONLY place that handles cart AJAX interactions. Do NOT add competing click handlers in cart.js, detail.js, or inline <script> blocks.
How CartManager Sends Requests
// POST requests (add, update)
$.ajax({
url: `/cart/add/${productId}/`,
type: 'POST',
data: {
quantity: qty,
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
},
success: function(data) {
if (data.success) { ... }
}
});
// GET requests (remove, wishlist)
$.ajax({
url: `/cart/remove/${productId}/`,
type: 'GET',
success: function(data) { ... }
});
Event Delegation (document-level)
| Selector | Action | Method |
|---|---|---|
.add-to-cart |
click | addToCart() - POST |
.remove-item, .remove-from-cart, .remove-from-mini-cart |
click | removeFromCart() - GET |
.cart-quantity-input, .quantity-input |
change | updateQuantity() - POST |
.add-to-wishlist |
click | addToWishlist() - GET |
.card-qty-up, .card-qty-down |
click | inline qty spinner adjust |
Button State During AJAX
// Showing spinner
$btn.html('<span class="fa fa-spinner fa-spin mr-1"></span><span class="btn-label">Adding…</span>')
.prop('disabled', true);
// Restore on complete
$btn.html(originalHtml).prop('disabled', false);
Counter Updates After AJAX
CartManager updates these DOM elements after every cart operation:
// Badge counters
$('.cart-text').text(data.cart_count);
$('.cart-counter').text(data.cart_count);
$('#cart-count').text(data.cart_count);
// Total display
$('#cart-total-display').text(data.cart_total);
Cart Duality - Session vs DB
Adding Variant Data to AJAX
When adding variant support, the add_to_cart endpoint must accept an optional variant_id parameter:
// CartManager.addToCart() - updated for variants
data: {
quantity: qty,
variant_id: $btn.data('variant-id') || '', // empty = simple product
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
}
Server-side, check if the product has variants and validate:
variant_id = request.POST.get("variant_id", "")
if product_has_variants and not variant_id:
return JsonResponse({"success": False, "error": "Please select options"}, status=400)
Session Cart Format with Variants
Current format:
request.session["basket"] = {"42": 3, "57": 1}
# {product_id_str: quantity}
With variants (backward compatible):
request.session["basket"] = {
"42": 3, # simple product - no variant
"57:variant:12": 1, # product 57, variant 12
"57:variant:15": 2, # product 57, different variant
}
Key format: "{product_id}" for simple products, "{product_id}:variant:{variant_id}" for variant products. This preserves backward compatibility - existing sessions without variants continue working.
DB Cart with Variants
The Basket model gains an optional variant FK:
# unique_together changes from ['user', 'product'] to ['user', 'product', 'variant']
variant = models.ForeignKey("ProductVariant", null=True, blank=True, on_delete=models.SET_NULL)
Minicart Integration
minicart.js MiniCartWidget.open() fetches /basket/mini/ and renders HTML. After any cart operation that adds/removes items, CartManager calls:
$('#mini-cart, #floating-mini-cart').each(function() {
$.get('/basket/mini/', function(data) {
$(dropSelector).html(data.html);
});
});
When displaying variant products in the minicart, the basket_mini view must include variant details in the rendered HTML (e.g., "T-Shirt - Size: M, Color: Blue").
CSRF Token
Always include csrfmiddlewaretoken in POST data. Get it from the hidden form field:
$('[name=csrfmiddlewaretoken]').val()
Every page includes this via {% csrf_token %} in the base template forms. Do NOT use X-CSRFToken header - the project uses form data, not header-based CSRF.
Anti-patterns
- Using
fetch()instead of$.ajax()- the entire frontend uses jQuery; mixing causes inconsistency - Adding click handlers outside CartManager - creates duplicate event firing
- Returning non-JSON from AJAX endpoints - always return
JsonResponse, even for errors - Forgetting
successfield - JS callers checkdata.successbefore acting - Hardcoding URLs in JS - use
data-*attributes on HTML elements (e.g.,data-cart-url) - Not invalidating checkout session - call
_invalidate_checkout_session()after any cart modification - Using
$.post()shorthand - use full$.ajax()for consistent error handling