0

Cross posting an issue I posted from Drupal.org in the hope that it gets a bit more visibility here.

I'm currently loading facet blocks via AJAX into an off canvas dialog provided by the core Dialog API. To achieve this, I have a controller set up that accepts a facet ID as a parameter, and then loads the block (I've attached the code at the end of this question as an example).

This works perfectly on the face of it (all of my facets are listed, the dialog works and shows the facet widgets for each facet). However, what I've noticed is that if I have a facet enabled, this isn't taken into account when generating facet links. For example; if I have a facet applied for language, and then look at the link generated for my collection facets, the facets link to my search page with the collection facet applied, but without the existing language facet.

The search view itself is not an AJAX view, it's just the loading of the facet blocks. Unfortunately, this is a requirement as we have a large amount of facets that kill the performance if loaded as part of the main request.

Here are some things I've tried:

  1. Adding the main page parameters to my dialog links
  2. Using these query parameters to configure, load, and execute my search view during the AJAX request before the facet block is generated (see following example)

These facet blocks are only ever rendered on the same page as the facet source View, however as it's not the same request it may as well be on a different page.

I guess my question is one of multiple parts:

  1. Am I missing something obvious in my approach
  2. Is it possible to do what I'm trying to achieve
  3. If it is and I'm going the wrong way about it, what's the right way to go about it

As an aside, I'm rendering the blocks isn't a requirement if it's easier to render the facets themselves - it just felt like it would be easier.

As promised, here's my controller code. As mentioned, the facet blocks display, but already-applied facets aren't taken into account during link generation.

MY_MODULE.routing.yml

my_facets.ajax_facet_block:
  path: '/ajax/my-facets/{facet_id}'
  defaults:
    _title: 'Filter by'
    _controller: '\Drupal\my_facets\Controller\AjaxFacetBlockController::renderFacetBlockAction'
  requirements:
    _permission: 'access content'

Controller

<?php

namespace Drupal\my_facets\Controller;

use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\RendererInterface;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Returns responses for Facets routes.
 */
class AjaxFacetBlockController extends ControllerBase {

  public function __construct(
    protected BlockManagerInterface $blockManager,
    protected RendererInterface $renderer,
    protected Request $request
  ) {
  }

  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('plugin.manager.block'),
      $container->get('renderer'),
      $container->get('request_stack')?->getCurrentRequest()
    );
  }

  /**
   * Builds the response.
   */
  public function renderFacetBlockAction(string $facet_id) {
    // We need to execute the search view to correctly generate links.
    // @ToDo: Once this is working inject the entity type manager rather than calling `Views::getView`
    $search_view = Views::getView('search');

    if (!$search_view) {
      throw new NotFoundHttpException();
    }

    $search_view->setDisplay('search_page');

    $search_view->setExposedInput(array_filter(
      $this->request->query->all(),
      static fn ($k) => !str_starts_with('_', $k),
      ARRAY_FILTER_USE_KEY
    ));

    $non_facet_params = array_filter(
      $this->request->query->all(),
      static fn ($key) => $key !== 'f' && !str_starts_with($key, '_'),
      ARRAY_FILTER_USE_KEY
    );

    $search_view->exposed_raw_input = array_merge(
      $search_view->exposed_raw_input,
      $non_facet_params
    );

    $search_view->exposed_data = array_merge(
      $search_view->exposed_data,
      $non_facet_params
    );

    $search_view->preExecute();
    $search_view->execute();

    try {
      /** @var \Drupal\facets\Plugin\Block\FacetBlock $block */
      $block = $this->blockManager->createInstance(
        'facet_block' . PluginBase::DERIVATIVE_SEPARATOR . $facet_id, []
      );
    }
    catch (\Exception) {
      throw new NotFoundHttpException();
    }

    if (!$block || !$block->access($this->currentUser())) {
      throw new NotFoundHttpException();
    }

    return $block->build();
  }

}
3
  • 1
    What are you doing about caching of your controller? Sounds like the response is being cached Commented Jan 16, 2023 at 9:02
  • Is the Ajax response supposed to vary by $facet_id? If yes, you haven't set caching metadata on the block.
    – mona lisa
    Commented Jan 16, 2023 at 13:14
  • Thanks both - but the caching wasn't the issue, it was a Facets internal thing.
    – Chapabu
    Commented Jan 24, 2023 at 11:59

1 Answer 1

1

This ended up being due to the QueryString processor provided by the Faces module performing a check for XmlHttpRequests as it's very first step (I assume to cover ajax based Views). If it finds that a request is an XmlHttpRequest, then it'll act on a subrequest (which in my case doesn't exist).

My rather rudimentary fix was to strip the X-Requested-With header from the request with an event listener on the KernelEvents::REQUEST event. This makes the underlying Symfony request respond as a standard request (read: non-XMLHttpRequest), and allowed the query string processor to act on the correct response instead of the sub-response.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.