Jump to content

Infinite Scroll without a single line of Javascript


gebeer
 Share

Recommended Posts

Hello all,

I wanted to share a proof of concept approach on how we can have an infinite scroll pattern without writing Javascript. We can achieve this with htmx.

What you will get

An overview page with a list of posts (PW pages). On initial page load 5 posts will be shown. When scrolling down and reaching the last post, another 5 posts will be loaded via AJAX in the background and appended after the last post until no more pages are found.

Prerequisites

You are using the delayed output strategy, with a _main.php appended. Just like using the default site profile when installing ProcessWire

You are using markup regions, in my _main.php I have a div#content that will be used for the output of the posts

Inside site/config.php

$config->useMarkupRegions = true;

Inside site/templates/_main.php

		<!-- main content -->
		<div id='content'>
		</div>

For script loading I am using a custom $config->bodyScripts FilenameArray

Inside site/config.php

$config->bodyScripts = new FilenameArray();

Inside site/templates/_main.php before the closing </body> tag

	<?php foreach ($config->bodyScripts as $file) : ?>
		<script src="<?= $file ?>"></script>
	<?php endforeach ?>
</body>

PW page structure for this tutorial

In the page tree I have a parent page "Posts" with template "posts". All child pages of that page have template "post"

posts-page-tree.png.0a56ab24fb8ca882f690734074fc3c84.png

In the template "posts" settings in the "URLs" tab, check "Allow Page Numbers" and save. Needed for pagination.

template-posts-settings.thumb.png.4ef1927338b4af67e4c73f7cd27c17cd.png

When viewing the page "Posts" all logic happens inside site/templates/posts.php

site/templates/posts.php

<?php

namespace ProcessWire;

// posts.php template file 

// add htmx js from site/templates/scripts
$config->bodyScripts->add($config->urls->templates . 'scripts/htmx.min.js');

$limit = 5;
$posts = $pages->find("template=post, limit={$limit}");
$lastPost = $posts->last();
$nextPageUrl = $page->url . $input->pageNumStr((int) $input->pageNum() + 1);
$hxAttributes = array();
$hxAttributes[] = 'hx-get="' . $nextPageUrl . '"';
$hxAttributes[] = 'hx-trigger="revealed"';
$hxAttributes[] = 'hx-swap="afterend"';
?>
<?php if (!$config->ajax) : ?>
    <section pw-append="content" class="posts" hx-headers='{"X-Requested-With": "XMLHttpRequest"}'>
    <?php endif ?>
    <?php foreach ($posts as $post) : ?>
        <article class="post" <?php if ($post == $lastPost) echo implode(' ', $hxAttributes)  ?>>
            <header>
                <h3><?= $post->title ?></h3>
            </header>
        </article>
    <?php endforeach ?>
    <?php if ($config->ajax) return $this->halt() ?>
    <?php if (!$config->ajax) : ?>
    </section>
<?php endif ?>

And that is all there is to it. Not a single line of Javascript, thanks to htmx. I followed the infinite scroll pattern from the official htmx examples.

Now let's break the code down into easily digestible chunks

// add htmx js from site/templates/scripts
$config->bodyScripts->add($config->urls->templates . 'scripts/htmx.min.js');

This adds site/templates/scripts/htmx.min.js to our custom $config->bodyScripts FilenameArray so it will be loaded in _main.php. You can get the script here from unpkg.com.

$limit = 5;
$posts = $pages->find("template=post, limit={$limit}");

Sets our pagination limit to 5 and loads the correct set of posts.

$lastPost = $posts->last();

Saves the last post of each set. We use this later to determine whether the htmx attributes should be rendered.

$nextPageUrl = $page->url . $input->pageNumStr((int) $input->pageNum() + 1);

 We are building the link to the next "page" with the next set of posts. Will result in something like "/posts/page2", "/posts/page3" etc.

$hxAttributes = array();
$hxAttributes[] = 'hx-get="' . $nextPageUrl . '"';
$hxAttributes[] = 'hx-trigger="revealed"';
$hxAttributes[] = 'hx-swap="afterend"';

Define our htmx attributes as an array. They will be added to every last post's HTML. Note the hx-get attribute which will be the URL for the AJAX call in the background. That request is triggered whenever the last post becomes visible inside the viewport while scrolling down. hx-swap afterend tells htmx to append the next batch of posts after the last post.

<?php if (!$config->ajax) : ?>
    <section pw-append="content" class="posts" hx-headers='{"X-Requested-With": "XMLHttpRequest"}'>
    <?php endif ?>
// and
<?php if (!$config->ajax) : ?>
    </section>
<?php endif ?>

