Skip to content

document-level delegated click handler for non-infinite Load More button#56

Closed
jbouganim wants to merge 1 commit into
a8cteam51:trunkfrom
jbouganim:update/update-load-more-listeners
Closed

document-level delegated click handler for non-infinite Load More button#56
jbouganim wants to merge 1 commit into
a8cteam51:trunkfrom
jbouganim:update/update-load-more-listeners

Conversation

@jbouganim
Copy link
Copy Markdown

@jbouganim jbouganim commented Aug 27, 2025

Summary

Switch Load More click handling to a document-level delegated listener to prevent lost bindings after DOM replacements (Interactivity API/router updates) and avoid duplicate listeners. Infinite scroll remains unchanged.

What’s fixed

  • Per-button click bindings were lost when regions were replaced, breaking Load More until rebind.

Changes

  • assets/js/src/frontend.js: replace per-button listeners with a single document-level delegated click handler for .wp-load-more__button:not(.wp-load-more__infinite-scroll).
  • readme.txt: add 1.0.17 changelog entry documenting the change.

How to test

  1. Add a Query Loop with Pagination and enable “Load More”.
  2. Click Load More: posts append once; button updates to next page.
  3. Trigger a client-side DOM replacement (e.g., Interactivity API filter) and click Load More again: posts still append (no rebind needed).
  4. With multiple Query Loops, each Load More only affects its own loop; no duplicates.
  5. If infinite scroll is enabled, verify it still triggers as before.

Version

  • 1.0.17

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of the non-infinite “Load More” button using document-level click handling. Prevents lost click bindings after DOM updates and avoids duplicate listeners.
  • Documentation

    • Updated readme with 1.0.17 changelog and stable tag.
  • Chores

    • Bumped version to 1.0.17 and updated associated build asset versions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 27, 2025

Walkthrough

This change updates version identifiers across package.json (1.0.16 → 1.0.17), query-loop-load-more.php header, readme.txt stable tag, and assets/js/build/frontend.asset.php (hash). In JavaScript, non-infinite "Load More" click handling is moved from per-button listeners to a document-level delegated click handler. The IntersectionObserver and handler variable names are swapped, with corresponding updates to observe/unobserve calls. The compiled build files reflect these adjustments. Overall fetch, DOM update, URL handling, and error flow remain the same.

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (9)
assets/js/src/frontend.js (3)

164-172: Delegated click handler: good fix; guard against duplicate bindings.

If this script is executed more than once (e.g., via HMR or certain navigations), the document-level listener can be attached multiple times. Wrap the registration with a one-time guard without short-circuiting the rest of domReady.

Apply:

- // Delegated click handler for non-infinite buttons
- document.addEventListener( 'click', function ( e ) {
-   const btn = e.target.closest( '.wp-load-more__button:not(.wp-load-more__infinite-scroll)' );
-   if ( ! btn ) {
-     return;
-   }
-   e.preventDefault();
-   fetchPosts( btn );
- } );
+ // Delegated click handler for non-infinite buttons (bind once)
+ if ( ! window.__qllmDelegatedClickBound ) {
+   document.addEventListener( 'click', function ( e ) {
+     const btn = e.target.closest( '.wp-load-more__button:not(.wp-load-more__infinite-scroll)' );
+     if ( ! btn ) {
+       return;
+     }
+     e.preventDefault();
+     fetchPosts( btn );
+   } );
+   window.__qllmDelegatedClickBound = true;
+ }

55-60: Use Accept header for GET instead of Content-Type.

Content-Type describes a request body (none here). Prefer Accept to signal expected response.

 fetch( url, {
   method: 'GET',
   headers: {
-    'Content-Type': 'text/html',
+    'Accept': 'text/html',
   },
 } )

174-181: Infinite scroll after DOM replacement: observe newly added buttons.

Delegation fixed non-infinite clicks across client-side DOM swaps; infinite buttons added later won’t be observed because observe() runs only at domReady. Add a lightweight MutationObserver to auto-observe future .wp-load-more__infinite-scroll nodes.

 document
   .querySelectorAll( '.wp-load-more__infinite-scroll' )
   .forEach( function ( button ) {
     intersectionObserver.observe( button );
   } );
+
+ // Observe future additions for infinite scroll buttons
+ const qllmMo = new MutationObserver( (mutations) => {
+   for ( const m of mutations ) {
+     m.addedNodes.forEach( (node) => {
+       if ( node.nodeType !== 1 ) return;
+       if ( node.matches?.('.wp-load-more__infinite-scroll') ) {
+         intersectionObserver.observe( node );
+       }
+       node.querySelectorAll?.('.wp-load-more__infinite-scroll').forEach( (el) => {
+         intersectionObserver.observe( el );
+       } );
+     } );
+   }
+ } );
+ qllmMo.observe( document.body, { childList: true, subtree: true } );

To verify: after triggering a client-side region replacement that re-renders a Query Loop with infinite scroll enabled, ensure new buttons get observed (posts load when button comes into view) without a full page reload.

assets/js/build/frontend.js (6)

1-1: Harden delegated click: guard non-Element targets and respect meta/middle-click.

Click events can originate from non-Element targets; .closest would throw. Also, allow Cmd/Ctrl/middle-click to open in a new tab. Apply in assets/js/src/frontend.js; rebuild afterward.

- document.addEventListener("click",(e)=> {
-   const btn = e.target.closest('.wp-load-more__button:not(.wp-load-more__infinite-scroll)');
-   if (btn) { e.preventDefault(); fetchPosts(btn); }
- });
+ document.addEventListener('click', (e) => {
+   const target = e.target instanceof Element ? e.target : null;
+   const btn = target?.closest('.wp-load-more__button:not(.wp-load-more__infinite-scroll)');
+   if (!btn) return;
+   // Let users open in a new tab/window.
+   if (e.metaKey || e.ctrlKey || e.button === 1) return;
+   e.preventDefault();
+   fetchPosts(btn);
+ });

1-1: Rebind infinite-scroll buttons added after DOM replacements.

Interactivity/router updates can replace the sentinel button; the observer won’t reattach. Add a lightweight MutationObserver to observe new .wp-load-more__infinite-scroll nodes. Apply in assets/js/src/frontend.js; rebuild.

Please verify infinite scroll still triggers after a client-side region replacement.

 domReady(() => {
   // existing: document.addEventListener('click', ...) and initial observes
   document.querySelectorAll('.wp-load-more__infinite-scroll').forEach((el) => observer.observe(el));
+
+  // Observe future additions/removals of infinite-scroll buttons.
+  const ensureObserved = (root) => {
+    root.querySelectorAll?.('.wp-load-more__infinite-scroll')?.forEach((el) => observer.observe(el));
+  };
+  ensureObserved(document);
+  const mo = new MutationObserver((mutations) => {
+    for (const m of mutations) {
+      m.addedNodes.forEach((node) => {
+        if (node.nodeType === 1) {
+          if (node.matches?.('.wp-load-more__infinite-scroll')) observer.observe(node);
+          else ensureObserved(node);
+        }
+      });
+      m.removedNodes.forEach((node) => {
+        if (node.nodeType === 1) {
+          if (node.matches?.('.wp-load-more__infinite-scroll')) observer.unobserve(node);
+          else node.querySelectorAll?.('.wp-load-more__infinite-scroll')?.forEach((el) => observer.unobserve(el));
+        }
+      });
+    }
+  });
+  mo.observe(document.body, { childList: true, subtree: true });
 });

1-1: Preserve existing query params when updating the button href.

Current code overwrites the full query string. Use URL API to only mutate the paging param. Apply in assets/js/src/frontend.js; rebuild.

- btn.href = '?' + btn.dataset.queryUrl + '=' + btn.dataset.queryNextPage;
+ {
+   const url = new URL(btn.href, window.location.href);
+   url.searchParams.set(btn.dataset.queryUrl, btn.dataset.queryNextPage);
+   btn.href = url.toString();
+ }

1-1: Don’t set Content-Type on a GET fetch.

The header is unnecessary for GET and can cause odd server behavior. Apply in assets/js/src/frontend.js; rebuild.

- fetch(url, { method: 'GET', headers: { 'Content-Type': 'text/html' } })
+ fetch(url, { method: 'GET' })

1-1: Avoid re-arming the observer after the button is removed.

Guard with a DOM presence check to skip unnecessary measure/re-observe. Apply in assets/js/src/frontend.js; rebuild.

- if (btn.classList.contains('wp-load-more__infinite-scroll')) {
-   const rect = btn.getBoundingClientRect();
-   if (rect.bottom > 0 && rect.top < window.innerHeight) {
-     observer.unobserve(btn);
-     observer.observe(btn);
-   }
- }
+ if (btn.classList.contains('wp-load-more__infinite-scroll')) {
+   if (!document.body.contains(btn)) return;
+   const rect = btn.getBoundingClientRect();
+   if (rect.bottom > 0 && rect.top < window.innerHeight) {
+     observer.unobserve(btn);
+     observer.observe(btn);
+   }
+ }

1-1: Remove stray container “loading” class toggle or also add it.

You remove loading from the .wp-block-button container but never add it. Either delete the removal or add a matching add for consistency. Apply in assets/js/src/frontend.js; rebuild.

- const container = btn.closest('.wp-block-button');
- if (container) container.classList.remove('loading');
+ // If container-level loading state is desired, also add it where the button state is set.
+ // Otherwise, remove this block for consistency.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6c6a88a and c8662ae.

📒 Files selected for processing (6)
  • assets/js/build/frontend.asset.php (1 hunks)
  • assets/js/build/frontend.js (1 hunks)
  • assets/js/src/frontend.js (1 hunks)
  • package.json (1 hunks)
  • query-loop-load-more.php (1 hunks)
  • readme.txt (2 hunks)
🔇 Additional comments (5)
query-loop-load-more.php (1)

7-7: Version bump matches release intent.

Header updated to 1.0.17; aligns with readme and package.json.

package.json (1)

3-3: Package version updated correctly.

Version set to 1.0.17; consistent with plugin header and readme.

assets/js/build/frontend.asset.php (1)

1-1: Asset version updated.

Hash bump reflects rebuilt frontend bundle; deps unchanged.

readme.txt (1)

6-6: Docs aligned with the change.

Stable tag and changelog accurately describe the delegated click handler and its motivation.

Also applies to: 74-76

assets/js/build/frontend.js (1)

1-1: Document-level delegated click handler: solid fix.

This removes per-button listeners, prevents duplicate handlers, and survives client-side DOM replacements. Infinite-scroll path remains intact.

@robrobsn
Copy link
Copy Markdown
Contributor

@jbouganim Thank you for the PR. The latest version 1.0.17 attaches the listener to the document. #57

@jbouganim
Copy link
Copy Markdown
Author

@robrobsn great, good if I close this then?

@robrobsn
Copy link
Copy Markdown
Contributor

great, good if I close this then?

@jbouganim Yes. Thanks!

@jbouganim jbouganim closed this Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants