<p><span class="drop-cap">S</span>ometimes as developers we encounter situations that seem pretty straightforward, but as we try to implement them, we find weird nonsensical bugs that completely ruin the experience for our users. I’m going to tell you about one of those cases, and how we fixed it. </p>
<p><br />
Our requirement was to have several views in one page, one below the other. The last one, at the bottom of the page, had exposed filters with Ajax (so that the page would not reload when adding new filters). By default, for any regular Drupal site, when filtering a view with exposed filters the page will scroll up to the beginning of the page, usually where those exposed filters are, without reloading the whole thing. So as you might imagine by now, when filtering in our beautifully long page, the user would be taken to the top of the page and have to scroll all the way down again to find the results of the filter.</p>
<p><br />
This happens because by default Drupal views have a “scroll to top” effect when using Ajax. It doesn’t matter if the view’s exposed filters are at the bottom of the page: after submitting your filters, Ajax will refresh the view and scroll to the top of the page.</p>
<p><br />
As you can imagine this is not a really nice behaviour for users.</p>
<p>After some research, we found out that the solution was to disable this behaviour (it’s called a command) through a preprocess function, in this case <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21form.api.php/function/hook_ajax_render_alter/8.9.x">hook_ajax_render_alter</a><br />
This hook has all the commands that will be sent to the client’s browser, and all we need to do is unset the one that does the scroll to top.</p>
<p><br />
Here is a snippet of code that shows how we altered our Ajax behavior:</p>
<pre>
<code class="language-php">function MY_MODULE_ajax_render_alter(array &$data) {
if (isset($data[0]['settings']['views']['ajaxViews'])) {
foreach ($data[0]['settings']['views']['ajaxViews'] as $key => $value) {
if ($value['view_name'] === YOUR_VIEW_NAME) {
foreach ($data as $key => $command) {
if ($command['command'] === 'viewsScrollTop') {
unset($data[$key]);
break;
}
}
}
}
}
}
</code></pre>
<p>This hook will check if your view is present (avoiding PHP notices) and unset the command that creates the Scroll to top effect when updating a view through Ajax.</p>
<p><br />
We’re making sure this is only unsetting this command for our view only, leaving intact all other views.<br />
In case you need to use this for a specific display id, you can search on the array for the display id as well.</p>
<blockquote>
<p><em>Friendly tip:</em><br />
If you’re a var_dump/kint debugger kind of developer, keep in mind that the $data array, the information won’t come up on the browser, but rather on Inspect tools (Ctrl+Shift+I on Chrome), under Network, since this is an Ajax call.</p>
</blockquote>
<p>One could have thought that our job was done, but then we were facing the issue of not having a scroll to top anymore:once the view is updated it just stays there.</p>
<p><br />
So to fix this new issue we had just introduced we had to create our own ajax command with some custom JS. Panic not, here is the snippet that does the magic.</p>
<p><br />
First we must define an AJAX Command Class, located in my_module/src/Ajax/NameOfCommand.php</p>
<pre>
<code class="language-php"><?php
namespace Drupal\my_module\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Class NameOfCommand.
*/
class NameOfCommand implements CommandInterface {
/**
* Render custom ajax command.
*
* @return ajax command function
*/
public function render() {
return [
'command' => 'NameOfCommand',
];
}
}
</code></pre>
<p><br />
Once the command is created, a library definition is required in my_module.libraries.yml</p>
<pre>
<code class="language-php">my_module_name_of_library:
js:
js/my_javascript_file.js: {}
dependencies:
- core/drupal.ajax</code></pre>
<p>It might be possible that you need additional dependencies, such as:</p>
<pre>
<code class="language-php"> - core/drupal
- drupal/jquery
- core/jquery.once</code></pre>
<p><span><span><span><span><span><span>Now, there are several ways to include an Ajax command. In our example we used an Event Subscriber tied to our view id.</span></span></span></span></span></span></p>
<pre>
<code class="language-php"><?php
namespace Drupal\my_module\EventSubscriber;
use Drupal\views\Ajax\ViewAjaxResponse;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Drupal\my_module\Ajax\BlogLandingScrollToView;
/**
* Alter a Views Ajax Response.
*/
class NameofClassSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = ['onResponse'];
return $events;
}
/**
* Allows us to alter the Ajax response from a view.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event process.
*/
public function onResponse(FilterResponseEvent $event) {
$response = $event->getResponse();
// Only act on a Views Ajax Response.
if ($response instanceof ViewAjaxResponse) {
$view = $response->getView();
// Only act on the view to tweak.
if ($view->storage->id() === id_of_view) {
$response->addCommand(new NameOfCommand());
}
}
}
}
</code></pre>
<p>And finally, once we have set up the Subscriber to trigger our command, we have to write the Javascript for the desired effect we want, in the file specified in my_module.libraries.yml</p>
<pre>
<code class="language-javascript">(function ($, Drupal) {
/**
* Add new custom command.
*/
Drupal.AjaxCommands.prototype.NameOfCommand = function (ajax, response, status ) {
document.getElementById('your-id').scrollIntoView(true)
}
})(jQuery, Drupal);</code></pre>
<p>In this case we opted for getting the ID of the view with plain JS and just use the scrollIntoView method</p>
<p>That's it, now the page will stay on the view and not scroll to the top when the filters get updated<br />
</p>
Related blog posts
Pantheon Quicksilver script to send notifications to Discord
This is just one of those moments when I realized something obvious. I just wanted to share it.
Following the one way or another series we’ll solve the problem of overriding the default order of a view with hand picked contents through Entity queues.