Getting started with Ajax Pagination & AlpineJs

I recently decided to go down a rabbit hole of wanting to learn a new client side library. I was interested in learning more about libraries that aimed to have a minimal footprint even at the cost of providing a more modest API. For this site I have simple requirements, and I wanted to see how simple the ‘simple’ abstractions are these days.

I eventually found my way to Alpine.js and was drawn in by the HTML attribute driven approach. If combined with well structured server rendered templates, you could very easily emulate the behaviour of ‘react’ but in a server rendered context. As I dug into the documentation, the various interaction focused built-ins like x-show, and x-on would make basic UI interactions easy to build, while x-for, and x-if give the foundation for conditional rendering and loops. Finally, x-data is where Alpine became very interesting to me. A simple example of x-data looks like:

Show Plain Text
  1. <div x-data="{ open: false }">
  2.     <div x-show="open">Hide me</div>
  3.     <span x-on:click="show = !show">Toggle</span>
  4. </div>

This is pretty easy to digest and lets you build simple ‘components’ that have scoped state. State defined in an x-data attribute is available to child nodes, and can be mutated with x-on:click which executes a snippet of code through an onClick handler. Using expressions and logic in HTML attributes is pretty convenient for simple state transitions, but it does have a few drawbacks:

  • Logic embedded in attributes relies on dynamic code execution from HTML which makes it incompatible with strict CSP configurations.
  • Logic in attributes can get messy, hard to maintain and even harder to debug as you can’t easily set breakpoints.

Alpine components

The Alpinejs folks are open about these limitations and have built-in an escape hatch. The escape hatch for improving CSP is also one of the more powerful features of AlpineJs in my opinion. The contents of x-data let you refer to a registered ‘data provider’ that Alpine will call for you. A data provider defines the starting state of a component, and can define all the event handler required for the component. To bind our component to our HTML we use:

Show Plain Text
  1. <div x-data="pagination">
  2.   ...
  3. </div>

The attribute value here defines the provider we want Alpine to call. Data providers are registered with Alpine and can be defined in simple JavaScript files, or script tags in your HTML. For example, I use Ajax pagination for comments on my posts, the code for that looks like:

Show Plain Text
  1. // Attach our provider when alpine boots up.
  2. document.addEventListener('alpine:init', function () {
  3.'pagination', function () {
  4.     // this.$el uses one of Alpines 'magic' attributes.
  5.     // $el refers to the current element scope. In this
  6.     // case it is the element where x-data is used.
  7.     var element = this.$el;
  9.     return {
  10.       // Data objects can have state
  11.       // This one is used in a show + transisition directive.
  12.       visible: true,
  14.       // Data objects can also define event handlers.
  15.       handleClick(event) {
  16.         event.stopPropagation();
  17.         event.preventDefault();
  19.         this.visible = false;
  20.         // Here the $el is the pagination link where x-on is used.
  21.         var url = this.$el.getAttribute('href');
  23.         fetch(url, {
  24.           headers: {'X-Requested-With': 'XMLHttpRequest'},
  25.         }).then(function (res) {
  26.           return res.text();
  27.         }).then(function (text) {
  28.           element.innerHTML = text;
  29.           this.visible = true;
  30.         }).catch(function () {
  31.           // Reset so we can try again.
  32.           this.visible = true;
  33.         });
  34.       },
  35.     }
  36.   });
  37. });

This module gives a simple AJAX pagination ‘component’ that can be applied to CakePHP’s pagination link generation by setting templates:

Show Plain Text
  1. // In config/paginator-templates.php
  2. return [
  3.     'nextActive' => '<li class="next"><a rel="next" href="{{url}}" x-on:click="handleClick">{{text}}</a></li>',
  4.     'prevActive' => '<li class="prev"><a rel="prev" href="{{url}}" x-on:click="handleClick">{{text}}</a></li>',
  5.     'number' => '<li><a href="{{url}}" x-on:click="handleClick">{{text}}</a></li>',
  6. ];
  8. // In Element/comments_block.php
  9. ?>
  10. <div id="comment-list" x-data="pagination">
  11.     <div x-show="visible" x-transition.opacity>
  12.     <?php
  13.     foreach($comments as $comment):
  14.         echo $this->element('comments/comment', array(
  15.             'comment' => $comment,
  16.             'author' => $post->user_id
  17.         ));
  18.     endforeach;
  19.     $this->Paginator->options(array(
  20.         'url' => array('controller' => 'Comments', 'action' => 'postComments', $post->id),
  21.     ));
  22.     echo $this->element('paging');
  23.     ?>
  24.     </div>
  25. </div>

These templates define the next, previous and page number links. In them we can see how we bind the behaviour of our Alpine component into our server rendered HTML. This is where the combination of programming models can create tension. Because my server side rendering framework wants to load templates from configuration data, parts of my template logic ends up very far away from their containers. This of course will vary based on how your server side logic is structured, and several other behaviours in my site are nicely co-located.

Being able to define ‘components’ in their own files helped with debugging as I could set breakpoints, and also played well with my existing asset pipeline for CakePHP . Not needing to use a more complex build tool like webpack or vite was another benefit of using Alpine.

So that’s all there was to it. With this setup, I’ve been able to model each rich interaction as an Alpine ‘component’ and then use those components in my server rendered HTML.

Was it worth it?

Previously my site was using an old version of jQuery 3. The total size for jQuery and my site was around 55KB. With Alpine, the total size is 17KB. This is a fantastic improvement, and I find the code I wrote in my components was succinct and simple. There isn’t much overhead and complexity, and because the browser APIs are much better these days I didn’t need any other libraries. Overall, I enjoyed the experience of learning Alpine. The documentation is excellent and I was able to google up any questions or problems I encountered and got useful directions. While I didn’t end up needing to use even half the features of Alpine to meet my requirements. I would use Alpine again for similar shaped sites in the future as well. I’m really happy that there are folks in the JavaScript community providing solutions for this space as it runs totally opposite to what we often hear about on social media.


Thanks for the write up! I’d been considering using Alpine for a new Cake 4 project but was having a hard time digesting anything beyond simple logic in attributes. I wasn’t aware how seamless Alpine components could be, so your example was really helpful. This led me to integrating Alpine with a build system (parcel) and writing Alpine components in Typescript. I’m really happy with the setup so far, and it’s reduced and simplified my existing framework-less Typescript.

Gregory on 3/31/23

Gregory: Sounds awesome! I’m planning on building a project with hotwire and stimulus next, and would love to learn more about how you’re using parcel.

mark story on 5/31/23

La claridad con la que presentas tus ideas en tu blog es impresionante.

Haces que incluso los conceptos más complejos sean comprensibles.
¡Un aplauso por tu habilidad comunicativa! brochas
beili – abrazaderas isofonica

Linette on 2/13/24

Have your say: