Jump to content

Continuous Scroll - Recommended Approach?


Gazley
 Share

Recommended Posts

What you need to do is basically:

  • Get a list of pages with limit for pagination $pages->find('template=post, limit=10, sort=-published');
  • Render a list of items
  • Render the pagination at the bottom of the page
  • Using JS listen to scroll event, and trigger AJAX request
  • Get the response, filter the list of elements, replace the container that holds the old items
  • Repeat

As a different approach, you can implement barba.js for PJAX (ajax with history support) (and limit it to only pagination links, if you prefer), and trigger click() on the next pagination link when it gets closer to viewport. For that you can use scrollMonitor.js. Total weight of these two scripts are under 15KB, they don't add much load on the page.

One bonus is that you can enable barba.js on all (internal) links (it's enabled by default) to make transitions between pages feel more responsive.

  • Like 2
Link to comment
Share on other sites

5 minutes ago, Gazley said:

Hi @DaveP - thanks so much for pointing me towards Intercooler! It looks awesome and I hadn't previously heard of it :) 

 

No problem. I've used it quite a bit, although not for the kind of stuff you're talking about at the moment. For anyone like me who's a bit sketchy on plain js, it's a great help and very powerful. It makes doing ajaxy search boxes and such an absolute doddle, cos PW plays real nice with ajax requests.

  • Like 1
Link to comment
Share on other sites

  • 5 months later...

Is there no way to do this without plugins?

I am trying, but can't get even the most basic Ajax to work. I am trying to jQuery load() a test.php from an /inc folder in templates, but keep getting file not found.

Is there something in Processwire that prevents getting template parts via Javascript?

When I put 'test.php' outside of Processwire, in the root of the site, including it with jQuery load() is no problem.

Link to comment
Share on other sites

Thanks szabesz. I had seen some info about that and have made a "loadmore" template and set up some kind of page.

But how should I configure that page? I have it hidden etc., but you still have to publish it, right? Will it behave exactly the same as a plain php inc template part? Or do I have to do other things to prevent it from being parsed and filtered etc.?

What is the mysterious $config->ajax; ?

  • Like 1
Link to comment
Share on other sites

43 minutes ago, modifiedcontent said:

What is the mysterious $config->ajax; ?

It can be used out of the box when jQuery is used for AJAX, more: https://processwire.com/talk/topic/15809-sessions-db-http404/?do=findComment&comment=141119

Hidden pages are still accessible via their URL so you can use it if your page template file handling the AJAX request is not used for displaying anything else. Unpublished is not accessible (except for logged in superusers) so you need to publish it.

43 minutes ago, modifiedcontent said:

Will it behave exactly the same as a plain php inc template part?

A page can be dedicated solely to be an AJAX call processor if you wish to, just check for the sign of an AJAX call, echo your output and use return $this->halt(); to stop the process. Since such a setup is a normal page request it behaves like one, so that is why "ifs" are needed.

Here is Ryan's post about the basics: https://processwire.com/talk/topic/225-how-to-work-with-ajax-driven-content-in-processwire/

43 minutes ago, modifiedcontent said:

Or do I have to do other things to prevent it from being parsed and filtered etc.?

If you redirect to 404 if it is not an AJAX call, it should be enough to make sure such a page is practically nonexistent for the outside world.

 

 

Edited by szabesz
typos
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

  • 4 years later...

Returning to this after 5 years, one of my annual Christmas/New Year web development projects. I think I got something working 5 years ago, but it was clunky and too confusing to implement throughout my sites.

What is the recommended approach for 2023?

Wish list:

Infinite scroll - within reason, turning into a 'More' button after the first dozen items

Lazy loading

Not using plugins/modules

...

 

Should I start with pagination? Or some other built-in feature to creates an array of items on page load, ready to be added to the page as you scroll down? The clunky first attempt called items by batch based on the latest added item, I think.

I'll add my solutions here as I find them, using this thread to keep track of what I'm doing. Any tips/suggestions appreciated.

  • Like 1
Link to comment
Share on other sites

4 hours ago, modifiedcontent said:

Should I start with pagination?

I'd start with what we all love PW for: The API.

So I'd ask myself: What would I want it to work (without thinking about the consequences and the work that has to be done to make it work like that).

And I'd probably come up with something like this:

$newspages = $pages->find("template=newsitem, limit=12");

foreach($newspages as $p) ...

echo $newspages->renderPager();
echo $infiniteScroll->renderTag();

To make that work you'd need an $infiniteScroll module with a renderTag() method. That method would render a JS <script> tag that finds the previous sibling (https://gomakethings.com/how-to-get-the-next-and-previous-siblings-of-an-element-with-vanilla-js/#an-example) and detects what page we are on and once the script tag comes into view which next page it should request via ajax and inject into the DOM.

Then you'd only have to add the InfiniteScroll.js file to your <head> and maybe make the renderTag() configurable (like how many pages should be loaded before showing a "load more" button or to make the "load more" button markup configurable as well:

echo $infiniteScroll->renderTag([
	'pages' => 3, // auto-load 3 pages, then show the load-more button
	'button' => "<button class='uk-button uk-button-small'>load more</button>"
]);

That would be how I'd like to have it work. But I have to admit that I've never built an infinite scroll so I might be missing something obvious or important 🙂 

Good luck and have fun 🙂 

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

9 hours ago, modifiedcontent said:

Not using plugins/modules

 

Why do you have this requirement? JS plugins like https://infinite-scroll.com/ are there for a reason. So developers don't have to reinvent the wheel. Said plugin is well thought out and documented and makes it quite easy to achieve what you want. All you need is a container with your posts and a link to the next page. The rest is taken care of by the plugin. Of course, you could spend hours and write the JS by yourself. I'd rather honor desandro's work and give him 25 bucks for a dev license. Already done it because I needed it for a custom template of a WP site some years ago.
If you want to code the JS functionality yourself for whatever reason, you can certainly do that and and have fun with it. You should take browser history into account for usability reasons. The plugin does that.  

If you decided to use that plugin, setting up the markup and JS initialization could look somewhat like this (simplified example)

/** @var PaginatedArray $newspages */
$newspages = $pages->find("template=newsitem, limit=12");
/** @var string $nextPageUrl typically something like /newspage/page2 */
$nextPageUrl = $page->url . $input->pageNumStr((int) $input->pageNum() +1) ?>
<div class="infinitescroll">
<?php foreach($newspages as $p) { ?>
    <article class="post">...</article>
<?php } ?>
</div>
<a href="<?= $nextPageUrl ?>" class="pagination__next">Next page</a>
<script>
  let elem = document.querySelector('.infinitescroll');
  let infScroll = new InfiniteScroll( elem, {
  // options
  path: 'newspage/<?= $config->pageNumUrlPrefix ?>{{#}}',
  append: '.post',
  history: false, // or true
});
</script>

I have not tested this example code but it should get you started.

  • Like 3
Link to comment
Share on other sites

Interesting read: https://www.nngroup.com/articles/infinite-scrolling-tips/

Quote

Infinite scrolling typically works best for situations where users will want to scroll through homogeneous items with no particular task or goal in mind — for example, entertainment, news, or social media. 

Quote

Because of the usability issues that infinite scrolling can cause, we do not recommend it when users will want to use the listing page’s content to:

  • Find something specific (e.g., looking for a specific article or item)
  • Compare items in a long list (e.g., choose between several products that might be very far apart in the list)
  • Inspect only a few items at the top of the list (e.g., pick the best search results)
  • Infinite scrolling is also not a good fit if you have a large user group from areas with low bandwidth or if your website is visited frequently by users with accessibility needs.

 

  • Like 4
Link to comment
Share on other sites

Thanks @bernhard and @gebeer, that is excellent input for me to start thinking through the options. 

@gebeer, the 'infinite scroll' solution seems to presume that you already have a pagination system, with next pages etc., that you then convert into infinite scroll. Is that correct? Do I first have to set up pagination in Processwire - PaginatedArray etc.?

I want to avoid/bypass the messiness of pagination, have a continuous scroll system instead that just adds items, keeping everything on one page, in combination with a 'more button' and a search field, with advanced search and sorting options where necessary.

I see renderPager() from @bernhard's suggestion is also part of PW's pagination system. Is it unavoidable? Looking into it...

My clunky solution 5 years ago was based on ID's in the items and then adding the next one, like this, via Ajax - still searching for the other bits and pieces in my archives...:

 

Spoiler
<?php

$lastpost_id = $_GET ['lastpost_id']; // from the url to the loadcontent script

$action = $_GET ['action'];
if($action <> 'get') {

$limit = '1';

$topic = $pages->get("template=topic, title=$page->title"); 
$videos = $pages->find("template=video, topic=$topic, date<$lastpost_id, limit=$limit, sort=-date");

$lastpost_id = ''; // reset back to 0

foreach ( $videos as $video ) { 

$date= date( 'F Y', $video->getUnformatted('date') );

echo '<div id='. $video->getUnformatted('date') .' class=post><a href='. $video->url .'><h3>'. $video->title .'</h3></a>';

videoembed($video->video);

echo '<p>'. $video->summary .' <span style="opacity: 0.3">  '. $date . ' </span></p></div>';

};

};

?>

 

 

This worked in combination with this jquery:

 

Spoiler
var jLoad = jQuery.noConflict(); 

jLoad(document).ready(function(){
	jLoad(window).on('scroll', function() {
	
	if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
	
    	jLoad('#moreloader').html('<img src=<?php echo $config->urls->templates ?>/img/loader.png>');
		
		var lastpost_id = '';
		var lastpost_id = jLoad('.post:last').attr('id');

		jLoad.ajax({
			url: '?lastpost_id=' + jLoad('.post:last').attr('id'),
			error: function(html) {
				jLoad('#moreloader').remove();
				},
			success: function(html) {
				if( html && lastpost_id == jLoad('.post:last').attr('id') ) {
					jLoad('#posts').append(html);
					jLoad('#moreloader').html('more');
					} 
				},
			timeout: 7000 // sets timeout to 7 seconds
		});
		
	}
		
    });
});

 

 

