Template filter chỉ biến đổi được một giá trị, nên khi bạn cần tính toán từ nhiều tham số hoặc render cả một khối HTML lặp lại, filter không đủ sức. Template tag tùy chỉnh giải quyết đúng việc đó: simple_tag trả về một chuỗi tính từ nhiều tham số, còn inclusion_tag render hẳn một template con với context riêng.

Cả hai viết trong vài dòng, đặt trong thư mục templatetags của app, và dùng ở mọi template sau một dòng {% load %}.

Vấn đề

Tình huống quen thuộc: trang nào cũng cần hiển thị giá sau giảm — giá gốc nhân phần trăm giảm, làm tròn rồi format tiền. Dùng filter thì bạn chỉ truyền được một giá trị kèm một tham số qua dấu hai chấm, đọc lại sau một tháng là không hiểu 20 kia nghĩa là gì.

Tệ hơn là khối “sản phẩm nổi bật”: cùng một vòng lặp và vài dòng HTML bị copy-paste sang trang chủ, trang danh mục, trang chi tiết — sửa một chỗ là quên chỗ khác.

{# Khối này bị copy-paste sang mọi template #}
{% for product in featured %}
<div class="card">
<h3>{{ product.name }}</h3>
<p>{{ product.price }} đ</p>
</div>
{% endfor %}

Giải pháp

Tạo package templatetags trong app, gồm file __init__.py (để trống) và file chứa tag, ví dụ shop_tags.py. Rồi đăng ký tag bằng decorator trên một hàm Python thường.

# shop/templatetags/shop_tags.py
from django import template
register = template.Library()
@register.simple_tag
def discounted_price(price, percent):
"""Giá sau giảm, đã format: 1.000.000 giảm 20% -> 800.000 đ."""
value = price * (100 - percent) // 100
return f"{value:,}".replace(",", ".") + " đ"

Dùng trong template sau một dòng load:

{% load shop_tags %}
<p>Giá sau giảm: {% discounted_price product.price 20 %}</p>

inclusion_tag dành cho khối HTML lặp lại. Thay vì copy vòng lặp, bạn viết một hàm chuẩn bị dữ liệu và một template con chỉ render khối đó:

@register.inclusion_tag("shop/featured_card.html")
def featured_products(count=4):
products = Product.objects.filter(
is_featured=True
).order_by("-created_at")[:count]
return {"products": products}
{# shop/templates/shop/featured_card.html #}
{% load shop_tags %}
{% for product in products %}
<div class="card">
<h3>{{ product.name }}</h3>
<p>{% discounted_price product.price 20 %}</p>
</div>
{% endfor %}

Từ giờ, mọi template chỉ cần một dòng là có khối sản phẩm nổi bật, khỏi copy vòng lặp:

{% featured_products 4 %}

Vì sao cách này tốt hơn? Logic nằm đúng một chỗ nên sửa một lần là đổi khắp site; tag là hàm Python thường nên viết test dễ như test hàm khác; template chỉ còn những dòng gọi tag ngắn gọn.

Áp dụng vào dự án của bạn

Rà lại template của bạn: phép tính dùng ở nhiều chỗ thì viết thành simple_tag, khối HTML xuất hiện từ hai nơi trở lên thì viết thành inclusion_tag. Đặt tên tag rõ nghĩa và viết một dòng docstring cho mỗi tag — sau vài tháng, chính bạn cũng quên tag đó làm gì nếu không có docstring. Một lưu ý nhỏ: sau khi tạo file tag mới, hãy khởi động lại runserver để Django nạp thư viện tag.

Bài viết liên quan

Câu hỏi thường gặp

Template tag khác template filter ở điểm nào?

Filter biến đổi một giá trị, viết trong {{ }}, chỉ nhận thêm một tham số. Tag là hàm Python đầy đủ: nhận nhiều tham số, truy vấn database, render template con, và gọi bằng dạng {% ten_tag ... %}.

Vì sao phải tạo thư mục templatetags và file __init__.py?

Django chỉ quét tag trong app khi module nằm trong package templatetags. Thiếu __init__.py, Python không coi thư mục là package và {% load shop_tags %} sẽ báo lỗi.

simple_tag có truy vấn database được không?

Được, vì nó là hàm Python thường. Nhưng tránh query nặng trong tag vì tag chạy mỗi lần template render. Nếu view đã có dữ liệu, truyền queryset vào tag thay vì query lại.

inclusion_tag và {% include %} khác nhau thế nào?

{% include %} chỉ chèn một template với context hiện tại, không tính toán thêm. inclusion_tag chạy một hàm Python trước để chuẩn bị context riêng — query, lọc, tính toán — rồi mới render template con.

Nên đặt tag dùng chung nhiều app ở đâu?

Đặt trong app dùng nhiều nhất, hoặc tạo một app core chuyên chứa tag và filter dùng chung toàn dự án, để khỏi import chéo giữa các app nghiệp vụ.

💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.

Nguồn tham khảo