Steve Oliver

Web developer, Drupal Commerce specialist

Follow me on GitHub Drupal.org LinkedIn

First look at Drupal (8.5.0) Layout Builder module

I have been interested in but not actively tracking “Layouts in Drupal core”, so I decided to start catching up on the progress made here recently. I’m a long time fan of Panels and Page Manager, so I am expecting something along the same lines, met with something like the new Drupal 8 blocks architecture. Let’s see what we can do out of the box!

Install

At /admin/modules I search for “layout”, and find 3 modules.

  1. Layout Discovery (“Provides a way for modules or themes to register layouts.”) – It looks like this module will provide the necessary API that any of my custom layouts will need to interact with other parts of Drupal and Layouts.
  2. Field Layout (“Adds layout capabilities to the Field UI.”) – Sounds good. This sounds like an alternative to the Display Suite contributed module.
  3. Layout Builder (“Provides layout building utility.”) – OK. It sounds like this is the main UI workhorse. I expect it is something like Panelizer was in D7.

I enable all three modules:

Experimental modules warning when enabling Field Layout and Layout Builder

Now what?

Now that the three layout modules are enabled, I wonder how to use them. So I look around. In /admin/structure I see nothing new:

Drupal 8 Structure administration -- looking for Layouts

Hmmm… Maybe the goodies are hidden somewhere. Ah, whatever… let’s look at source code.

For Layout Discovery, I see no routing.yml file, so I look at layout_discovery.services.yml:

services:
  plugin.manager.core.layout:
    class: Drupal\Core\Layout\LayoutPluginManager
    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler']
  layout.icon_builder:
    class: Drupal\Core\Layout\Icon\SvgIconBuilder
    shared: false

Unsurprisingly, Layout Discovery provides a manager and icon builder to provide layouts and layout icons to the system.

Layout Builder does have a routing.yml file:

layout_builder.choose_section:
  path: '/layout_builder/choose/section/{section_storage_type}/{section_storage}/{delta}'
  defaults:
   _controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build'
  requirements:
    _permission: 'configure any layout'
  options:
    _admin_route: TRUE
    parameters:
      section_storage:
        layout_builder_tempstore: TRUE

layout_builder.add_section:
  path: '/layout_builder/add/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
  defaults:
    _controller: '\Drupal\layout_builder\Controller\AddSectionController::build'
  requirements:
    _permission: 'configure any layout'
  options:
    _admin_route: TRUE
    parameters:
      section_storage:
        layout_builder_tempstore: TRUE

...

There are several /layout_builder “child” routes, but I don’t see the entry point to a UI. So I look for the module’s .links.*.yml file(s). I see there is a links.contextual.yml and a links.task.yml, but no links.menu.yml. Without looking into the files, I know that the UI for Layout Builder is within the Tabs and Tasks links of some content or config entities. Let’s see.

layout_builder.links.contextual.yml:

layout_builder_block_update:
  title: 'Configure'
  route_name: 'layout_builder.update_block'
  group: 'layout_builder_block'
  options:
    attributes:
      class: ['use-ajax']
      data-dialog-type: dialog
      data-dialog-renderer: off_canvas

layout_builder_block_remove:
  title: 'Remove block'
  route_name: 'layout_builder.remove_block'
  group: 'layout_builder_block'
  options:
    attributes:
      class: ['use-ajax']
      data-dialog-type: dialog
      data-dialog-renderer: off_canvas

Interesting! What are these awesome attributes doing to my contextual links? :)

Let’s look at layout_builder.links.task.yml:

layout_builder_ui:
  deriver: '\Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver'

Alright, now let’s look at the Deriver for these tasks. LayoutBuilderLocalTaskDeriver.php:

<?php

namespace Drupal\layout_builder\Plugin\Derivative;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides local task definitions for the layout builder user interface.
 *
 * @todo Remove this in https://www.drupal.org/project/drupal/issues/2936655.
 *
 * @internal
 */
class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface {

  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a new LayoutBuilderLocalTaskDeriver.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('entity_type.manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    foreach ($this->getEntityTypesForOverrides() as $entity_type_id => $entity_type) {
      // Overrides.
      $this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
        'route_name' => "layout_builder.overrides.$entity_type_id.view",
        'weight' => 15,
        'title' => $this->t('Layout'),
        'base_route' => "entity.$entity_type_id.canonical",
        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
      ];
      $this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
        'route_name' => "layout_builder.overrides.$entity_type_id.save",
        'title' => $this->t('Save Layout'),
        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
      ];
      $this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
        'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
        'title' => $this->t('Cancel Layout'),
        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
        'weight' => 5,
        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
      ];
      // @todo This link should be conditionally displayed, see
      //   https://www.drupal.org/node/2917777.
      $this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
        'route_name' => "layout_builder.overrides.$entity_type_id.revert",
        'title' => $this->t('Revert to defaults'),
        'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
        'weight' => 10,
        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
      ];
    }

    foreach ($this->getEntityTypesForDefaults() as $entity_type_id => $entity_type) {
      // Defaults.
      $this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
        'route_name' => "layout_builder.defaults.$entity_type_id.view",
        'title' => $this->t('Manage layout'),
        'base_route' => "layout_builder.defaults.$entity_type_id.view",
      ];
      $this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
        'route_name' => "layout_builder.defaults.$entity_type_id.save",
        'title' => $this->t('Save Layout'),
        'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
      ];
      $this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
        'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
        'title' => $this->t('Cancel Layout'),
        'weight' => 5,
        'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
      ];
    }

    return $this->derivatives;
  }

  /**
   * Returns an array of entity types relevant for defaults.
   *
   * @return \Drupal\Core\Entity\EntityTypeInterface[]
   *   An array of entity types.
   */
  protected function getEntityTypesForDefaults() {
    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
    });
  }

  /**
   * Returns an array of entity types relevant for overrides.
   *
   * @return \Drupal\Core\Entity\EntityTypeInterface[]
   *   An array of entity types.
   */
  protected function getEntityTypesForOverrides() {
    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
    });
  }

}

So this creates Layout, Save Layout, Cancel Layout, Revert to defaults, and Manage Layout task links for all fieldable entities and entities that have canonical routes, meaning they control their own display.

Now we know where to find our Layout Builder UI. Let’s go.

Finding Layout Builder

Navigate to Admin > Structure > Content types > Basic page > Manage display:

Manage display page for Basic page content type

We see Manage layout and Layout options!

Using Layout Builder

I click Manage Layout and get this live-edit overlay type of UI:

The new Manage Layout UI provided by Layout Builder

Hey! There are those Save Layout and Cancel Layout local task links provided by the Deriver.

When I click Add section, a “Choose a layout” tray shows up on the right:

The 'Add Section' link shows the layout options available for a new section

Ah-ha! This must be where I would expect to see my layouts (if I had provided some).

When I click Two column, the live-edit is updated with a new section below the Add Section link I just clicked:

A new 2-column section is added to the Layout preview

When I click Add block, the right tray opens up again, but this time is has two things relevant to the Basic page node this Layout is for: 1. Node fields for this node type — awesome!, and 2. Blocks from throughout the system:

'Add block' provides the ability to place both entity fields as well as block entities.

I click to add the Authored by node field. The tray changes to load the options for this field:

Field settings for the 'Authored by' field being added to the Basic page content item layout

I’ve seen these settings before! These are the same settings I am used to seeing in the regular Manage display page for Nodes:

The regular 'Manage Display' page for nodes

In fact, I just realized that those fields’ settings form was actually completely replaced by the new “Manage Layout” button provided by the Layout Builder module!

I also notice that there are WAY MORE fields as “blocks” provided by Layout Builder, just like Panels used to provide.

After configuring my “field” “block” settings, I see the Authored by field is in the column I selected:

The node 'Authored by' field block is added to the Two-column layout section

Next I add the ‘Authored on’ field to the second column following the same steps:

The node 'Authored on' field block is added to the Two-column layout section

That’s enough! I’m dying to see it on a real node!

Save.

Click the 'Save layout' link

Bam. What?

Here she is:

2-column layout works!

Responsive, even!:

2-column layout is responsive!

Alright, that’s enough for now. I’m super excited!!

Nice work, everyone who has been working on this. I know @tim.plunkett and many others have been hard at work on this for a long while. Amazing, super clean work so far – I love it! I know there’s lots more to do, but this is such a powerful core feature for Drupal, I can’t wait to use it in production!

I intentionally didn’t search for existing content on Layouts because I wanted to see what my first experience with the module(s) would be. Afterward, I found that Lee Rowlands did a similar overview just last month, but as a screencast! Check it out, as it goes into a little more detail about layouts themselves and may be in an easier format to follow.

References: