Skip to content

Add new UX Pagination component - #3753

Open
smnandre wants to merge 6 commits into
symfony:3.xfrom
smnandre:sa/ux-pagination
Open

Add new UX Pagination component#3753
smnandre wants to merge 6 commits into
symfony:3.xfrom
smnandre:sa/ux-pagination

Conversation

@smnandre

@smnandre smnandre commented Aug 5, 2026

Copy link
Copy Markdown
Member
Q A
Bug fix? no
New feature? yes
Deprecations? no
Documentation? yes
Issues Fix #...
License MIT

This PR adds symfony/ux-pagination, an experimental bundle that provides
cursor and numbered pagination behind one request-aware PaginatorInterface
service.

PHP owns the query. Twig renders accessible navigation. The browser
follows ordinary links: no JavaScript, no Stimulus, no Turbo required.

$pagination = $paginator
    ->cursor($events->createQueryBuilder('event'))
    ->orderBy(['createdAt', 'id'], 'DESC')
    ->perPage(20)
    ->paginate();
{% for event in pagination %}
    <article>{{ event.name }}</article>
{% endfor %}

{{ ux_pagination(pagination) }}

What is included

  • One PaginatorInterface entry point: paginate(), query(), cursor(),
    and fromCallbacks(), plus named paginator profiles injectable by
    argument name, #[Target], or service id.
  • Three strategies: numbered, lookahead (no COUNT query), and bidirectional
    cursor.
  • Adapters for arrays, Doctrine ORM 3, Doctrine DBAL 4.4+, and callbacks.
    Custom adapters register through one interface and a service tag.
  • Immutable builders for page size, ordering, and URL composition: route,
    path, query-string policy, fragment, and lazy application-provided totals.
  • The ux_pagination() Twig function and the <twig:ux:pagination>
    component, with default, Bootstrap, and Tailwind themes, structural
    blocks, validated attribute hooks, and accessible markup: nav landmark,
    aria-current, rel prev/next, translated labels.
  • Translations in ten locales. Message ids are natural English sentences,
    like core validators: without symfony/translation, the identity
    translator renders readable English, %count% pluralization included.
  • JSON serialization with stable prev/next links.
  • Optional LiveComponent integration through ComponentWithPaginationTrait:
    one createPagination() method to implement, real-link fallback, URL
    synchronization that follows the configured page parameter (path mapping
    included), and link URLs that stay on the page route during re-renders,
    never on the internal component endpoint.
  • Test\PaginatorFactory for deterministic application tests, including
    signed-cursor round trips.

Design decisions reviewers may want to check

  • Experimental: the bundle ships under the usual UX experimental notice.
  • count($pagination) returns the items on the current page, not the
    total. This matches iteration; getTotalItems() returns the total. The
    method documents it.
  • The cursor signing secret defaults to %kernel.secret%, like
    LiveComponent. Offset pagination works without any secret: the codec is a
    lazy proxy that fails only when signing is needed.
  • The result interfaces stay small. Serialization shapes (getMetadata(),
    getLinks()) and URL conveniences live on the concrete classes only.
  • Cursor tokens are signed, not encrypted: boundary values are readable.

@carsonbot carsonbot added Documentation Improvements or additions to documentation Feature New Feature Status: Needs Review Needs to be reviewed labels Aug 5, 2026
@carsonbot carsonbot changed the title Add new UX Pagination component Add new UX Pagination component Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📊 Packages dist files size difference

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

FileBefore (Size / Gzip)After (Size / Gzip)
Pagination
style.min.css Added 1.86 kB / 626 B

@Kocal Kocal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Except minor comments and types-hardening suggestions, it looks fine to me. Symfony will finally have built-in Pagination :D

Comment thread src/Pagination/assets/package.json Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sshipping default styles is indeed super useful, but I'm a little worried about what will happen when users want to make more significant changes to styles. What if users use CSS layers, etc..

This is also a subject of BC.

I'm not sure if we should ship default styles like that or not, what about moving it in a dedicated Flex recipe?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Having one is like having a default theme, it allows to work out of the box, but this is clearly documented to be something user would need/want to override or probably even rewrite themselves...

Here it's really about "plug and play and basic working behaviour".

Problem with a recipe is also it cannot differenciate "first install" and update, so it would bring the same problems (if not worst, as we cannot control we does or does not its recipe:updates out there)...

Maybe though this is something we can solve later on, providing URL / commands to get theme from ux toolkit or something ?

Comment thread src/Pagination/config/services.php Outdated
Comment thread src/Pagination/config/services.php Outdated
Comment on lines +88 to +99
.. code-block:: text
Does the UI need page numbers or an exact total?
|
+ -> yes -> offset
|
+ -> no -> Can rows be inserted or removed while users browse?
|
+ -> yes -> cursor
|
+ -> no -> lookahead

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does symfony.com documentation supports diagram? Like mermaid syntax?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You really should watch the repository I suggested you to watch 😆

Comment thread src/Pagination/CHANGELOG.md Outdated
Comment thread src/Pagination/CHANGELOG.md Outdated
Comment thread splitsh.json
"ux-leaflet-map": "src/Map/src/Bridge/Leaflet",
"ux-native": "src/Native",
"ux-notify": "src/Notify",
"ux-pagination": "src/Pagination",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remember to ping Fabien for creating the repo on GitHub/Packagist

Comment thread src/Pagination/src/NumberedPaginationInterface.php
Comment thread src/Pagination/src/NumberedPaginationInterface.php
@seb-jean

seb-jean commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

In the three templates under src/Pagination/templates/theme/*.html.twig, some variables are written in camelCase (linkClass, userClass, rootAttributes, userNavigationClass and navigationAttributes) These should use snake_case instead (link_class, user_class, root_attributes, user_navigation_class, navigation_attributes), to stay consistent with the usual Twig naming convention : https://twig.symfony.com/doc/3.x/coding_standards.html.

Example:

{% set linkClass = 'px-3 py-2 text-sm font-medium rounded-lg text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-2 focus:ring-blue-500 transition-colors duration-150' %}
{% set userClass = attributes.class|default('') %}
{% set rootAttributes = {
    'aria-label': 'Pagination'|trans({}, 'UXPaginationBundle'),
}|merge(attributes)|merge({
    class: ('ux-pagination-tailwind ' ~ userClass)|trim,
}) %}
{% set userNavigationClass = navigationAttributes.class|default('') %}
{% set navigationAttributes = navigationAttributes|merge({
    class: ('flex flex-wrap items-center justify-center gap-1 sm:gap-2 ' ~ userNavigationClass)|trim,
}) %}

`jq -r '.peerDependencies | keys[]'` errors out with exit code 5 when the
key is absent, and the step runs under `bash -e`. Every published package so
far ships a Stimulus controller, so all of them declare at least one peer
dependency and the case never showed up.

Fall back to an empty object so packages with no peer dependency, such as a
CSS-only one, are skipped instead of killing the job.
@ker0x

ker0x commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Doctrine 3.7 will be release with a new CursorPaginator

@seb-jean

seb-jean commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Doctrine 3.7 will be release with a new CursorPaginator

Yes, but there's currently a blocking issue for the release of cursor-based pagination in Doctrine ORM 3.7.

@smnandre

Copy link
Copy Markdown
Member Author

Doctrine 3.7 will be release with a new CursorPaginator

Great, and as I see it some of this PR API inspired some changes on Doctrine PR.. and this is why I love Open Source ! 🤝

@smnandre
smnandre requested a review from kbond August 10, 2026 07:05

@Kocal Kocal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, waiting for your PR on ux.symfony.com :D

@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Aug 10, 2026
@smnandre

Copy link
Copy Markdown
Member Author

Thank you very much for the review @Kocal !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation Feature New Feature Status: Reviewed Has been reviewed by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants