Skip to content

Layouts and Layout Builder

Pinto Layout exists to easily create Drupal Layouts, as in Layout Discovery (layout_discovery.module), as used in Layout Builder, etc.

Pinto Layout supports defining Layouts in multiple ways. The method you choose will depend on how you want your Components to be designed, and thusly is a preference than a rule.

Because both Pinto and Pinto Layout are so flexible with how components are set up, there are an arguably overwhelming amount of options. You'll find modified lines-of-code to be quite small however.

Site configuration

Most setups require the following to be added to site settings.php:

php
$settings['twig_sandbox_allowed_classes'] = [
    \Drupal\Core\Template\Attribute::class,
    \Drupal\pinto_layout\PintoLayout\Data\RegionAttributes::class,
];

Factory methodologies

  • Constructor as factory
    • Use regular class constructors (__construct) as the factory.
  • Component implements PintoLayoutInterface
    • Component implements \Drupal\pinto_layout\PintoLayout\PintoLayoutInterface.
    • The component can use any factory method, but implement PintoLayoutInterface and its required createForLayout method. This method is responsible for creating an instance of the component, and transferring region values into the component.
    • Regions are defined on the class itself with #[Regions] attribute.
  • Defined factory method
    • Component uses #[LayoutDefinition] with factoryMethod above the class:
    • Just like the PintoLayoutInterface method above, but with a method reference instead of an interface.
  • Externally Mapped
    • A component can be designated as a Layout without adding any Drupal or Layout concepts by setting up mappings externally.
    • A service is added, the service implements \Drupal\pinto_layout\PintoLayout\External\ExternallyDefinedInterface
    • ExternallyDefinedInterface::getDefinitions() generates mappings. These mappings include references to the component, and region definitions, among other things.
    • A static factory method is defined anywhere, which produces a component. A mapping of region values is provided, similar to PintoLayoutInterface::createForLayout in the previous method.

Choose the first option if you use a public constructor with a limited set of slots, otherwise choose the second or third option if you don't mind mixing Layout concepts in with your component. Choose the fourth option to keep components free of Layout concepts.

If the component is not a Slots component type, only Externally Mapped may be used.

Constructor as factory

Setup and configure Pinto components as usual. Add regions as parameters, and add the #[Drupal\pinto_layout\Attribute\Region] attribute adjacent to the regions.

TIP

All non-region arguments of the constructor must have a default value.

Properties typed with \Drupal\pinto_layout\PintoLayout\Data\RegionAttributes will be supplied with attributes used for the layout container, and wrappers for each region. These attributes are necessary to work with the Layout Builder editor, but are otherwise unused on render or outside of Layout Builder.

php
final class Obj {
  use DrupalInvokableSlotsTrait;

  public function __construct(
    #[Region]
    public readonly mixed $region1,
    #[Region]
    public readonly mixed $region2,
    public RegionAttributes $regionAttributes,
  ) {
  }
}

Associated Twig template:

twig
<div {{ regionAttributes.containerAttributes() }}>
  <div {{ regionAttributes.regionAttributes('region1') }}>
    {{ region1 }}
  </div>
  <div {{ regionAttributes.regionAttributes('region2') }}>
    {{ region2 }}
  </div>
</div>

Component implements PintoLayoutInterface

Setup and configure Pinto components as usual.

Modify the component to implement the interface, and define regions above the class with \Drupal\pinto_layout\Attribute\Regions attribute.

php
#[Regions([
  'region1', 
  'region2', 
])]
final class Obj implements PintoLayoutInterface { 
  use DrupalInvokableSlotsTrait;

  public function __construct(
    public readonly mixed $region1,
    public readonly mixed $region2,
    public RegionAttributes $regionAttributes,
  ) {
  }
  
  public static function createForLayout(LayoutData $layoutData): static { 
    return new static(
      region1: $layoutData->regionsData->getRegion('slot1'),
      region2: $layoutData->regionsData->getRegion('slot2'),
      regionAttributes: $layoutData->regionAttributes,
    ); 
  } 
}

Associated Twig template:

twig
<div {{ regionAttributes.containerAttributes() }}>
  <div {{ regionAttributes.regionAttributes('region1') }}>
    {{ region1 }}
  </div>
  <div {{ regionAttributes.regionAttributes('region2') }}>
    {{ region2 }}
  </div>
</div>

Defined factory method

Setup and configure Pinto components as usual.

Implement a public static method, which takes a LayoutData parameter or has parameters with the same names as the regions.

Add #[LayoutDefinition] attribute above the class.

Configure #[LayoutDefinition($factoryMethod) to refer to the method.

Regions can be defined with #[Region] property, or #[LayoutDefinition($regions)].

php
#[LayoutDefinition(
  factoryMethod: 'customFactory', 
)]
final class Obj { 
  use DrupalInvokableSlotsTrait;

  public function __construct(
    #[Region]
    public readonly mixed $region1,
    #[Region]
    public readonly mixed $region2,
    public RegionAttributes $regionAttributes,
  ) {
  }
  
  public static function customFactory(LayoutData $layoutData): static { 
    return new static(
      region1: $layoutData->regionsData->getRegion('slot1'),
      region2: $layoutData->regionsData->getRegion('slot2'),
      regionAttributes: $layoutData->regionAttributes,
    ); 
  } 
}

Associated Twig template:

twig
<div {{ regionAttributes.containerAttributes() }}>
  <div {{ regionAttributes.regionAttributes('region1') }}>
    {{ region1 }}
  </div>
  <div {{ regionAttributes.regionAttributes('region2') }}>
    {{ region2 }}
  </div>
</div>

Alternative with parameters

php
public static function customFactory(
  mixed $slot1,
  mixed $slot2, 
  RegionAttributes $regionAttributes, 
): static {
  return new static( 
    region1: $slot1, 
    region2: $slot2, 
    regionAttributes: $regionAttributes, 
  ); 
}

Externally Mapped

Setup and configure Pinto components as usual.

No Layout-specific modifications are required.

php
final class Obj { 
  use DrupalInvokableSlotsTrait;

  public function __construct(
    public readonly mixed $region1,
    public readonly mixed $region2,
    public RegionAttributes $regionAttributes,
  ) {
  }
}

Associated Twig template:

twig
<div {{ regionAttributes.containerAttributes() }}>
  <div {{ regionAttributes.regionAttributes('region1') }}>
    {{ region1 }}
  </div>
  <div {{ regionAttributes.regionAttributes('region2') }}>
    {{ region2 }}
  </div>
</div>

Definitions service

php
namespace Drupal\my_module;

final class PintoLayoutDefinitions implements ExternallyDefinedInterface {
  public function getDefinitions(): iterable {
    yield ExternallyDefined::create(
      id: 'layout_id_for_component',
      label: new TranslatableMarkup('Layout for component'),
      pintoEnum: PintoList::Obj,
      regions: [
        'region1',
        'region2',
      ],
      factoryMethod: [static::class, 'createLayoutObject'],
    );
  }

  public static function createLayoutObject(LayoutData $layoutData): Obj {
    return new Obj(
      region1: $layoutData->regionsData->getRegion('region1'),
      region2: $layoutData->regionsData->getRegion('region2'),
      regionAttributes: $layoutData->regionAttributes,
    );
  }
}

The factory method can be located anywhere. In this example it is colocated in the definition service for simplicity.

ExternallyDefined::create($pintoEnum) is a reference to the enum case, as in Lists.

ExternallyDefined::create($regions) can use the Regions type to customise region ID and labels independently.

Service setup

Set up class as a service. Critically, autoconfigure is set on the service:

yaml
services:
  _defaults:
    autoconfigure: true
    autowire: true
    public: false

  Drupal\my_module\PintoLayoutDefinitions:
    public: true

Replicating Region Attributes from Core

The preferred way to use RegionAttributes is as a single all-in-one variable. But in some cases you may need to match the same $region_attributes Twig variable design as Cores' Layout Discovery.

The first example is modified accordingly:

php
final class Obj {
  use DrupalInvokableSlotsTrait;

  public function __construct(
    #[Region]
    public readonly mixed $region1,
    #[Region]
    public readonly mixed $region2,
    public RegionAttributes $region_attributes,
    public array $attributes = [],
  ) {
  }

  protected function build(Slots\Build $build): Slots\Build {
    return $build
      ->set('attributes', $this->regionAttributes->containerAttributes()) 
      ->set('region_attributes', $this->regionAttributes->regionsAsArray()); 
  }
}

Associated Twig template:

html
<div {{ attributes }}>
  <div {{ region_attributes.region1 }}>
    {{ region1 }}
  </div>
  <div {{ region_attributes.region2 }}>
    {{ region2 }}
  </div>
</div>

Customising Layout ID

Layout ID's are automatically generated, but you may want to customise them as these ID's are stored and serialized in content.

php
#[LayoutDefinition(
  id: 'a_layout',
)]
final class Obj {

For externally defined layouts, this option is available at ExternallyDefined::create($id).

Customising Layout Label

Layout labels are automatically generated by default, but you may want to customise them. These labels are shown when choosing a layout in Layout Builder, among other places.

php
#[LayoutDefinition(
  label: new TranslatableMarkup('Custom Layout Label'),
)]
final class Obj {

For externally defined layouts, this option is available at ExternallyDefined::create($label).