Skip to content

Instantly share code, notes, and snippets.

@dkjensen
Last active October 31, 2024 22:27
Show Gist options
  • Save dkjensen/5190c554420ea2f19987a3f31ac95785 to your computer and use it in GitHub Desktop.
Save dkjensen/5190c554420ea2f19987a3f31ac95785 to your computer and use it in GitHub Desktop.
WordPress Gutenberg Query Loop View More AJAX
<?php
/**
* Add data attributes to the query block to describe the block query.
*
* @param string $block_content Default query content.
* @param array $block Parsed block.
* @return string
*/
function query_render_block( $block_content, $block ) {
global $wp_query;
if ( 'core/query' === $block['blockName'] ) {
$query_id = $block['attrs']['queryId'];
$container_end = strpos( $block_content, '>' );
$inherit = $block['attrs']['query']['inherit'] ?? false;
// Account for inherited query loops
if ( $inherit && $wp_query && isset( $wp_query->query_vars ) && is_array( $wp_query->query_vars ) ) {
$block['attrs']['query'] = query_replace_vars( $wp_query->query_vars );
}
$paged = absint( $_GET[ 'query-' . $query_id . '-page' ] ?? 1 );
$block_content = substr_replace( $block_content, ' data-paged="' . esc_attr( $paged ) . '" data-attrs="' . esc_attr( json_encode( $block ) ) . '"', $container_end, 0 );
}
return $block_content;
}
\add_filter( 'render_block', __NAMESPACE__ . '\query_render_block', 10, 2 );
/**
* Replace the pagination block with a View More button.
*
* @param string $block_content Default pagination content.
* @param array $block Parsed block.
* @return string
*/
function query_pagination_render_block( $block_content, $block ) {
if ( 'core/query-pagination' === $block['blockName'] ) {
$block_content = sprintf( '<a href="#" class="view-more-query button">%s</a>', esc_html__( 'View More' ) );
}
return $block_content;
}
\add_filter( 'render_block', __NAMESPACE__ . '\query_pagination_render_block', 10, 2 );
/**
* AJAX function render more posts.
*
* @return void
*/
function query_pagination_render_more_query() {
$block = json_decode( stripslashes( $_GET['attrs'] ), true );
$paged = absint( $_GET['paged'] ?? 1 );
if ( $block ) {
$block['attrs']['query']['offset'] += $block['attrs']['query']['perPage'] * $paged;
\add_filter( 'query_loop_block_query_vars', function( $query ) {
// Only return published posts.
$query['post_status'] = 'publish';
return $query;
} );
echo render_block( $block );
}
exit;
}
add_action( 'wp_ajax_query_render_more_pagination', __NAMESPACE__ . '\query_pagination_render_more_query' );
add_action( 'wp_ajax_nopriv_query_render_more_pagination', __NAMESPACE__ . '\query_pagination_render_more_query' );
/**
* Replace WP_Query vars format with block attributes format
*
* @param array $vars WP_Query vars.
* @return array
*/
function query_replace_vars( $vars ) {
$updated_vars = [
'postType' => $vars['post_type'] ?? 'post',
'perPage' => $vars['posts_per_page'] ?? get_option( 'posts_per_page', 10 ),
'pages' => $vars['pages'] ?? 0,
'offset' => 0,
'order' => $vars['order'] ?? 'DESC',
'orderBy' => $vars['order_by'] ?? '',
'author' => $vars['author'] ?? '',
'search' => $vars['search'] ?? '',
'exclude' => $vars['exclude'] ?? array(),
'sticky' => $vars['sticky'] ?? '',
'inherit' => false
];
return $updated_vars;
}
( function( $ ) {
$( '.view-more-query' ).on( 'click', function( e ) {
e.preventDefault();
const self = $( this );
const queryEl = $( this ).closest( '.wp-block-query' );
const postTemplateEl = queryEl.find( '.wp-block-post-template' );
if ( queryEl.length && postTemplateEl.length ) {
const block = JSON.parse( queryEl.attr( 'data-attrs' ) );
const maxPages = block.attrs.query.pages || 0;
$.ajax( {
url: i18n.ajax_url,
dataType: 'json html',
data: {
action: 'query_render_more_pagination',
attrs: queryEl.attr( 'data-attrs' ),
paged: queryEl.attr( 'data-paged' ),
},
complete( xhr ) {
const nextPage = Number( queryEl.attr( 'data-paged' ) ) + 1;
if ( maxPages > 0 && nextPage >= maxPages ) {
self.remove();
}
queryEl.attr( 'data-paged', nextPage );
if ( xhr.responseJSON ) {
console.log( xhr.responseJSON ); // eslint-disable-line
} else {
const htmlEl = $( xhr.responseText );
if ( htmlEl.length ) {
const html = htmlEl.find( '.wp-block-post-template' ).html() || '';
if ( html.length ) {
postTemplateEl.append( html );
return;
}
}
self.remove();
}
},
} );
}
} );
}( jQuery ) );
@wpexplorer
Copy link

This is a cool implementation ;)

@hannahmwool
Copy link

This has worked great for me but unfortunately doesn't work if inheriting a query from the current template is enabled on the query block because inherit ignores all GET queries so I'm working on a workaround. Any suggestions would be greatly appreciated! Thanks so much for this!

@dkjensen
Copy link
Author

dkjensen commented Oct 9, 2023

@hannahmwool Good catch, I just updated the gist to account for inherited query blocks. Let me know if you encounter any issues or have feedback. Thanks!

@hannahmwool
Copy link

Amazing!! I am going to implement it now and will let you know. Thanks so much!

I have a version of your jQuery Ajax functionality as a vanilla JS alternative I use if you want me to share it I'd be happy to!

@dkjensen
Copy link
Author

dkjensen commented Oct 9, 2023

@hannahmwool I think that would be useful to others

@hannahmwool
Copy link

@dkjensen I implemented and it pulls in draft posts as well as published posts. the 'status' arg doesn't appear to do anything:

'status'    => $vars['post_status'] ?? 'publish',

I did try adjusting and adding following statements but no luck on the drafts:

if ( ! $vars['inherit'] ) {
        $vars['post_status'] = 'publish';
}

I also added the following for tax queries and those filtered successfully:

      'taxQuery'  => [
            'category' => $vars['cat'] ? [$vars['cat']] : [],
            'post_tag'      => $vars['tag'] ? [$vars['tag']] : [],
        ],

Thanks!

@dkjensen
Copy link
Author

@hannahmwool Good catch, I updated the code again, specifically here.

@hannahmwool
Copy link

hannahmwool commented Oct 17, 2023

Thanks so much @dkjensen I just discovered that filter today after filtering through main queries and such and was planning to mention it here and share today so It's great to see where you implemented it!

Also sharing my vanilla JS version of the jQuery part for anyone who may want to use it. I left in my scroll to the newly added posts function in case that is helpful for anyone. I also have a class added to new items as I like to bring them in in certain ways, figured the class may be helpful and can be removed also. Would definitely welcome any improvements for the below.

var ajaxLoadPosts = {
			ajaxurl: '/wp-admin/admin-ajax.php',
		  };
		  if (document.querySelector('.view-more-query') !== null) {
			document.querySelector('.view-more-query').addEventListener('click', function (e) {
				e.preventDefault();
		  
				const self = this;
				const queryEl = this.closest('.wp-block-query');
				const postTemplateEl = queryEl.querySelector('.wp-block-post-template');
		  
				if (queryEl && postTemplateEl) {
				  	const block = JSON.parse(queryEl.getAttribute('data-attrs'));
				  	const maxPages = block.attrs.query.pages || 0;
		  
				  	var xhr = new XMLHttpRequest();
				 	xhr.open('POST', ajaxLoadPosts.ajaxurl, true);
				 	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
				  	xhr.onload = function () {
						const nextPage = Number(queryEl.getAttribute('data-paged')) + 1;
			
						if (maxPages > 0 && nextPage >= maxPages) {
							self.remove();
						}
			
						queryEl.setAttribute('data-paged', nextPage);
			
						const htmlEl = document.createElement('html');
						htmlEl.innerHTML = xhr.responseText;
			
						const postTemplate = htmlEl.querySelector('.wp-block-post-template');
						if (postTemplate) {
							const newPosts = Array.from(postTemplate.querySelectorAll('.wp-block-post'));
							if (newPosts.length > 0) {
								newPosts.forEach((newPost, index) => {
									newPost.classList.add('new-item__loaded');
									postTemplateEl.appendChild(newPost);
								});
			
								// Scroll to the first newly added post with an offset (optional)
								const scrollOffset = -100; // Adjust the value to your desired offset
								const scrollOptions = {
								behavior: 'smooth',
								block: 'start',
								inline: 'nearest',
								};
								window.scrollTo({
								top: newPosts[0].offsetTop + scrollOffset,
								...scrollOptions,
								});
								return;
							}
						}
			
						self.remove();
					};
		  
					const data = {
						action: 'query_render_more_pagination',
						attrs: queryEl.getAttribute('data-attrs'),
						paged: queryEl.getAttribute('data-paged'),
					};
					const params = Object.keys(data)
						.map(function (key) {
						return encodeURIComponent(key) + '=' + encodeURIComponent(data[key]);
						})
						.join('&');
			
					xhr.send(params);
				}
			});
		}

@hannahmwool
Copy link

Hey @dkjensen, I noticed another issue when using it on the search template as there are no $vars['search'] available so it just loads all posts as normal when clicked on so I adjusted my $updated_vars to reflect that issue, and used the get_query_var('s') instead and this is currently working great so thought I'd share. I also have my tax query checks too.

function query_replace_vars( $vars ) {
    $updated_vars = [
        'postType'  => $vars['post_type'] ?? 'post',
        'perPage'   => $vars['posts_per_page'] ?? get_option( 'posts_per_page', 10 ),
        'pages'     => $vars['pages'] ?? 0,
        'offset'    => 0,
        'order'     => $vars['order'] ?? 'DESC',
        'orderBy'   => $vars['order_by'] ?? 'date',
        'author'    => $vars['author'] ?? '',
        'exclude'   => $vars['exclude'] ?? array(),
        'sticky'    => $vars['sticky'] ?? 'exclude',
        'inherit'   => false,
    ];

    // get the search term from the query string
    $search_term = get_query_var('s');
    if ($search_term) {
        $updated_vars['search'] = $search_term;
    }

    if ($vars['cat'] ) {
        $updated_vars['taxQuery']['category'] = [$vars['cat']];
    }

    if ($vars['tag'] ) {
        $updated_vars['taxQuery']['post_tag'] = [$vars['tag']];
    }

    return $updated_vars;
}

One item I am working on is making the "Load More" button hidden if there is no 2nd page of posts initially or when loading up the next set as it still shows if there's no more and only when you click again, does it remove itself. It looks like I'd have to try to identify if the core/query-pagination-next inner block has any items inside. I'm not sure how the actual core/query-pagination is doing it as it doesn't display if there aren't any more posts when not using the "Load More" button.

Thanks so much again!

@damianoporta
Copy link

@dkjensen is there a way to prevent showing the View More link in case the page has no posts to show. (For example after a search)

@dkjensen
Copy link
Author

dkjensen commented Mar 18, 2024

@damianoporta You could try this:

 * Replace the pagination block with a View More button.
 *
 * @param string $block_content Default pagination content.
 * @param array  $block Parsed block.
 * @return string
 */
function query_pagination_render_block( $block_content, $block ) {
	if ( 'core/query-pagination' === $block['blockName'] ) {
		if ( $block_content ) {
			$block_content = sprintf( '<a href="#" class="view-more-query button">%s</a>', esc_html__( 'View More' ) );
		}
	}

	return $block_content;
}
\add_filter( 'render_block', __NAMESPACE__ . '\query_pagination_render_block', 10, 2 );```

@laurelstreng
Copy link

laurelstreng commented Oct 31, 2024

Curious if anyone has tried using this with a query block variation? I'm attempting to and having trouble getting the load more to actually work. This has been very helpful though, thanks for everyones input!

@hannahmwool
Copy link

hannahmwool commented Oct 31, 2024 via email

@laurelstreng
Copy link

@hannahmwool that would be great!

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