Hi everyone, I am facing a weird issue on Drupal 11. I am trying to hide a specific Views Block (let's call it block_5) and show a custom markup message instead, but only if the current logged-in user has already created a node of a specific content type (preventivo_tenute_meccaniche) that references the current page node ID (field_riferimento_richiesta_tenu).

The issue is that whatever hook I use, the view keeps rendering normally for users who already submitted the node, and it's completely hidden for users who haven't submitted anything yet (the exact opposite of what I need). Even if I invert the PHP logic (empty() vs !empty()), nothing changes at all on the screen.

When doing a diagnostic check with hook_block_view_alter(), the page only intercepts generic blocks like system_main_block or page_title_block. The views_block:richieste-block_5 string is never intercepted directly as a standalone block configuration ID, meaning the view is probably rendered inside the main content element (e.g., via Layout Builder, an Entity Reference field, or Eva/Views Field View).

Here is the custom module code I tried using:

PHP

<?php

use Drupal\Core\Block\BlockPluginInterface;

function controllo_bottoni_block_view_alter(array &$build, BlockPluginInterface $block) {
  if ($block->getConfiguration()['id'] === 'system_main_block') {
    $current_user_id = \Drupal::currentUser()->id();
    if ($current_user_id == 0) return;

    $node = \Drupal::routeMatch()->getParameter('node');
    if ($node instanceof \Drupal\node\NodeInterface) {
      $richiesta_nid = $node->id();
    } else {
      return;
    }

    $query = \Drupal::entityQuery('node')
      ->condition('type', 'preventivo_tenute_meccaniche')
      ->condition('field_riferimento_richiesta_tenu', $richiesta_nid)
      ->condition('uid', $current_user_id)
      ->accessCheck(FALSE)
      ->range(0, 1);

    $ids = $query->execute();

    if (!empty($ids)) {
      // Logic to hide the view inside $build['#content'] or inject CSS
      // Neither recursive scanning of #content nor injecting a <style> tag to hide the view class worked.
    }
  }
}

Every time I update the code, I clear the cache via performance admin UI, but the block behavior remains completely unaligned.

What is the correct way or hook to intercept a View display injected inside the main node rendering array to dynamically hide it based on an entity query? Thanks in advance!

Comments

jaypan’s picture

Generally for situations like this, I would just create my own block plugin, that handles the logic. Make sure you add the 'user' cache context, so the block renders separately per user.

<?php

namespace Drupal\[module]\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a block with some logic
 *
 * @Block(
 *   id = "module_conditional_block",
 *   title = @Translation("[MODULE] conditional block"),
 * )
 */
class MODULEConditionalBlock extends BlockBase implements ContainerFactoryPluginInterface {

  public function __construct(
    $configuration,
    $plugin_id,
    $plugin_definition,
    protected readonly CurrentRouteMatch $routeMatch,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly AccountProxyInterface $currentUser,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('current_route_match'),
      $container->get(EntityTypeManagerInterface::class),
      $container->get(AccountProxyInterface::class),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $block = [];
    if ($node = $this->routeMatch->getParameter('node')) {
      $existing_nodes = $this->entityTypeManager->getStorage('node')->loadByProperties([
        'type' => 'preventivo_tenute_meccaniche',
        'uid' => $this->currentUser->id(),
        'field_riferimento_richiesta_tenu' => $node->id(),
      ]);

      if (empty($existing_nodes)) {
        $block = [
          '#type' => 'view',
          '#name' => '[VIEW NAME]',
          '#display_id' => '[DISPLAY ID]',
        ];
      }
      else {
        $block = [
          '#markup' => $this->t('You've already created a preventivo_tenute_meccaniche referencing this node.),
        ];
      }
    } 

    return $block;
  }
  

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    $contexts = parent::getCacheContexts();
    // Ensure the block caches differently per user, as the visibility will be different depending on user.
    $contexts[] = 'user';
    return $contexts;
  }
  
}

Contact me to contract me for D7 -> D10/11 migrations.

linux56’s picture

Thank you very much for the answer.

I applied your solution. The block is now correctly detected and registered by Drupal's UI (the class is parsed without issues and the service injection is correct), but I'm still facing a runtime issue regarding its dynamic behavior.

Here is what we did/tested based on your version, and the current status:

1. Modifications and tests applied compared to your version

  • Cache Variation (getCacheContexts): To prevent the block from getting "frozen" on the first page the user visits, we integrated the cache contexts by adding both the user and the route (the current page). This forces Drupal to re-evaluate the logic on every URL change:

    PHP

    $contexts[] = 'user';
    $contexts[] = 'route';
    
  • Testing the View Render Array (build): We tested the #arguments key. Since the richieste view (display block_5) already has a contextual filter configured in the Views UI to "Provide default value -> Content ID from URL", we tried both explicitly passing the argument ('#arguments' => [$node->id()]) and removing that line entirely to let Views handle it natively.

2. Current situation and the issue encountered

Currently, the code runs, but the output on the page is not correct:

  • Case 1 (User has ALREADY submitted the quote): It works. The code enters the else branch, correctly detects the existing node, and displays our custom markup message on the screen.

  • Case 2 (Supplier has NOT yet submitted the quote): It doesn't work. When the array is empty (empty($existing_nodes)), the entire block completely disappears from the page. The View with the button is not rendered, and even the block title vanishes from the region, leaving the layout completely blank.

It seems that when the build() method returns a view render array (#type => 'view'), the Drupal rendering pipeline or the Views contextual filter validation fails, causing Drupal to treat the block as "empty" and hide it entirely due to layout cleanup rules.

Do you have any idea why a view render array behaves this way or gets emptied when returned conditionally inside another block's build() method?

Additionally, do you know if there is a way to achieve this entire conditional behavior using only the View itself, without any custom PHP code or additional modules? For example, by leveraging Views relationships, contextual filters, or "No Results Behavior" to check if the current user has already created a 'preventivo_tenute_meccaniche' node linked to the current 'richiesta' node?

Lino

linux56’s picture

Following up on my previous post where I was struggling to pass the current Request ID to a prepopulated form when a View returns no results—I finally solved it! After days of fighting with missing row tokens and URL aliases, here is the clean, definitive solution.

Why the usual methods failed:

  1. No Row Tokens: Since the View is empty (zero results), Drupal doesn't generate any tokens for fields.

  2. No Contextual Filters: If your View architecture doesn't use contextual filters, Twig variables like {{ view.args }} remain completely empty.

  3. URL Aliases: If your site uses clean URLs (e.g., /requests/my-test-title), a basic browser JavaScript regex looking for /node/ID fails because the actual ID is hidden.

The Solution

Instead of forcing Drupal to look for server-side tokens, we can solve this entirely on the client side (in the user's browser). We can insert a small HTML + Vanilla JavaScript snippet inside a Global: Text area under the No Results Behavior of the View.

Crucial step: Make sure the Text format under the text area is set to Full HTML, otherwise Drupal will strip out the <script> tags.

The script bypasses the visible URL and directly queries Drupal’s global system object window.drupalSettings (or falls back to the <link rel="shortlink"> tag in the page HEAD). It extracts the raw numeric Node ID, cleans it, and injects it straight into the button link.

The Code

Paste this directly into your View's No Results Text Area:

HTML

<div class="blocco-inserimento-bottone" style="margin: 20px 0;">
  <a id="bottone-preventivo-dinamico" href="/node/add/preventivo_tenute_meccaniche?richiesta=" class="button button--primary" style="display: inline-block; padding: 10px 20px; background-color: #007bff; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">
    ADD A QUOTE FOR THIS REQUEST
  </a>
</div>

<script>
  (function() {
    var nid = '';

    // 1. Check Drupal's global path settings (works perfectly with URL Aliases)
    if (window.drupalSettings && window.drupalSettings.path && window.drupalSettings.path.currentPath) {
      var percorso = window.drupalSettings.path.currentPath; // Returns e.g., "node/307"
      var estraiNumeri = percorso.match(/\d+/);
      if (estraiNumeri) {
        nid = estraiNumeri[0]; // Isolates only the digits (e.g., "307")
      }
    }

    // 2. Fallback: check the HTML shortlink in the HEAD if the first method fails
    if (!nid) {
      var tagShortlink = document.querySelector('link[rel="shortlink"]');
      if (tagShortlink) {
        var href = tagShortlink.getAttribute('href');
        var estraiNumeriHref = href.match(/\d+/);
        if (estraiNumeriHref) {
          nid = estraiNumeriHref[0];
        }
      }
    }

    // 3. Inject the clean numeric ID into the button's href attribute
    if (nid) {
      var elementoBottone = document.getElementById('bottone-preventivo-dinamico');
      if (elementoBottone) {
        elementoBottone.href = "/node/add/preventivo_tenute_meccaniche?richiesta=" + nid;
      }
    }
  })();
</script>

Why this is the best approach:

  • Zero Server Overhead: It doesn't trigger extra database queries or heavy token modules.

  • Alias-Proof: It works seamlessly whether your path is a raw system path (/node/307) or a customized semantic alias (/quotes/industrial-seal-maintenance).

  • Clean Data Input: By using match(/\d+/), it strips out any trailing text or duplicates (avoiding buggy strings like node/307,307), ensuring your entity prepopulate module receives a flawless, clean integer.

Hopefully, this saves someone else from the headache I went through!