Link to comment
Share on other sites

2 minutes ago, modifiedcontent said:

I see renderPager() from @bernhard's suggestion is also part of PW's pagination system. 

That has two reasons:

  1. SEO --> bots that do not understand JS will see the plain old paginated site (so every content is accessible)
  2. efficiency --> ProcessWire already does the pagination out of the box, so I'd build on that rather than implementing my own logic
  • Like 1
Link to comment
Share on other sites

Thanks again @bernhard.

I don't care about the SEO argument; those paginated nextpages should not exist. All content will have its own page and those pages will be reachable via search and limited continuous scrolling on index pages.

Efficiency; good point. I guess PW's pagination solution takes care of caching and limiting database calls etc.? I have always avoided pagination, so will have to do some homework... 

This is probably what I'm looking for; continuous scroll (and load more button) with items from a array, not pagination. This example includes a lot of stuff at once... - https://codeblock.co.za/infinite-scroll-load-more-jquery-php/

I've downloaded the github files here and stripped out/dumbed down all the distracting css and fancy filtering, sorting, etc. to get a minimal working proof of concept that I can sort of understand and apply to a PW posts array later.

This works as a demo:

index.php

Spoiler
<!Doctype html>
<html>

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo</title>
    
    <script  src="https://code.jquery.com/jquery-3.5.1.min.js"></script>

    <script src="js.js"  type=" text/javascript"></script>

</head>

<body>
     <main class=container>
     <input type=hidden id=current-query value="" />
     <div id=all-products></div>
     </main>
     
</body>

</html>

 

js.js

Spoiler
var limit = 2;
var offset = 0;
var noMoreProducts = false;
var loadingInProgress = false;

function loadProducts() {
    
    if (noMoreProducts) return;
    
    let queries = {
        'offset' : offset,
        'limit' : limit
    };
    
    if (!loadingInProgress) {
        
        loadingInProgress = true;
        
        $.get('products.php', queries, function(data) {
            
            if(!data) {
                noMoreProducts = true;
                $('#load-more').remove();
            }
            else {
                offset += limit;
                
                $('#load-more').remove();
                
                $('#all-products').append(data);
                
            }
            
            loadingInProgress = false;
            
        });
    }
    
}

/************************************************
 * Adds Load More Button Functionality
 * Exclude this if only infinite scroll is needed
*************************************************/
$(document).on('click', '#load-more', function() {
    
    let currentQuery = {};
    
    $('#load-more').html('Loading...').prop('disabled', true);
    
    if($('#current-query').val().length) {
        currentQuery = JSON.parse($('#current-query').val());
    }
    loadProducts( currentQuery );
    
    
});

/************************************************
 * Adds Infinite Scroll Functionality
 * Exclude this if only load more button is needed
*************************************************/

$(window).scroll(function() {
    
    if( ( $(window).scrollTop() + $(window).height() ) >= $(document).height() ) {
        
        let currentQuery = {};
    
        if( $('#current-query').val().length) {
            currentQuery = JSON.parse($('#current-query').val());
        }

        $('#load-more').html('Loading...').prop('disabled', true);
        
        loadProducts(currentQuery);
        
    }
});

$(document).ready(function() {
    loadProducts();
})

 

products.php

Spoiler
<?php

$products = array(
    	array(
    		'id' => '1',
    		'title' => 'Blue Shirt',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '2',
    		'title' => 'Green Shirt',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '3',
    		'title' => 'Yellow Shirt',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '4',
    		'title' => 'Cargo Pants',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '5',
    		'title' => 'Product 5',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '6',
    		'title' => 'Formal Shirt',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '7',
    		'title' => 'Peak Cap',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '8',
    		'title' => 'Beret',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '9',
    		'title' => 'Sleeveless Jacket',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),    	
    	array(
    		'id' => '10',
    		'title' => 'Windbreaker',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),    	
    	array(
    		'id' => '11',
    		'title' => 'Trousers',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '12',
    		'title' => 'Bomber Jacket',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '13',
    		'title' => 'Bomber Jacket Orange',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '14',
    		'title' => 'Black T-Shirt',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	),
    	array(
    		'id' => '15',
    		'title' => 'Beanie',
    		'image' => 'https://via.placeholder.com/500x300.png'
    	)
);


// Default limit
$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;

// Default offset
$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;

$queried_products = array_slice($products, $offset, $limit);

$total_products = count($products);

// Get the total rows rounded up the nearest whole number
$total_rows = (int)ceil( $total_products / $limit );

$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;


if (count($queried_products)) {
    foreach ($queried_products as $product) { ?>
    
       <h2><?php echo $product['title']; ?></h2>
       <img src="<?php echo $product['image']; ?>" />
           
     <?php }
}

else {
    return false;
}

