Skip to content

Refactored convert pagination to django-components structure. #586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

Antoliny0919
Copy link
Collaborator

I refactoring the existing pagination using django-components.

Screenshot 2025-07-07 at 9 12 07 PM

@Antoliny0919 Antoliny0919 marked this pull request as draft July 7, 2025 12:13
@Antoliny0919 Antoliny0919 force-pushed the refactor_pagination_component branch from 8688786 to a6709a1 Compare July 7, 2025 23:47
@Antoliny0919
Copy link
Collaborator Author

@JuroOravec
It’s a bit nerve wracking to get a review from a maintainer, but thank you so much ☺️

)

def get_template_data(self, args, kwargs, slots, context):
pagination = kwargs.get("pagination_obj")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add input validation by defining a Kwargs class. When you do so, then the kwargs argument to get_template_data() will be an instance of Kwargs (instead of plain dict).

This will have the effect of input validation, because if we render the component without pagination_obj then the instantiation of NamedTuple will raise an error:

@register("pagination")
class PaginationComponent(Component):
    template_file = "pagination.html"

    # This will raise if `pagination_obj` is missing
    class Kwargs(NamedTuple):
        pagination_obj: Pagination

    def get_template_data(self, args, kwargs: Kwargs, slots, context):
        pagination = kwargs.pagination_obj
        # Rest of the code...

The limitation of using NamedTuple for Kwargs is that it will only check the pagination_obj input is given, but it won't check that pagination_obj is of correct type.

You could potentially also get rid of the input validation on lines 34-35. For that you'd have to add Pydantic as a dependency. When you use Pydantic's BaseModel for defining Kwargs, then Pydantic will check both the shape AND the types of individual fields.

So overall to not have to do component input validation by hand, the code could look like this:

from django_components import Component, register
from django.utils.html import format_html, format_html_join
from pydantic import BaseModel  # NEW

from base.pagination import  PAGE_VAR, Pagination
from base.templatetags.base_templatetags import querystring


@register("pagination")
class PaginationComponent(Component):
    template_file = "pagination.html"

    # This will raise if `pagination_obj` is missing or of wrong type
    class Kwargs(BaseModel):
        pagination_obj: Pagination

    def get_template_data(self, args, kwargs: Kwargs, slots, context):
        pagination = kwargs.pagination_obj
        page_elements = format_html_join(
        # Rest of the code...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Kwargs with Pydantic definitely makes input validation much cleaner.
Type hints haven’t been applied in djangosnippets yet, but I think it’s worth considering adding them.
Thanks again!

page_elements = format_html_join(
"\n",
'<li class="inline-block">{page_element}</li>',
({"page_element": self.pagination_number(pagination, page_num)} for page_num in pagination.page_range)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following is just my personal preference - when writing components I try to keep HTML and CSS inside the templates, so different languages mix as little as possible (if possible).

Here you are rendering a list of page numbers and you are rendering then as HTML, but still from within Python. I'd move that in place of the {{ page_elements }} on line 10 in pagination.html.

One way how that could be done could be to define data objects and then simply render each item based on the data object's state:

class PaginationItem(NamedTuple):
    kind: Literal["current", "ellipsis", "number"]
    text: str | None = None
    attrs: dict | None = None

@register("pagination")
class PaginationComponent(Component):
    template_file = "pagination.html"

    def pagination_number(self, pagination: Pagination, num: int | str) -> PaginationItem:
        if num == pagination.paginator.ELLIPSIS:
            return PaginationItem(kind="ellipsis", text=pagination.paginator.ELLIPSIS)
        elif num == pagination.page_num:
            return PaginationItem(kind="current", text=num)
        else:
            link = querystring(None, {**pagination.params, PAGE_VAR: num})
            return PaginationItem(
                kind="number",
                text=num,
                attrs={"href": link},
            )

    def get_template_data(self, args, kwargs, slots, context):
        page_elements =  self.pagination_number(pagination, page_num)} for page_num in pagination.page_range

        ...

        page_link_css = "py-[5px] px-[10px] border-1 border-transparent rounded-lg no-underline hover:border-base-orange-400"

        return {
            "pagination": pagination,
            "previous_page_link": previous_page_link,
            "next_page_link": next_page_link,
            "page_elements": page_elements,
            "page_link_css": page_link_css,
        }

And then we could move all the CSS and HTML to the template. We'd replace line 10 of the HTML template with following:

{% for page_item in page_elements %}
  <li class="inline-block">
    {% if page_item.kind == "ellipsis" %}
        {{ page_item.text }}
    {% if page_item.kind == "current" %}
        <a
            href=""
            class="{{ page_link_css }} bg-base-orange-400 text-base-white-400"
            aria-current="page"
        >
            {{ page_item.text }}
        </a>
    {% else %}
        <a
            {% html_attrs
                page_item.attrs
                class=page_link_css
            %}
        >
            {{ page_item.text }}
        </a>
    {% endif %}
  </li>
{% endfor %}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.
The approach you suggested really feels like it maximizes the strengths of using components.
pagination_number method determines the attributes of each pagination item,
and leaving the actual rendering to the HTML makes the structure much cleaner and better overall 🙌

@@ -12,6 +12,6 @@
{% endfor %}
</ul>

{% pagination pagination %}
{% component "pagination" pagination_obj=pagination %}{% endcomponent %}
Copy link

@JuroOravec JuroOravec Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no content between the {% component %} and {% endcomponent %} tags, we could shorten it with self-closing /:

{% component "pagination" pagination_obj=pagination / %}

Same applies also to other files where this component was used.

@JuroOravec
Copy link

@Antoliny0919 The use of django-components looks good to me, left a few suggestions :)

Can't comment on the PR from the perspective of the djangosnippets project, someone else will have to take a look.

@Antoliny0919
Copy link
Collaborator Author

Thank you for taking the time to review this despite your busy schedule @JuroOravec ⭐️
Your suggestions have been a great help in guiding the direction I should take for future implementations using django-components.

@Antoliny0919 Antoliny0919 force-pushed the refactor_pagination_component branch 4 times, most recently from 9ba9b18 to ca8dba5 Compare July 12, 2025 06:48
@Antoliny0919 Antoliny0919 marked this pull request as ready for review July 12, 2025 06:50
@Antoliny0919 Antoliny0919 changed the title Refactor convert pagination to django-components structure. Refactored convert pagination to django-components structure. Jul 12, 2025
@Antoliny0919 Antoliny0919 force-pushed the refactor_pagination_component branch from ca8dba5 to 2344ab2 Compare July 12, 2025 07:08
@Antoliny0919
Copy link
Collaborator Author

Antoliny0919 commented Jul 12, 2025

I think the other PRs should be merged first, and this PR should be merged last 🙌

@chriswedgwood
Copy link
Collaborator

Great work!

@chriswedgwood
Copy link
Collaborator

chriswedgwood commented Jul 12, 2025

image we are getting this in heroku

Its from one of the prs I just merged

@Antoliny0919 Antoliny0919 force-pushed the refactor_pagination_component branch from 2344ab2 to 41811f1 Compare July 12, 2025 08:12
@Antoliny0919
Copy link
Collaborator Author

image we are getting this in heroku

Its from one of the prs I just merged

Is it possible to test the Heroku environment locally?

That said, even though I don't know the exact cause, I think it's safe to remove this part now, since the base TEMPLATES configuration has been updated.

# Use the cached template loader.
del TEMPLATES[0]["APP_DIRS"]
TEMPLATES[0]["OPTIONS"]["loaders"] = (
    (
        "django.template.loaders.cached.Loader",
        (
            "django.template.loaders.filesystem.Loader",
            "django.template.loaders.app_directories.Loader",
        ),
    ),
)

@Antoliny0919
Copy link
Collaborator Author

I suspect it might be because APP_DIRS was already removed in the base TEMPLATES configuration, and then it's being removed again in the production settings. 🧐

@Antoliny0919 Antoliny0919 force-pushed the refactor_pagination_component branch from 41811f1 to 09f543e Compare July 12, 2025 08:21
@Antoliny0919
Copy link
Collaborator Author

@chriswedgwood
Could you please check it one more time? If the issue still occurs, please let me know.

@chriswedgwood chriswedgwood merged commit 40a2459 into django:main Jul 19, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants