Quicktabs? Quick PITA! - How to Deal with Pagers


Let's be honest, I don't really like the Quicktabs module. It's not that I don't think it's useful, it's just that users can do some pretty gnarly stuff with it that causes issues, like a ton of PHP notices being thrown into logging. 

Where I work, we run Drupal as a service to a network of about 300 sites. Updates are rolled out to all these sites, so most sites have the exact same copy of the codebase running on them. Of course, there are your special snowflakes, but for the most part, each site needs to have sustainable code changes made to it in favor of one-off custimizations which would be quite a pain to maintain.  

We have one client who ended up nesting quicktabs about five levels deep and had some of the levels end up with no content in them just tabs to other content. Because of improper checking in the quicktabs module and Google crawling all of those links periodically, this one page caused hundreds of thousands of PHP notices about the same thing: "No contents to render." This accounted for nearly 98% of our logging output making it impossible to tell if issues were popping up at a quick glance at the logs. 

Enter the Pager of Hell

We ended up patching quicktabs to get rid of most of our PHP notices eminating from the one damn page, but another issue popped up with pagers inside of quicktabs. A client had made a quicktabs instance where they were using a custom bean we had created that used Drupal's default pager. 

The problem occurred both when AJAX was turned on and also when it was turned off for loading each individual tab. With AJAX on, the pager link returned the snippet to place in the quicktab but loaded that for the whole page giving end users a page full of jibberish. So, I turned off AJAX on the tabs and it looked like it was going to work but the page reloaded on the first quicktab and not the the paged one. 

JS All the Things!!!

After looking at theme_pager() and the like functions and seeing how my pager issue was very granular and not applicable to most pager output, I decided to use JavaScript to "fix" the issue. I know, using JS to fix something that should be done on the server-side is kind of like saying "we'll fix it in post" when recording a track, but hey whaddya gonna do?

(function ($) {
  Drupal.behaviors.quicktabBeansPager = {
    attach: function (context, settings) {

The first step in my JavaScript hackery was to setup the customary "Drupal.behaviors" function. I've been pretty used to this syntax for a while, but there was a time when I had no idea that attaching functions to the Drupal object was a best practice. 

The most useful part of the attach syntax, for me, was the context part. Whenever I would run XDebug or another profiler on a Drupal site, I was always confused as to why the JS ran multiple times. This was very frustrating when trying to transition a binary on/off state for something like a button. The button would be clicked and then un-clicked causing me to pull my hair out and shake my laptop. 

It was only after that point that I decided to check and see what the context function parameter was all about. Sure enough, there was a context for a logged in admin user (contextual links, overlays, etc.) and a context for your average user (the JS loaded for that particular page). Once I added that parameter, my JS only ran once. If you're familiar with jQuery, then you'll know that you can use a similar parameter to target JS...hell you can even use the jQuery.once() method to really, really make sure you don't end up executing your JS more than once...but I digress...

      // forEach method, could be shipped as part of an Object Literal/Module
      var forEach = function (array, callback, scope) {
        for (var i = 0; i < array.length; i++) {
          callback.call(scope, i, array[i]); // passes back stuff we need

Since I wanted to get away from jQuery and learn vanilla JS I had to add, ahem copy/paste from stackexchange, a function to handle traversing DOM elements as if they were arrays. The function basically calls another function passing in the part of the array you want it to run on. 

      // Get all quicktabs tabs.
      tabs = document.querySelectorAll('.quicktabs-tabpage');

      // Get name of quicktabs instance.
      qtInstance = document.querySelector('.quicktabs-wrapper');
      if (qtInstance) {
        qtName = qtInstance.getAttribute('id').split('-');

I then had to get all of the quicktabs and names of them to add to the pager links. The idea is to add query parameters to the links in order to have the page reload on the correct tab and the correct page of that quicktabs instance.

      // Loop through and add the tab reference to the pager link.
      forEach(tabs, function (index, value) {
        doop = value.querySelectorAll('.quicktabs-tabpage .pager a');
        forEach(doop, function (indexed, valued) {
          href = valued.getAttribute('href');
          // Don't add query parameter if it has been added already.
          if (href.indexOf('&qt-' + qtName[1] + '=') === -1) {
            // Add new href here.
            href = href + '&qt-' + qtName[1] + '=' + index;
            valued.setAttribute('href', href);
          valued.setAttribute('href', valued.getAttribute('href') + '#pager-full');

So, here is the janky loop that adds the query parameters to the quicktabs pager links. You'll notice a condition to not add anything more to the href attribute if the Quicktab name has already been appended. Before I added that part, I ended up having the quicktabs query parameter appended over and over to the links after each page load. 

The last part adds a fragment to have the user end up at the pager links instead of being back at the top of the page when it reloads. Adding this extra part makes it more likely to end up with incorrect or poorly formed links, so I would only add that part if your client asks for it. 

Double Pager, Double Trouble

After adding that piece of JS code, the quicktabs instance I was working on loaded the pager and other tabs fine and paged to the correct page and quicktab when its links were clicked. I thought I was done with the horrible problem of loading a pager within quicktabs, but boy was I wrong. 

What if there are two or three tabs that end up having a pager in them. With the above code, only the first quicktab is taken care of. The subsequent quicktabs don't reload with their correct page of the pager. This is because the "?page=" part of the URL only controls one pager. 

I did read that you can pass an "element" variable to the theme_pager() function and have multiple pagers on the same page; however, when I tried to do this, the whole pager disappeared. Since I didn't have much time to debug that issue, I found out that you can have multiple pagers controlled by the "?page=" parameter if you added the number of pagers on the page. So if there were three pagers on a page, you could add "?page=0,0,0&" and all three pager would be on page one of their results. Similarly, "?page=0,1,2" would result in the first pager being on page one, the second pager being on page two, and the third pager being on page three. 

The idea of using the page parameter this way seemed to do the trick and made sense to use until I thought about it and experimented more. What happens if each pager has a different number of results? What happens if you're on page two of one pager and want to go to page on of another? The pager built into Drupal doesn't seem to handle these situations well, and while I could have probably come up with a convoluted solution to "fix" those situations, there would likely be a new condition that came up and needed an additional jenga block thrown on the pile. 

So, the solution I and my team ended up deciding was...don't put pagers in quicktabs. If you want that functionality, you can always add a "read more" link at the bottom of the quicktab and redirect to another full page that has a pager in it. That is the simplest solution you can come up with, and I'd implore you to use it before you end up banging your head on the wall for telling your clients it's a great idea to use pagers in quicktabs. 

Keep in mind, my solution is using custom queries and the in-built Drupal pager functionality and not Views. I'm sure the Views ecosystem has solved this problem somehow, but if you're flying free and custom like me, save your time and sanity by just saying no.