How to make a title block with a custom background

A common request I’ve received lately when building sites in Drupal is to make the background of the page title configurable to allow the user upload and change it.

Of course, this could be solved with a new block type with two fields, one for the image and one for the title. But then we would need to add the block for each page where we want to show the title.

How does Drupal handle page titles? Looking in the block admin page, I found that actually it is a block. Well, we need the same functionality of that block in our new one.

So looking for the class that contains the block machine name page_title_block I found Drupal\Core\Block\Plugin\Block\PageTitleBlock which implements TitleBlockPluginInterface. This is all the interface:

    interface TitleBlockPluginInterface extends BlockPluginInterface {

      /**
       * Sets the title.
       *
       * @param string|array $title
       *   The page title: either a string for plain titles or a render array for
       *   formatted titles.
       */
      public function setTitle($title);

    }

Searching what uses that interface, I found that BlockPageVariant uses it to assign it a title:

    foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata_list) as $region => $blocks) {
          /** @var $blocks \Drupal\block\BlockInterface[] */
          foreach ($blocks as $key => $block) {
            $block_plugin = $block->getPlugin();
            if ($block_plugin instanceof MainContentBlockPluginInterface) {
              $block_plugin->setMainContent($this->mainContent);
              $main_content_block_displayed = TRUE;
            }
            elseif ($block_plugin instanceof TitleBlockPluginInterface) {
              $block_plugin->setTitle($this->title);
            }
      //...

This means that if I create a custom block that implements TitleBlockPluginInterface I will get the page title automatically (thanks to BlockPageVariant)? Apparently, yes.

Lets start writing the custom block extending PageTitleBlock to use the title field implementation:

    /**
     * @Block(
     *   id = "my_image_title_block",
     *   admin_label = "Image title block"
     * )
     */
    class ImageTitleBlock extends PageTitleBlock {

      /**
       * The image field.
       *
       * @var mixed
       */
      protected $image;

      public function defaultConfiguration() {
        return [
          'image' => [
            'value' => '',
          ],
          'label_display' => FALSE,
        ] + parent::defaultConfiguration();
      }

      /**
       * {@inheritdoc}
       */
      public function build() {
        $build = [];

        if (isset($this->configuration['image'])
          && !empty($this->configuration['image'])) {

          $image_field = $this->configuration['image'];
          $image_uri = File::load($image_field[0]);

          $build['image'] = [
            'uri' => ImageStyle::load('title_bar')->buildUrl($image_uri->getFileUri()),
          ];
        } else {
          $build['image']['#markup'] = '[' . t('Picture') . ']';
        }


        return $build + parent::build();
      }

      /**
       * {@inheritdoc}
       */
      public function blockForm($form, FormStateInterface $form_state) {

        $form['image'] = array(
          '#type' => 'managed_file',
          '#upload_location' => 'public://images/',
          '#title' => $this->t('Image'),
          '#description' => $this->t("Background image"),
          '#default_value' => $this->configuration['image'],
          '#upload_validators' => array(
            'file_validate_extensions' => array('gif png jpg jpeg'),
            'file_validate_size' => array(25600000),
          ),
          '#states' => array(
            'visible' => array(
              ':input[name="image_type"]' => array('value' => t('Upload New Image')),
            )
          )
        );

        return $form;
      }

      public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
        if (!$form_state->getValue('image')) {
          $form_state->setError($form['image'], $this->t('Select an image'));
        }
      }

      /**
       * {@inheritdoc}
       */
      public function blockSubmit($form, FormStateInterface $form_state) {
        /* Fetch the array of the file stored temporarily in database */
        $image = $form_state->getValue('image');

        $this->configuration['image'] = $image;

        /* Load the object of the file by it's fid */
        $file = \Drupal\file\Entity\File::load($image[0]);

        /* Set the status flag permanent of the file object */
        $file->setPermanent();

        /* Save the file in database */
        $file->save();
      }
    }

After clearing Drupal cache, you will find your new shiny block waiting for you in the Add block list. The build method returns the processed image URL (using title_bar style) instead of the render array for an image so we can use it later.

Let’s create title_bar image style and configure it accordingly (in my case, resize and crop to a predefined size will do).

Then, update your theme to make sure that image url shows in a style attribute, or a data-* if you’re using javascript to load the image.

block—image-title-block.html.twig

    <div{{ attributes.setAttribute('style',
        'background: #787c8a url(' ~ content.image.uri ~ ') no-repeat center;') }}>
      {{ title_prefix }}
      {% if label %}
        <h2{{ title_attributes }}>{{ label }}</h2>
      {% endif %}
      {{ title_suffix }}
      {% block content %}
        {{ content }}
      {% endblock %}
    </div>
    ...

Lets test it!