if ( $current_row < $total_rows ) { ?>
    
    <div>
        <button id=load-more>Load more</button>
    </div>
    
<?php }
else {

    return false;
}

 


I'll try to apply this in a PW page with an items array tomorrow... 

Or do you see obvious problems with this approach?

 

Link to comment
Share on other sites

11 hours ago, modifiedcontent said:

the 'infinite scroll' solution seems to presume that you already have a pagination system, with next pages etc., that you then convert into infinite scroll. Is that correct? Do I first have to set up pagination in Processwire - PaginatedArray etc.?

For my approach you don't need full pagination, just one link to the next page, something like 'newspages/page2'. The JS plugin will take care of fetching that next page and then updating the link to read 'newspages/page3' etc. Look at my code sample how I build that initial link URL.

11 hours ago, modifiedcontent said:

I want to avoid/bypass the messiness of pagination, have a continuous scroll system instead that just adds items, keeping everything on one page, in combination with a 'more button' and a search field, with advanced search and sorting options where necessary.

I don't see anything messy about pagination links in general. But, like I said, you don't need them for my approach. What you describe is exactly what the infinitescroll plugin does, except for search/sort. You could implement that via a JS filter. But typically infinite scroll implementations do not need search/filtering. From a UX perspective that would be overkill. If you need search/filtering capabilities, infinite scroll might not be the best solution for your use case in the first place. Mind the link that @bernhardposted above.

11 hours ago, bernhard said:
  • SEO --> bots that do not understand JS will see the plain old paginated site (so every content is accessible)
  •  

In my example approach search engines have a link to the next batch of posts and can index on from there.

11 hours ago, modifiedcontent said:

I have always avoided pagination, so will have to do some homework... 

Recommended resources: https://processwire.com/docs/front-end/markup-pager-nav/ 

11 hours ago, modifiedcontent said:

Or do you see obvious problems with this approach?

Yes.

  • vistors without JS (search engine bots etc.) will not see any posts on initial page load 
  • client side scroll performance because of non-throttled $(window).scroll event listener. generally the applied scroll detecting technique seems outdated. https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API would be a better approach
  • usability: no bowser history (back/forward) button
  • jQuery dependency (not a real problem more a matter of preference)

You still have not stated why you don't want to use plugins. I don't know if taking some seemingly outdated code to build upon is a good approach for a solution in 2023. No offence intended, just my opinion.

  • Like 1
Link to comment
Share on other sites

Thanks again for the feedback, @gebeer. Even if I don't (immediately) agree with all your points, it is super helpful to focus my attention, research things, sort out my priorities, etc.

I avoid plugins because they have to be all things to all people. Plugins come with lots of built-in assumptions about how everybody does stuff. I prefer to have a minimal solution tailored to the needs of my site that I understand and can expand/improve as necessary.

I have decided I absolutely don't want any pagination, not even 'just one link to the next page'. It goes against the logic of my site; you can browse the content by topic, location, etc., each with their own index pages and then limited continuous scroll + a search box. No page1, page2, page3, etc.

I don't necessarily want search engine bots index anything other than the top pages by topic, location, etc. and the posts' own pages. It would still be good to have the first few posts of the post array in the page without requiring javascript; the first post would also be a "featured" post, styled differently etc. So I probably have to rewrite the scripts - any ideas appreciated.

Quote

I don't know if taking some seemingly outdated code to build upon is a good approach for a solution in 2023 ... visitors without JS ... jQuery dependency ... 

The infinite-scroll.com plugin is originally from 2008, the granddaddy of continuous scrolling. The codeblock.co.za demo is from June 2020.

Millennial developers have been claiming for the last ten years that everything will move to Javascript, with V8 in Chromium, React and all the other js frameworks. Is 'visitors without JS' still a serious issue? Or like nobody is developing for IE anymore. 

jQuery is the uncool old js framework, but isn't it still default in Processwire? I use jQuery everywhere in my templates. 

Quote

... client side scroll performance because of non-throttled $(window).scroll event listener. generally the applied scroll detecting technique seems outdated ...

Yes, I noticed that. Thanks for the pointer. I'll have to rewrite that later. Any other suggestions how to improve that much appreciated. 

The codeblock.co.za demo has a line like this:

        $.get('posts.php', queries, function(data) {

In my version I want to just get a $posts array instead of an external file. How should I rewrite that? Syntax etc.? Or is that not possible? What should I google for to find the solution? Searching now, but if anyone knows, please let me know.

This doesn't work. Does anyone spot the problem? Should I use `if($config->ajax) { ...` etc.? Using `jQuery.noConflict()` did not fix it, but I still leave that in, because it works fine in the other test version:

Spoiler
<?php include 'inc/head.php'; ?>

<body>

     <main class=container>
     <input type=hidden id=current-query value="" />
     <div id=all-posts></div>
     </main>

</body>

<script>
var jLoad = jQuery.noConflict(); 

var limit = 2;
var offset = 0;
var noMorePosts = false;
var loadingInProgress = false;

function loadPosts() {
    
    if (noMorePosts) return;
    
    let queries = {
        'offset' : offset,
        'limit' : limit
    };
    
    if (!loadingInProgress) {
        
        loadingInProgress = true;
        
        jLoad.get('<?php echo $config->urls->templates ?>posts.php', queries, function(data) {
            
            if(!data) {
                noMorePosts = true;
                jLoad('#load-more').remove();
            }
            else {
                offset += limit;
                
                jLoad('#load-more').remove();
                
                jLoad('#all-posts').append(data);
                
            }
            
            loadingInProgress = false;
            
        });
    }
    
}

/************************************************
 * Adds Load More Button Functionality
 * Exclude this if only infinite scroll is needed
*************************************************/
jLoad(document).on('click', '#load-more', function() {
    
    let currentQuery = {};
    
    jLoad('#load-more').html('Loading...').prop('disabled', true);
    
    if(jLoad('#current-query').val().length) {
        currentQuery = JSON.parse(jLoad('#current-query').val());
    }
    loadPosts( currentQuery );
    
    
});

/************************************************
 * Adds Infinite Scroll Functionality
 * Exclude this if only load more button is needed
*************************************************/

jLoad(window).scroll(function() {
    
    if( ( jLoad(window).scrollTop() + jLoad(window).height() ) >= jLoad(document).height() ) {
        
        let currentQuery = {};
    
        if( jLoad('#current-query').val().length) {
            currentQuery = JSON.parse(jLoad('#current-query').val());
        }

        jLoad('#load-more').html('Loading...').prop('disabled', true);
        
        loadPosts(currentQuery);
        
    }
});

jLoad(document).ready(function() {
    loadPosts();
})
</script>

 

 

posts.php looks like this:

Spoiler
<?php 

$posts = $pages->find("template=video, limit='12', sort=-date");

// Default limit
$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;

// Default offset
$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;

$queried_posts = array_slice($posts, $offset, $limit);

$total_posts = count($posts);

// Get the total rows rounded up the nearest whole number
$total_rows = (int)ceil( $total_posts / $limit );

$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;


if (count($queried_posts)) {
    foreach ($queried_posts as $post) { ?>

			<div class=cell id=<?php echo $post->getUnformatted('date'); ?>>
			<h2><?php echo $post->title; ?></h2>
			</div>
			           
     <?php }
}

else {
    return false;
}

if ( $current_row < $total_rows ) { ?>
    
    <div>
        <button id=load-more>Load more</button>
    </div>
    
<?php }
else {

    return false;
}

 


I can't figure out how to pull in that posts.php file into Javascript within PW; hardcoded paths that work fine in a folder outside of PW, like this, don't work within PW:

        jLoad.get('/myserverpath/posts.php', queries, function(data) {

 

Link to comment
Share on other sites

10 hours ago, modifiedcontent said:

The infinite-scroll.com plugin is originally from 2008, the granddaddy of continuous scrolling. The codeblock.co.za demo is from June 2020.

 

Latest version of https://github.com/metafizzy/infinite-scroll is from 2020. It uses modern web APIs like fetch, throttles scroll events and uses the https://developer.mozilla.org/en-US/docs/Web/API/DOMParser API, so it is quite efficient in what it does.

10 hours ago, modifiedcontent said:

I avoid plugins because they have to be all things to all people.

This is true in many cases, but not in all. Just because a plugin offers configuration options it does not necessarily mean that it is bloated. To me all the config options in infinite-scroll plugin do make sense. It focuses on the task that it is trying to solve, and does this in a quite concise way. If you take a look at the source you will see that it is well structured.

 

11 hours ago, modifiedcontent said:

The codeblock.co.za demo has a line like this:

        $.get('posts.php', queries, function(data) {

In my version I want to just get a $posts array instead of an external file. How should I rewrite that? Syntax etc.? Or is that not possible? What should I google for to find the solution? Searching now, but if anyone knows, please let me know.

This doesn't work. Does anyone spot the problem? Should I use `if($config->ajax) { ...` etc.? Using `jQuery.noConflict()` did not fix it, but I still leave that in, because it works fine in the other test version:

posts.php looks like this:


I can't figure out how to pull in that posts.php file into Javascript within PW; hardcoded paths that work fine in a folder outside of PW, like this, don't work within PW:

 

Lets say your URL to the index page is /newspage and the template is newspage.php. You can handle the AJAX requests in newspage.php inside an if($config->ajax) block

if($config->ajax) {
    // build your posts array or return the posts HTML
    return $this->halt; // stop execution
}

Then your AJAX GET request goes to the same page. To make a request to the same page you can use $.get('./', queries, ...) or use the $.ajax object and ommit the url parameter

$.ajax({
  type: "GET",
  // url: "index.php", ommit
  data: queries,
}).done(function (data) {
  //...
});

All the logic with $limit and $offset in your posts.php example is there to figure out which posts to return in pure PHP. This is commonly called "pagination". You are here because you are using our all beloved content management framework ProcessWire. And PW has it's own implementation of pagination and it does it in a very well thought out and efficient way. So why not use the tools that your framework provides? Namely https://processwire.com/api/ref/paginated-array/ . You are already building a PaginatedArray with

$posts = $pages->find("template=video, limit='12', sort=-date"); // $posts is an instance of PaginatedArray class because of limit in the selector string

With PW's excellent API documentation and the well documented methods of the PaginatedArray class you should be able to figure out how to return what you need inside the if($config->ajax) block. Take a look at the setLimit() and setStart() methods of the PaginatedArray class. So right there you have some more homework :-)

Of course you could do it with pure PHP but this would go beyond the scope of this forum. We shall be happy to help you doing it the ProcessWire way, though.

  • Like 1
Link to comment
Share on other sites

Thanks again @gebeer. I am now puzzling with `if($config->ajax) { ...` - I have used Ajax elsewhere in my PW sites, but only with POST form data, not GET. 

Yes, I would prefer to use ProcessWire built-in methods. As long as PaginatedArray doesn't require creation of stupid URLs like nextpage1, nextpage2, etc., happy to leverage that. Either way, it shouldn't need an external plugin.

The documentation doesn't give me much to work with - no code examples for noobs etc. There are examples here, but only in combination with the MarkupPagerNav module, with all the page number and URL stuff I am trying to avoid. 

Someone else was struggling with PaginatedArray here - 'pagination never works!' There is a code example in that thread that looks promising

I have to go back to basics, because none of this is obvious to me. This works as a super minimal GET Ajax demo:
 

Spoiler
<?php 

if($config->ajax) {

	echo "We have no clue";
		
	return $this->halt();

} else {

include 'inc/head.php'; ?>

<body>

    <div id=result></div>
    <button type=button>Click Here</button>

</body>

<?php } ?>

<script>
var jLoad = jQuery.noConflict(); 

jLoad(document).ready(function(){
    jLoad("button").click(function(){
        
        jLoad.get("", function(data) {
            jLoad("#result").html(data);
        });
    });
});
</script>

 

 

Link to comment
Share on other sites

11 hours ago, modifiedcontent said:

I am now puzzling with `if($config->ajax) { ...` - I have used Ajax elsewhere in my PW sites, but only with POST form data, not GET.

$config->ajax works exactly the same for POST and GET requests. It tells you whether the request is a "normal" or an XMLHttpRequest , independent of the request type.

11 hours ago, modifiedcontent said:

As long as PaginatedArray doesn't require creation of stupid URLs like nextpage1, nextpage2, etc.

Why would you think that those URLs are stupid? They are a conventional way of telling PW which set of pages to retrieve. Other frameworks are using this convention, too. They consist of 2 parts:
{pagNumPrefix}: configured through $config->pageNumUrlPrefix, defaults to "page"; tells PW that this is not a regular URL but a "paginated" one
{pageNum}: number of page, in other words: number of set of pages to retrieve; setStart() is using this number
From these 2 PW is able to figure out which pages from a PaginatedArray it should return.

Let's look at your example code in posts.php and see how that relates to PW terms and conventions:
 

/** @var WireInput $input */
// Default limit
$limit = isset($_GET['limit']) ? $_GET['limit'] : 12; 
// $_GET['limit'] translates to the limit= partof the $pages->find selector

// Default offset
$offset = isset($_GET['offset']) ? $_GET['offset'] : ($input->pageNum - 1) * $limit;
// $_GET['offset'] translates to $input->pageNum -1 * $limit

// $queried_posts = array_slice($posts, $offset, $limit);
// translates to
/** @var PaginatedArray $posts */
$posts = $pages->find("template=video, limit={$limit}, start={$offset}, sort=-date");
// if URL is /page2, $posts will contain 12 pages starting from page at 12th index

You can already see how much simpler the code becomes when using "stupid" URLs :-)

Now let's look at the etymology of the word "stupid":
It originates from the Latin word "stupere" which means "to be amazed or stunned".
Isn't it amazing how simple pagination can become when doing it the PW way? I personally am really stunned by that fact :-)

Of course you could do it in a less amazing way. PW is flexible enough to allow that. But then you would have to calculate and pass the information about $limit and $offset back and forth manually in your ajax call. You can pass this inside an object, just like your example code does:

let queries = {
   'offset' : offset,
   'limit' : limit
};
jLoad.get('<?php echo $config->urls->templates ?>posts.php', queries, function(data)...

Actually jQuery translates this to a request URL with GET parameters like /site/templates/posts.php?offset=0&limit=2
In PW this URL would not work because you cannot call site/templates/posts.php directly (see https://processwire.com/talk/topic/20300-run-independent-php-files-within-processwire/ )
You can solve that by including posts.php for AJAX calls inside your template (videos.php)

if($config->ajax) include_once("posts.php");

 

 

This is not all there is to do. Inside your JS you would have to keep track of the offset. How would your JS know which offset we are on? You could pass the offset as data attribute somewhere in your HTML and then retrieve it in JS. You could also send it together with the response as JSON. You could store it as cookie. Many possibilities here. But still making everything more complicated than it has to be.
When using a #load-more button with an amazing URL like /videos/page2, in your JS you just have to update the "2" to "3" so the amazing URL reads /videos/page3. By the way, the infinite-scroll plugin does exactly that.

If you want to do it yourself and you have a link that points to the next page like

<a id="load-more" href="/videos/page2">Load more</>

You could use this JS to update href to 'videos/page3'

var elem = $("#load-more");
var path = elem.attr('href');
var pathParts = path.split('/');
var thisPage = pathParts.pop();
var pageNum = parseInt(thisPage.match(/[0-9]+$/));
var pagePrefix = thisPage.slice(0, thisPage.match(/[0-9]+$/).index);
var nextPage = pagePrefix.concat('', pageNum + 1);
pathParts.push(nextPage);
var newPath = pathParts.join('/');
elem.attr('href', newPath);

Is this nice? No. Could it be improved? Certainly.

While in the above there certainly are some code samples that you could use to achieve what you want, my intention was more to show that it is a lot more work to go this route. Look at my initial example to use with infinite-scroll plugin and how simple and intuitive that is.

Sorry if I cannot (or rather don't want to) supply you with a readymade copy/paste solution and sorry if I might sound sarcastic. I'm just not seeing the point in reinventing the wheel of pagination for the xth time over. I don't see the advantage of a custom implementation for your use case over that which PW provides out of the box.
If it is just the amazing URLs like page2, page3 that bug you, try to think of them like a tool that you can use to your advantage. And neither the site visitor nor search engines need to see them. As for site visitors, they would only see them in the browsers' dev tools. And search engines only if you allow them to (think of rel=nofollow).

If you still want to do it your own way then I wish you the best of luck and happy coding :-)

 

  • Like 2
Link to comment
Share on other sites

Finally got the demo working in a PW template testpage.php:

Spoiler
<?php 

	if($config->ajax) {
			
				$posts = array(
			    	array(
			    		'id' => '1',
			    		'title' => 'Title of the first post',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '2',
			    		'title' => 'Second post here',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '3',
			    		'title' => 'And another one',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '4',
			    		'title' => 'Fourth post',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '5',
			    		'title' => 'Why is this the fifth?',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '6',
			    		'title' => 'Number six',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '7',
			    		'title' => 'Post number seven',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '8',
			    		'title' => 'Number eight here - more to go',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '9',
			    		'title' => 'Article number nine',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),    	
			    	array(
			    		'id' => '10',
			    		'title' => 'Number ten - almost the end',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),    	
			    	array(
			    		'id' => '11',
			    		'title' => 'Article number eleven',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '12',
			    		'title' => 'And the twelfth post',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '13',
			    		'title' => 'But we also have thirteen',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '14',
			    		'title' => 'And a fourteenth post',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	),
			    	array(
			    		'id' => '15',
			    		'title' => 'And number fifteen',
			    		'image' => 'https://via.placeholder.com/500x300.png'
			    	)
			);
			
			
			// Default limit
			$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
			
			// Default offset
			$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
			
			$queried_posts = array_slice($posts, $offset, $limit);
			
			$total_posts = count($posts);
			
			// Get the total rows rounded up the nearest whole number
			$total_rows = (int)ceil( $total_posts / $limit );
			
			$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
			
			
			if (count($queried_posts)) {
			    foreach ($queried_posts as $post) { ?>
			    
			       <h2><?php echo $post['title']; ?></h2>
			       <img src="<?php echo $post['image']; ?>" />
			           
			     <?php }
			}
			
			else {
			    return false;
			}
			
			if ( $current_row < $total_rows ) { ?>
			    
			    <div>
			        <button id=load-more>Load more</button>
			    </div>
			    
			<?php }
			else {
			
			    return false;
			}
		
	return $this->halt();

} else {

include 'inc/head.php'; ?>

<body>

     <main class=container>
     <input type=hidden id=current-query value="" />
     <div id=all-posts></div>
     </main>

</body>

<?php } ?>

<script>
var jLoad = jQuery.noConflict(); 

var limit = 2;
var offset = 0;
var noMorePosts = false;
var loadingInProgress = false;

function loadPosts() {
    
    if (noMorePosts) return;
    
    let queries = {
        'offset' : offset,
        'limit' : limit
    };
    
    if (!loadingInProgress) {
        
        loadingInProgress = true;
        
        jLoad.get('', queries, function(data) {
            
            if(!data) {
                noMorePosts = true;
                jLoad('#load-more').remove();
            }
            else {
                offset += limit;
                
                jLoad('#load-more').remove();
                
                jLoad('#all-posts').append(data);
                
            }
            
            loadingInProgress = false;
            
        });
    }
    
}

/************************************************
 * Adds Load More Button Functionality
 * Exclude this if only infinite scroll is needed
*************************************************/
jLoad(document).on('click', '#load-more', function() {
    
    let currentQuery = {};
    
    jLoad('#load-more').html('Loading...').prop('disabled', true);
    
    if(jLoad('#current-query').val().length) {
        currentQuery = JSON.parse(jLoad('#current-query').val());
    }
    loadPosts( currentQuery );
    
    
});

/************************************************
 * Adds Infinite Scroll Functionality
 * Exclude this if only load more button is needed
*************************************************/

jLoad(window).scroll(function() {
    
    if( ( jLoad(window).scrollTop() + jLoad(window).height() ) >= jLoad(document).height() ) {
        
        let currentQuery = {};
    
        if( jLoad('#current-query').val().length) {
            currentQuery = JSON.parse(jLoad('#current-query').val());
        }

        jLoad('#load-more').html('Loading...').prop('disabled', true);
        
        loadPosts(currentQuery);
        
    }
});

jLoad(document).ready(function() {
    loadPosts();
})
</script>

 

I'll replace this step by step with PW native stuff and other updates. 

I see I can't simply replace the hardcoded demo $posts array with `$posts = $pages->find("template=video, limit='12', sort=-date");`. I guess I need `"id" => "1"`, "id" => "2"`, etc. or rewrite something somewhere? Or JSON encode $posts?

Why does $posts from $pages->find() not work when the manual/hardcoded $posts array from the demo works fine? I see the PW $posts array starts with the following:

ProcessWire\PageArray Object ( [count] => 12 [total] => 118 [start] => 0 [limit] => 12 [pager] => 1 to 12 of 118 [items] => Array ( [Page:0] => Array ( [id] => 7457 [... etc.

So there is the built-in pagination stuff. How can I work with that? I don't want "12 items per page". I want to grab a bunch of items and then add those to the page 1 by 1 (or 2) as you scroll and they come into view. How can I make $posts from PW behave like the demo script?

This works, but it is not the PW way:

Spoiler
<?php 

	if($config->ajax) {
		
			$template = $input->get('template');
			
			$batch = $input->get('batch');
		
			$posts = $pages->find("template=$template, limit=$batch, sort=-date");

			$posts_array = array();
			
			foreach ($posts as $post) {
			
				$title = $post->title;
			    	    
			    $posts_array[] = array(
			        'title' => $title
			    );
			}

			// Default limit
			$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;
			
			// Default offset
			$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
			
			$queried_posts = array_slice($posts_array, $offset, $limit);
			
			$total_posts = count($posts_array);
			
			// Get the total rows rounded up the nearest whole number
			$total_rows = (int)ceil( $total_posts / $limit );
			
			$current_row = !$offset ? 1 : ( $offset / $limit ) + 1;
			
			if (count($queried_posts)) {

			    foreach ($queried_posts as $post) { ?>
			    
			       <h2 style='height:400px'><?php echo $post['title']; ?></h2>
			           
			     <?php }

			} else {
			
			    return false;
			}
		
	return $this->halt();

} else {

include 'inc/head.php'; ?>

<body>

     <div class=container style='padding: 8% 8% 50%'>
     <input type=hidden id=currentquery value='' />
     <div id=loadposts></div>
     <button id=loadmore>Load more</button>
     </div>

</body>

<?php } ?>

<script>
var jLoad = jQuery.noConflict(); 

var template = 'video';
var batch = 32;
var limit = 2;
var offset = 0;
var noMorePosts = false;
var loadingInProgress = false;

function loadPosts() {
    
    if (noMorePosts) return;
    
    let queries = {
			'template' : template,
			'batch' : batch,
			'offset' : offset,
			'limit' : limit
    };
    
    if (!loadingInProgress) {
        
        loadingInProgress = true;
        
        jLoad.get('', queries, function(data) {
            
            if(!data) {
					noMorePosts = true;
					jLoad('#loadmore').remove();
            }
            else {
                offset += limit;               
                jLoad('#loadposts').append(data);
            }
            loadingInProgress = false;
        });
    }
}


const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (!entry.isIntersecting) {
      return;
    }
        let currentQuery = {};
    
        if( jLoad('#currentquery').val().length) {
            currentQuery = JSON.parse(jLoad('#currentquery').val());
        }

        jLoad('#loadmore').html('Loading...').prop('disabled', true);
        loadPosts(currentQuery);
  });
});

io.observe(document.getElementById('loadmore'));


jLoad(document).ready(function() {
    loadPosts();
})
</script>

 

It only works with `echo $post['title'];` in the output, not `echo $post->title;`. Why?

But this does what I need, so I'll use this in my sites after further clean-up and updates. Are there bits that could easily be replaced with PW equivalents? Do you see obvious problems/downsides? Any other suggestions how to improve this much appreciated.

I don't understand what `<input type=hidden id=currentquery value="" />` does, but removing it breaks the script... It looks hacky. Is there a better way to do whatever it does? 😕

I guess the syntax `$limit = isset($_GET['limit']) ? $_GET['limit'] : 2;` doesn't work within PW and I should use `$input->get('limit')` with `if ... else` instead?

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...