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

31.01.2021
Drupal 8
Layout Builder

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

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

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

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

<?php

/**
 * @file
 * Defines base theme hooks.
 */

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);
  }

}