Динамический макет для Layout Builder

09 ноября 2020

Создание собственного макет для Layout Builder. У нашего макеты будут динамические регионы с возможностью задать им определенный классы, а так же добавим поле для ввода библиотек, которые будут подключены с этим макетом. Исходник тут

Как это будет работать

Видео файл

Для начала создаем модуль. Регистрируем hook_theme который будет использовать наш динамический макет.

/**
* Implements hook_theme().
*/
function dynamic_layout_theme() {
return [
'dynamic_layout' => [
'render element' => 'content',
'file' => 'dynamic_layout.theme.inc',
],
];
}

Создаем файл темы и добавляем функцию подготовки данных для рендера в шаблоне.

use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;

/**
* Prepares variables for "dynamic_layout" theme.
*
* Default template: dynamic-layout.html.twig.
*
* @param array $variables
* An associative array containing:
* - content: A source data element.
*/
function template_preprocess_dynamic_layout(array &$variables): void {
if (isset($variables['content']['#settings'])) {
$variables['settings'] = $variables['content']['#settings'];
}
foreach (Element::children($variables['content']) as $child) {
$variables['regions'][$child] = $variables['content'][$child];
if (!isset($variables['content'][$child]['#attributes'])) {
$variables['content'][$child]['#attributes'] = [];
}
$variables['region_attributes'][$child] = new Attribute($variables['content'][$child]['#attributes']);
}
$variables['container_class'] = $variables['settings']['container_class'] ?? 'layout';
}

Объявляем наш макет создав файл dynamic_layout.layouts.yml с содержимым 

dynamic_layout:
label: 'Dynamic layout'
category: Custom
class: Drupal\dynamic_layout\DynamicLayout
theme_hook: dynamic_layout
icon_map:
- [content]

Создаем шаблон

{% if regions %}
<div {{ attributes.addClass(container_class) }}>
{% for key, region in regions %}
<div{{ region_attributes[key].addClass(settings.regions[key].class) }}>
<div class="inner">
{{ region }}
</div>
</div>
{% endfor %}
</div>
{% endif %}

В заключении создадим класс, с помощью которого мы будем управлять нашим динамическим макетом

<?php

namespace Drupal\dynamic_layout;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\LayoutDefault;

/**
* Provides special class for dynamic layout.
*/
class DynamicLayout extends LayoutDefault {

/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'attach_libraries' => '',
'container_class' => '',
];
}

/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$configuration = $this->getConfiguration();

$form['container_class'] = [
'#type' => 'textfield',
'#title' => $this->t('Container class'),
'#default_value' => $configuration['container_class'],
];

$form['regions'] = [
'#type' => 'details',
'#title' => $this->t('Regions'),
'#tree' => TRUE,
'#open' => TRUE,
'#attributes' => [
'id' => 'regions-list',
],
];

if (!$form_state->get('regions')) {
$form_state->set('regions', $configuration['regions'] ?? [
['class' => ''],
]);
}
foreach ($form_state->get('regions') as $delta => $region) {
$form['regions'][$delta] = [
'#type' => 'fieldset',
];
$form['regions'][$delta]['class'] = [
'#type' => 'textfield',
'#title' => $this->t('Classes for :delta', [':delta' => $delta]),
'#default_value' => $region['class'],
];
}
$form['add_region'] = [
'#type' => 'submit',
'#value' => $this->t('Add region'),
'#submit' => [
'callback' => [$this, 'addRegion'],
],
'#ajax' => [
'callback' => [$this, 'addRegionCallback'],
'wrapper' => 'regions-list',
],
];

$form['attach_libraries'] = [
'#type' => 'textarea',
'#title' => $this->t('Attach libraries'),
'#description' => $this->t('Input every library from new line. "module_name(theme_name)/library_name"'),
'#default_value' => $configuration['attach_libraries'],
];

return parent::buildConfigurationForm($form, $form_state);
}

/**
* The ajax submit click handler.
*
* @param array $form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function addRegion(array $form, FormStateInterface $form_state): void {
$extra_regions = $form_state->get('regions');
$extra_regions[] = ['class' => ''];
$form_state
->set('regions', $extra_regions)
->setRebuild();
}

/**
* The ajax callback.
*
* @param array $form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The form element.
*/
public static function addRegionCallback(array $form, FormStateInterface $form_state): array {
return $form['layout_settings']['regions'];
}

/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['attach_libraries'] = $form_state->getValue('attach_libraries');
$this->configuration['regions'] = $form_state->getValue('regions');
$this->configuration['container_class'] = $form_state->getValue('container_class');
$this->setRegions();
}

/**
* {@inheritdoc}
*/
public function build(array $regions) {
$this->setRegions();
$build = parent::build($regions);

$attach_libraries = \explode("\n", $this->configuration['attach_libraries']);
$attach_libraries = \array_map('trim', $attach_libraries);
$attach_libraries = \array_filter($attach_libraries, 'strlen');
foreach ($attach_libraries as $attach_library) {
$build['#attached']['library'][] = $attach_library;
}
return $build;
}

/**
* Set regions to this layout instance.
*/
protected function setRegions(): void {
if (empty($this->configuration['regions'])) {
return;
}

$regions = [];
foreach ($this->configuration['regions'] as $delta => $region) {
$regions[$delta]['label'] = $this->t('Region :delta', [':delta' => $delta]);
}
$this->getPluginDefinition()->setRegions($regions);
}

}