Renders the wrapping section tag only on initial page load which is a none AJAX request. Note the hx-headers='{"X-Requested-With": "XMLHttpRequest"}'. This adds an additional header to all AJAX requests with htmx. We need this header so ProcessWire understands that it is an AJAX request. Otherwise $config->ajax would always return false. See https://htmx.org/attributes/hx-headers/ for more info

    <?php foreach ($posts as $post) : ?>
        <article class="post" <?php if ($post == $lastPost) echo implode(' ', $hxAttributes)  ?>>
            <header>
                <h3><?= $post->title ?></h3>
            </header>
        </article>
    <?php endforeach ?>

Render each posts's HTML. If it is the last post, also render our htmx attributes. For brevity in this example I only output the post title.

<?php if ($config->ajax) return $this->halt() ?>

For AJAX requests stop execution of the template file and everything that follows. This prevents appending of _main.php for ajax calls. So we only get the desired HTML for the list of posts and no head, footer etc.

Summary

Compared to other approaches, htmx lets us control all our AJAX logic with a few html attributes. Really neat and concise. Easypeasy. I like that and will surely use an approach like this in future when infinite scroll is needed. What I like in particular is how easy this is implemented with ProcessWire's powerful pagination capabilities.

If you have the same page structure, the code in site/templates/posts.php is working out of the box as is. I have this running on a standard PW multilang site profile with additions/amendments mentioned above under "Prerequisites".

Here's a visual of the result:

infscroll.gif.3d3eba561f8d4eb16f6d4c2ecc5ad50f.gif

  • Like 22
  • Thanks 2
Link to comment
Share on other sites

Thanx @gebeer for sharing!

As a side note, anyone implementing infinite scroll, be aware that a simple and basic implementation can lead to very bad UX, see for example: https://builtin.com/ux-design/infinite-scroll

So it is a lot of work to implement it from scratch if one wants to provide something that is not pita for the user.

  • Like 2
Link to comment
Share on other sites

Very interesting/cool, but 'Htmx is a dependency-free, browser-oriented javascript library'. Is this really more efficient than jQuery or plain js? Does this leverage ProcessWire; is this the best PW-native solution?

I have implemented this solution here in my sites and have to move on to other things to finish before the new year. I'll keep an eye on Htmx - it's The Future apparently... - and maybe implement this next Christmas. 😉


 

Link to comment
Share on other sites

13 hours ago, szabesz said:

Thanx @gebeer for sharing!

As a side note, anyone implementing infinite scroll, be aware that a simple and basic implementation can lead to very bad UX, see for example: https://builtin.com/ux-design/infinite-scroll

So it is a lot of work to implement it from scratch if one wants to provide something that is not pita for the user.

You are making a good and important point here. My short tutorial serves as a proof of concept like stated in the introduction. For a full fledged implementation UX has to be taken in account.

To improve upon my example, we can add browser history support by adding the hx-push-url attribute. So the hxAttributes part of my code would look like

$hxAttributes = array();
$hxAttributes[] = 'hx-get="' . $nextPageUrl . '"';
$hxAttributes[] = 'hx-push-url="' . $nextPageUrl . '"';
$hxAttributes[] = 'hx-trigger="revealed"';
$hxAttributes[] = 'hx-swap="afterend"';

Now if a user follows a link to a single post and then returns to the overview via browser back button, they'd land on the same page they left off. To make this behaviour smarter and have the previous pages (batches) load on scrolling up, we could expand the whole logic by placing htmx attributes on the first post of every page (batch) with hx-swap="beforebegin". If a user came to /posts/page6 through a shared link, they could scroll up until they reach /posts/page1.

10 hours ago, modifiedcontent said:

but 'Htmx is a dependency-free, browser-oriented javascript library'. Is this really more efficient than jQuery or plain js?

IMHO you cannot compare htmx to jQuery since they serve completely different purposes.

11 hours ago, modifiedcontent said:

Does this leverage ProcessWire;

I think it leverages PW pagination capabilities quite well.

11 hours ago, modifiedcontent said:

is this the best PW-native solution?

I don't think there is a "best" solution for this use case. It depends very much on the context. It may fit well with some projects and might not be a good solution for others.

  • Like 3
Link to comment
Share on other sites

8 hours ago, gebeer said:

Now if a user follows a link to a single post and then returns to the overview via browser back button, they'd land on the same page they left off. To make this behaviour smarter and have the previous pages (batches) load on scrolling up, we could expand the whole logic by placing htmx attributes on the first post of every page (batch) with hx-swap="beforebegin". If a user came to /posts/page6 through a shared link, they could scroll up until they reach /posts/page1.

Still implementing pages is essential, I think. Thanks for the addition.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...