2

I have a Paragraph setup that has an entity reference field allowing an editor to select multiple nodes to use in a display.

Based on the count of nodes, I want to trigger a class. I figured a way to do this was with hook_preprocess_node() in the theme:

$referringItem = $node->_referringItem;
    if (!empty($referringItem)) {
      $referringEntity = $referringItem->getEntity();

      if (
        !empty($referringEntity)
        && ($referringEntity instanceof NodeInterface || $referringEntity instanceof Paragraph)
       ) {
        $referringEntityType = $referringEntity->getType();
        $referringEntityTypeId = $referringEntity->getEntityTypeId();
      }
    }

if ($referringEntityType == 'homepage_feature') {
        $count = $referringEntity->get('field_items')->count();

        // Default.
        $variables['top_image'] = FALSE;
        $variables['right_image'] = FALSE;

        // Should this be count is only 1 AND it is the first delta item in field_feature?
        // If not, any single item Homepage Feature will get top-image which may not be
        // desired.
        if ($count == 1) {
          $variables['top_image'] = TRUE;
        }
      }

The logic above might be too involved. Basically I am looking back to see what paragraph type was used, so I can pass new variables to the template.

What I noticed is that even though the code is hit for every node listed, the variables only exist in the first node. I dumped them in the template and it is correct there, but they are undefined in the other nodes.

In Twig, I tried dumping the variable at the top of the template:

{{ top_image }}

I can see it in the first record 1 above the markup, and then it never shows again.

How can I ensure the variables are passed to the template, correctly, every time?

Edit:

Looks like using the same node with the same view mode in different paragraphs or entity references on the same page can cause this to happen.

Is the solution then to add cache tags/context to the node being rendered to account for what parent is outputting it?

8
  • You can't count on the property _referringItem being present in preprocess, because this is added by the field formatter for internal use and when the node is rendered you might have a new instance of the node object. A solution might be extending the field formatter, add the information you want to the build array of the field elements and add cache keys (tags/context won't help in this case and you need to add the cache keys in the field formatter, preprocess is too late).
    – 4uk4
    Jul 18, 2019 at 21:52
  • Where are the keys added? The only place I see it is EntityReferenceFormatterBase::view. So this formatter would be applicable to Paragraphs and change that from Rendered Entity to the new one?
    – Kevin
    Jul 18, 2019 at 22:01
  • Where exactly do you want the class being added? Isn't it also that when dumping a variable multiple times with Kint and they all have the same output that Kint sometimes only shows one value? And: Why don't you simply load the paragraph and from there the field and count its items?
    – leymannx
    Jul 18, 2019 at 22:03
  • The class is added on the node, but the class (there are others) is entirely dependent upon where it is in the list (multi reference paragraph) or if its just a single entry. So the information has to get communicated somehow. The Paragraph type also allows for multiples. The thought is, using the node and view mode and altering the CSS classes based on that.
    – Kevin
    Jul 18, 2019 at 22:07
  • 1
    @leymannx, yes, there could be a solution in preprocess, but the node preprocess is too late. Perhaps it's possible in the parent paragraph preprocess hook, retreiving there the information like you said and add it to the node reference field including the cache keys.
    – 4uk4
    Jul 18, 2019 at 22:23

1 Answer 1

2

Based on the comments to the question, I extended the EntityReferenceEntityFormatter class and modified one part in the viewElements method (the rest is the same):

  $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId());
  $elements[$delta] = $view_builder->view($entity, $view_mode, $entity->language()->getId());

  try {
    $parent = $items->getParent();
    $parent_entity = $parent->getValue();
    $elements[$delta]['#cache']['keys'][] = $parent_entity->id();
    $elements[$delta]['#cache']['keys'][] = $parent_entity->bundle();
    $elements[$delta]['#cache']['keys'][] = $parent_entity->getEntityTypeId();
    $elements[$delta]['#cache']['keys'][] = 'context_aware';

    // Add special information for different paragraph types.
    if ($parent_entity->getEntityTypeId() == 'paragraph') {
      if ($parent_entity->bundle() == 'homepage_feature') {
        $elements[$delta]['#style_flags']['top_image'] = FALSE;
        $elements[$delta]['#style_flags']['right_image'] = FALSE;

        if ($parent_entity->get('field_items')->count() === 1) {
          $elements[$delta]['#style_flags']['top_image'] = TRUE;
        }
      }
    }
  } catch (\Exception $e) {
    // Add logging
  }

Later on, down in preprocess_node:

// ONLY for the homepage handling of featured content.
      if ($referringEntityType == 'homepage_feature') {
        $variables['top_image'] = $variables['elements']['#style_flags']['top_image'];
        $variables['right_image'] = $variables['elements']['#style_flags']['right_image'];
      }

I was able to then add the same node in the same paragraph type multiple times with the correct information. The correct classes are now applied.

Edit:

Just to add to this too, be sure to set your cacheable dependencies so any change to what holds references will not require a cache clear:

$parent = $items->getParent();
$entity->addCacheableDependency($parent);

Edit 2:

Thanks to extra guidance from 4k4, I injected the renderer service and reduced down to:

    $parent = $items->getParent();
    $parent_entity = $parent->getValue();
    $elements[$delta]['#cache']['keys'][] = $parent_entity->id();
    $elements[$delta]['#cache']['keys'][] = $parent_entity->bundle();
    $elements[$delta]['#cache']['keys'][] = $parent_entity->getEntityTypeId();
    $elements[$delta]['#cache']['keys'][] = 'context_aware';

    if ($this->paragraphDisplaySettings->isApplicable($parent_entity)) {
      $elements[$delta]['#style_flags'] = $this->paragraphDisplaySettings->getSettings($parent_entity);
    }

    $this->renderer->addCacheableDependency($elements[$delta], $parent);

paragraphDisplaySettings is a custom service object that aids in evaluating different flags to pass through from the original code just to keep it cleaner and leaner. It's working great.

5
  • Adding the tags is a good idea, but I would attach them to the render array $renderer->addCacheableDependency($elements['delta'], $parent); Otherwise you get again a dependency on the object instance as discussed before. The risk is lower now because of the added cache keys, but I think the render array is a cleaner solution.
    – 4uk4
    Jul 20, 2019 at 8:23
  • Where is the $renderer object? I don't see that mentioned in EntityReferenceEntityFormatter or parent classes.
    – Kevin
    Jul 20, 2019 at 13:46
  • $renderer = \Drupal::service('renderer'); see drupal.org/docs/8/api/render-api/cacheability-of-render-arrays
    – 4uk4
    Jul 20, 2019 at 13:51
  • Ah okay, so I should inject the service to my formatter?
    – Kevin
    Jul 20, 2019 at 13:58
  • That works. Thanks again for your help, this was very insightful.
    – Kevin
    Jul 20, 2019 at 14:43

Your Answer

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

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