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.
- 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.
- Field Layout (“Adds layout capabilities to the Field UI.”) – Sounds good. This sounds like an alternative to the Display Suite contributed module.
- 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:
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:
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
:
We see Manage layout and Layout options!
Using Layout Builder
I click Manage Layout and get this live-edit overlay type of UI:
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:
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:
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:
I click to add the Authored by node field. The tray changes to load the options for this field:
I’ve seen these settings before! These are the same settings I am used to seeing in 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:
Next I add the ‘Authored on’ field to the second column following the same steps:
That’s enough! I’m dying to see it on a real node!
Save.
Here she is:
Responsive, even!:
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.