Controlling Block Visibility with a Custom Field in Drupal 8

Awhile back I wrote up a pattern for creating static blocks on Drupal 8 sites. This week I was working on a site where one of those blocks needs to be enabled or disabled on specific nodes at the discretion of the content author. To make this happen I’m adding a new feature to my pattern.

In older versions of Drupal there were a number of ways to go, like the PHP Filter, or custom handling in the block’s view hook, but I figured there were probably more appropriate tools for this in Drupal 8.  And I found what I needed in the Condition Plugin (more evidence that plugins are addictive). According to the change record they were designed to centralize a lot of the common logic used for controlling blocks, and I found it works quite nicely in this case as well (although a more generalized version might be useful).

I have the complete condition plugin at the end so you don’t have to get all the details exactly right as we go.

I started by adding a boolean field to the content type named field_enable_sidebar. Then I using drupal console generated the stub condition plugin:drupal generate:plugin:condition. In doing this the first time I also looked at the one defined in the core Node module to handle block visibility by content type.

The console will ask you a couple questions, obviously you can attach it to any module you’d like and call it whatever you’d like. For this example I have it in a fake module called my_blocks and the condition is named SidebarCondition. But the next couple questions are less obvious and more important.

Context Type should be entity

The context type should be set to entity since we are looking to work based on the node being displayed.

Context Entity Type should be Content

Next it’s trying to filter between entity types, and since we’re doing this based on content, select “Content” to get a list of content entities on your site.

Context Definition ID should be Content

Finally select “Content” again since that’s the label for your node content entities.  Of course if you have placed your field on another entity type, pick that entity here instead.

Once you run through the wizard you’ll have a new file in your module: my_blocks/src/Plugin/Condition/sidebarContition.php

The condition plugin contains two main elements: a form that’s attached to all block settings forms, and an evaluate function that is called by blocks to determine if this condition applies in their current context.

buildConfigurationForm() defines the form array elements you need. In this case that means a simple checkbox to indicate that this condition applies to this block. We also need to define submitConfiguration() to save the values on block save.

  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form['sidebarActive'] = [
      '#type'          => 'checkbox',
      '#title'         => $this->t('When Sidebar Field Active'),
      '#default_value' => $this->configuration['sidebarActive'],
      '#description'   => $this->t('Enable this block when the sidebar field on the node is active.'),
    ];
    return parent::buildConfigurationForm($form, $form_state);
  }

  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $this->configuration['sidebarActive'] = $form_state->getValue('sidebarActive');
    parent::submitConfigurationForm($form, $form_state);
  }

In the complete example you’ll see there is summary() which provides the human friendly description of the values that have been set for this condition.

Now let’s jump back to the top of the plugin and review the annotation. Conditions are annotated plugins and those questions I guided you through above were used to generate the annotation at the start of the file:

/**
 * Provides the 'Sidebar condition' condition.
 *
 * @Condition(
 *   id = "sidebar_condition",
 *   label = @Translation("Sidebar block condition"),
 *   context = {
 *     "node" = @ContextDefinition(
 *        "entity:node",
 *        required = TRUE ,
 *        label = @Translation("node")
 *     )
 *   }
 * )
 */

This is defining the context you’ll want passed to the condition for evaluation. In this case we are requiring that a node entity labeled “node” is provided when we need it.

The real work of the plugin is handled by evaluate():

  /**
   * Evaluates the condition and returns TRUE or FALSE accordingly.
   *
   * @return bool
   *   TRUE if the condition has been met, FALSE otherwise.
   */
  public function evaluate() {
    if (empty($this->configuration['sidebarActive']) && !$this->isNegated()) {
      return TRUE;
    }
    $node = $this->getContextValue('node');
    if ($node->hasField('field_enable_sidebar') && $node->field_enable_sidebar->value) {
      return TRUE;
    }
    return FALSE;
  }

The first condition ensures that this plugin doesn’t disable all blocks that aren’t using it. Next we ask for the context node value we defined in the annotation, which provides us the current node able to be displayed. Since not all node types are guaranteed to have our sidebar field we first check that it exists and then check its value and return the correct status for block display.

Now every time a user checks a box on the node, any blocks with this condition enabled will be displayed along with the node. And the best part is that the user doesn’t need to even have block display permissions, we’ve allowed them to bypass that part of the system entirely.

Time estimation: making up numbers as we go along.

Any experienced developer, and anyone who has worked with developers, knows that we’re terrible at estimating project times.  There are mountains of blog posts telling developers how to do estimates (spoiler alert, they are wrong), and at least as many telling project managers not to rely on the bad estimates from developers. Most of the honest advice doesn’t actually help you develop a number it helps you develop strategies to make a slightly better guess.

Any time I start to work with a new project manager on time estimates I try to make sure they understand any estimate is – at best – an educated guess, not a promise. I’ve learned to give ranges to imply inaccuracy and round up to offset my bias as a developer to underestimate (I recently noticed I’m frequently doing that too well and badly overestimating but that’s another story). However, that still lead to greater faith in the estimates than they deserve.

A few months ago I was asked about this topic by a PM I really enjoy working with and who was trying to work with me to find a better solution for our projects together. One of the articles I sent her was an old one from Joel Spolsky written in 2007. Re-reading the article I again drawn to his discussion of using Monte Carlo simulations to help come up with estimates about project duration. While he argues it helps increase accuracy, I mostly think it helps emphasis their lack of accuracy.

And since I’m a developer I wrote a simple tool to create project estimates that simulates how long a list of tasks might take (code on GitHub and pull requests are welcome). It’s nothing fancy, just a simple JavaScript tool that allows you to enter some tasks and estimates (or upload a CSV file) and run the simulation as many number of times you’d like.

Currently the purpose of it is more to encourage people to understand risk levels and ranges than to provide a figure to hang your hat on. Since estimates are bad, the tool is inherently garbage in garbage out. But I’m finding helpful in explaining to PMs about the fuzziness of the estimates. By showing a range of outcomes – including some that are very high (it assumes that your high-end estimate could be as low as ⅓ the total time needed on a task) – and providing a simple visualization of the data, it helps make it clear that estimates can be wrong, and added up error can blow a budget.

Histogram of time estimates.
This is the output from a recent set of estimates I was asked for, hopefully it’ll be good news

Please take some time to play around the tool and let me know what you think. It’s extremely rough at the moment, but if people find it useful I could polish some edges and add features.