Display-only Pseudo Fields in Drupal 8
In the past I’ve been happy to rely on Display Suite to create what I call “Frontend Only” (DSField) fields (which I created via php). These fields appeared in the Manage Displays screens so that site admins can easily drag them around in the display as needed. As it turns out, you can achieve nearly the same result using a few built-in hooks in Drupal 8. The Display Suite DSField plugin has a few handy helpers like the ability to only show the field on specific display mode forms (e.g., show the field on Full content but not on Teaser). You can still control whether or not the display-only field renders if you use the core hooks, so we’ll be okay.
[UPDATE January 30, 2020] After some discussions with a colleague I decided the world needs a Plugin-based approach to the solution below. We talked through how it’d work before going about our day. After writing the info file, .module file, and prepping the folder structure I realized I never checked to see if this already existed.
I was pleasantly surprised to see that Sutharsan had already created a module that fits the bill! Extra Field is nearly identical in structure and implementation as what I’d planned to build. I just finished writing my first ExtraField\Display plugin and it works beautifully! I will leave my post below in tact, but I highly recommend stopping here and simply using Extra Field. Note that the README.txt file is worth reading, and there is an extra_field_example sub-module that explains things quite well.
Before I show the code, let me give some background.
This is a paragraph bundle with a machine name of news3col. This bundle has a News Items field and a Tag field. If the user picks items via the former, the latter won’t render. If the user does not pick News Items but does pick a tag we load a View filtered on the chosen tag. Lastly, if using tags, we render a More link to the listing page filtered on the tag, otherwise we render a More link to the listing page without the tag filter.
Below you’ll see what the Manage Display screen looks like. Notice how I include the name of the module where the custom field comes from; this makes it easy to track down the source.
To get these custom fields to show up on the Manage Display screen you can use hook_entity_extra_fields_info(). These should go in your custom module’s .module file. My module is called bps.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * Implements hook_entity_extra_field_info(). */ function bps_entity_extra_field_info() { $display_fields = []; $display_fields['paragraph']['news3col']['display']['bps_news3col_view_by_tag'] = [ 'label' => t('Views: News 3 columns by tag (bps.module)'), 'description' => t('Shows latest news by tag using the news.news3col View'), 'weight' => 100, 'visible' => TRUE, ]; $display_fields['paragraph']['news3col']['display']['bps_news3col_more_link'] = [ 'label' => t('More link (bps.module)'), 'description' => t('Shows a link to the news.listing View'), 'weight' => 101, 'visible' => TRUE, ]; return $display_fields; } |
After a cache rebuild the two fields will appear.
Next, we need to make them do something. This is done via hook_ENTITY_TYPE_view().
These functions show a few things:
- How to check for a component on an entity and do something if it exists
- How to render a view as a pseudo display-only field
- How to check if an entity field has a value
- How to render a link to a URL with parameters
- How to pass contextual filter values via query parameters
- My listing view is a block that lives on a basic page at /news (not a Views-based page).
- This block has a “Has Taxonomy Term ID” filter with “Provide default value” set to “Query parameter”. This is how I am able to affect filters via the URL (like /news?tags=5).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
/** * Implements hook_ENTIIY_TYPE_view(). */ function bps_paragraph_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { if ($display->getComponent('bps_news3col_view_by_tag')) { bps_news3col_add_field_bps_news3col_view_by_tag($build, $entity); } if ($display->getComponent('bps_news3col_more_link')) { bps_news3col_add_field_bps_news3col_more_link($build, $entity); } } /** * Adds output from the news.news3col View to the paragraph. * * @param array $build * Render array for the paragraph bundle (fields). * @param \Drupal\Core\Entity\EntityInterface $entity * Paragraph entity. */ function bps_news3col_add_field_bps_news3col_view_by_tag(array &$build, EntityInterface $entity) { if (empty($build['field_news3col_items'][0])) { if ($view = Views::getView('news')) { $view->setDisplay('news3col'); $args = []; if (isset($entity->field_news3col_tags->target_id)) { $args[] = $entity->field_news3col_tags->target_id; } $build['bps_news3col_view_by_tag'] = [ '#type' => 'view', '#name' => 'news', '#display_id' => 'news3col', '#arguments' => $args, ]; } } } /** * Adds a "More" link pointing to news.news3col View with tag param if by tag. * * @param array $build * Render array for the paragraph bundle (fields). * @param \Drupal\Core\Entity\EntityInterface $entity * Paragraph entity. */ function bps_news3col_add_field_bps_news3col_more_link(array &$build, EntityInterface $entity) { if (!empty($build['field_news3col_items'][0])) { $url = Url::fromUserInput('/news'); } else { if (isset($entity->field_news3col_tags->target_id)) { $url = Url::fromUserInput('/news', ['query' => ['tags' => $entity->field_news3col_tags->target_id]] ); } else { $url = Url::fromUserInput('/news'); } } $link = Link::fromTextAndUrl(t('More'), $url); $build['bps_news3col_more_link'] = $link->toRenderable(); } |
1 2 3 4 5 6 7 8 9 10 11 |
{% if content.bps_news3col_view_by_tag %} <div class="news3col__items--by-tag"> {{ content.bps_news3col_view_by_tag }} </div> {% endif %} {% if content.bps_news3col_more_link %} <div class="news3col__more"> {{ content.bps_news3col_more_link }} </div> {% endif %} |
What About Twig for Everything?
Great question! I too wondered this, so I attempted to meet my goals with Twig and Twig Tweak.
I was able to achieve the exact same results with no PHP and the following twig template code (replaces what’s shown just above this section):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{% if content.field_news3col_items.0 %} <div class="news3col__items"> {{ content.field_news3col_items }} </div> {% else %} {% set tag = 'all' %} {% if paragraph.field_news3col_tags.0.target_id %} {% set tag = paragraph.field_news3col_tags.0.target_id %} {% endif %} <div class="news3col__items--by-tag"> {{ drupal_view('news', 'news3col', tag) }} </div> <div class="news3col__more"> {{ drupal_link('More'|t, 'news?tags=' ~ tag) }} </div> {% endif %} |
So, which will I ultimately commit to the repo? Pseudo-fields, or Twig?
I prefer the transparency of the pseduo-field, php-based approach. The site admins can clearly see that we’re introducing a few extra “Fields” via PHP. If we brought the view into play via Twig it wouldn’t be too obvious why a view was being rendered. With that said, there is a lot more code in the pseudo-field solution. I’m on the fence, especially if I drop a warning message on the display mode edit screen suggesting that there is some logic in a twig template.