.

Jak stworzyć niestandardowy layout? Dostosowanie Layout Buildera w Drupalu

Layout Builder umożliwia szybkie tworzenie układów stron z gotowych komponentów, dodawanych do sekcji. Standardowo Drupal dostarcza cztery typy sekcji - one, two, three i four columns. Kolumny te mają predefiniowane zachowanie, na które edytorzy nie mają wpływu. Drupal oferuje możliwość tworzenia własnych typów sekcji, dzięki czemu możemy je uszyć na miarę swojego projektu. Ten właśnie proces przybliżymy Ci w naszym artykule.

Jak stworzyć niestandardową sekcję w Layout Builderze?

Pierwszym i najważniejszym krokiem jest określenie założeń, a co za tym idzie listy funkcjonalności, które powinna dostarczać sekcja. Następnie warto rozbić funkcjonalności na małe zadania, możliwe do zrobienia w określonym czasie. Celem naszej sekcji będzie dostarczenie możliwości dodawania klas do głównego wrappera sekcji oraz do poszczególnych regionów.

Bazowo będziemy korzystać z szablonu dostępnego w samym module Drupala Layout Builder, czyli tego, który jest wykorzystywany w sekcjach dostępnych wraz z instalacją modułu. Na naszej liście zadań powinno znaleźć się:

  • stworzenie niestandardowego modułu,
  • definicja sekcji w pliku layouts.yml,
  • zdefiniowanie szablonu dla naszych sekcji oraz samego pluginu, w którym osadzimy logikę dodawania naszych klas.

Tworzenie nowego modułu w Drupalu

Należy stworzyć standardowy plik *.info.yml, jak w każdym module, po szeroki opis odsyłamy do dokumentacji na stronie Drupal.org.

# nazwa pliku: custom_layout_builder.info.yml

name: Custom Layout Builder sections
description: Functionality which extends Layout Builder
core_version_requirement: ^9
type: module
package: custom
dependencies:
  - drupal:layout_builder

Wiemy, jaki jest cel modułu, bo określiliśmy potrzebną funkcjonalność. Mamy więc pewność już na tym etapie, że na liście zależności (dependencies) powinien znaleźć się przynajmniej moduł Layout Builder. Po zdefiniowaniu pliku info.yml warto wyczyścić cache i zobaczyć, czy moduł pojawił się na liście. W tym celu należy przejść do widoku modułów i wyszukać moduł po tytule lub machine name. Powinniśmy zobaczyć nasz moduł wraz z listą wymaganych zależności.

Informacja o Twoim nowym module Drupala, która zawiera listę zależności

 

Jak można łatwo zauważyć, mimo że podaliśmy zależność jedynie do modułu Layout Builder, ich lista jest nieco dłuższa. Dzieje się tak, ponieważ sam moduł Layout Builder posiada własną listę zależności i jest ona dziedziczona przez nasz moduł.

Widok listy pokazującej wszystkie zależności modułu Drupala Layout Builder

 

Już na tym etapie warto wziąć pod uwagę zdrowie psychiczne innych programistów (lub swoje, jeśli wrócisz do tego kodu za parę miesięcy) i rozpocząć budowanie jego dokumentacji. Warto zacząć od implementacji hooka hook_help().

Przykładowa implementacja hooka hook_help() w kodzie nowego modułu Drupala

Tworzenie pliku README.md i jego stała aktualizacja jest również dobrym pomysłem.

Rejestracja sekcji przy użyciu *.layouts.yml

Aby zarejestrować nową sekcję, najprościej będzie dodać plik *.layouts.yml (gdzie * to machine name naszego modułu). Plik należy dodać w głównym folderze modułu, tam gdzie dodaliśmy plik *.info.yml.

Zacznijmy od zdefiniowania jednej sekcji:

# nazwa pliku: custom_layout_builder.layouts.yml

layout_custom_one_column:                 # Główny klucz sekcji
  label: '[CUSTOM] One column'            # Tytuł sekcji
  category: 'Custom layouts'
  path: layouts/custom_onecol_section     # Relatywna ścieżka do szablonu
  template: layout--custom-onecol-section # Nazwa szablonu
  default_region: first
  icon_map:
    - [first]
  regions:                                # Tablica regionów
    first:                                # Machine name regionu
      label: First                        # Tytuł regionu

Po konfiguracji, podczas dodawania sekcji powinniśmy być w stanie zobaczyć naszą nowo zdefiniowaną sekcję.

Tak wygląda świeżo zdefiniowana sekcja w nowym module w systemie Drupal

 

Definiowanie szablonu sekcji

Aby móc dodać sekcję, musimy jeszcze dodać szablon, którego nazwę i ścieżkę określiliśmy. W naszym przypadku musimy stworzyć folder layouts/custom_onecol_section, wewnątrz którego należy umieścić plik layout--custom-onecol-section.html.twig.

Domyślnie w szablonie będziemy mieli dostęp do czterech zmiennych: content, attributes, region_attributes oraz settings. Jeśli do sekcji nie wrzucimy żadnego bloku, zmienna content po rzutowaniu na wartość logiczną zwróci fałsz. Możemy wykorzystać to zachowanie, żeby nie wyświetlać pustych tagów HTML. Wewnątrz zmiennej content znajdziemy klucze, odpowiadające każdemu zdefiniowanemu regionowi, a wewnątrz tych regionów bloki, które dodaliśmy. W zmiennej content znajdziemy wyłącznie klucz first, gdyż tylko ten zdefiniowaliśmy. Zachowanie content.first przy rzutowaniu do wartości logicznej jest analogiczne do zachowania zmiennej content. Wykorzystamy to, aby nie wyświetlać pustych tagów.

# nazwa pliku: layout--custom-onecol-section.html.twig

{#
/**
 * @file
 * Default implementation for a custom layout onecol section.
 *
 * Available variables:
 * - content: The content for this layout.
 * - attributes: HTML attributes for the layout <div>.
 * - region_attributes: HTML attributes for the region <div>.
 * - settings: An array of configured settings for the layout.
 *
 * @ingroup themeable
 */
#}
{% if content %}
  <div{{ attributes }}>

    {% if content.first %}
      <div {{ region_attributes.first }}>
        {{ content.first }}
      </div>
    {% endif %}

  </div>
{% endif %}

Po dodaniu szablonu powinniśmy móc bez przeszkód dodać swoją sekcję:

Miejsce do dodania sekcji, widoczne po dodaniu szablonu do modułu Drupala

 

Definiowanie wtyczki Layoutu

Patrząc od strony użytkownika końcowego, można powiedzieć, że do tej pory nie zrobiliśmy nic, bo redaktor treści zobaczy jedynie nowy tytuł sekcji z wielkim prefixem [CUSTOM]. Jest tak dlatego, że sekcja którą dodaliśmy działa identycznie, co domyślna, dostarczona wraz z modułem Layout Builder (z małym wyjątkiem - nasza implementacja nie dodaje żadnych klas). Aby zmienić jej zachowanie, należy zaimplementować nowy layout plugin.

Szkielet klasy bazowej

Klasa powinna znaleźć się w folderze src/Plugin/Layout. Będzie na tyle generyczna, że może zostać wykorzystana dla dowolnej liczby regionów. Klasa Drupal\Core\Layout\LayoutDefault zawiera wiele bazowych metod i implementuje potrzebne interfejsy. Aby nie wymyślać koła na nowo, rozszerz ją w swojej klasie.

# nazwa pliku: CustomLayoutClassBase.php

<?php

namespace Drupal\custom_layout_builder\Plugin\Layout;

use Drupal\Core\Layout\LayoutDefault;

/**
 * Base class of our custom layouts with configurable HTML classes.
 *
 * @internal
 *   Plugin classes are internal.
 */
class CustomLayoutClassBase extends LayoutDefault {

}

Dodanie opcji konfiguracyjnych do klasy bazowej

Jednym z wymagań jest możliwość wybrania klasy dla taga wrapującego regiony sekcji. Aby to osiągnąć, należy w pierwszej kolejności nadpisać metodę defaultConfiguration i dodać do niej nową opcję konfiguracyjną.

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

Następnie należy też dać możliwość podania wartości dla tej opcji konfiguracyjnej. Możemy to zrobić nadpisując metody buildConfigurationForm oraz submitConfigurationForm.

/**
 * {@inheritdoc}
 */
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
  $form['wrapper_classes'] = [
    '#type' => 'textfield',
    '#title' => $this->t('Wrapper extra classes'),
    '#default_value' => $this->configuration['wrapper_classes'],
    '#description' => $this->t('Extra wrapper classes. Type as many as you want but remember to separate them by using a single space character.'),
  ];
  return parent::buildConfigurationForm($form, $form_state);
}

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

Jeśli zaistnieje potrzeba dopisania walidacji formularza, można to zrobić nadpisując metodę validateConfigurationForm. Rekomendujemy implementację walidacji dla tego pola, gdyż klasy powinny być zgodne ze standardem określonym przez Community Drupala. Może się przydać w tym przypadku metoda Html::getClass().

Wykorzystanie konfiguracji do budowania sekcji

Tablica renderowania jest budowana w metodzie build i to właśnie jej nadpisaniem teraz się zajmiemy. Jeśli pamiętasz zawartość szablonu, który dodaliśmy, to pewnie wiesz już, że klasy dodajemy do obiektu attributes.

/**
 * {@inheritdoc}
 */
public function build(array $regions): array {
  $build = parent::build($regions);
  $wrapper_classes = explode(' ', (string) $this->configuration['wrapper_classes']);
  $build['#attributes']['class'] = [...$wrapper_classes];

  return $build;
}

Użycie bazy do stworzenia Layout Pluginu

Nasza klasa jest już gotowa, pora na użycie jej w sekcji. W tym celu należy wrócić do pliku *.layouts.yml, żeby zadeklarować nowy plugin. Robi się to podając pełny namespace klasy pod kluczem class.

# nazwa pliku: custom_layout_builder.layouts.yml

layout_custom_one_column:
  label: '[CUSTOM] One column'
  category: 'Custom layouts'
  path: layouts/custom_onecol_section
  template: layout--custom-onecol-section
  class: '\Drupal\custom_layout_builder\Plugin\Layout\CustomOneColLayout'
  default_region: first
  icon_map:
    - [first]
  regions:
    first:
      label: First

Po wprowadzeniu powyższych zmian możesz zauważyć, że formularz sekcji posiada nowe pole oraz że klasy wpisane w to pole znajdują się w odpowiednim miejscu w HTML-u.

Oto widok nowo dodanego pola w formularzu w Drupalu

Fragment kodu HTML zawierający klasy, widoczny podczas tworzenia Layout Pluginu

Dodanie opcji wyboru klas dla regionów

Możemy już definiować listę klas dla elementu wrapującego w swojej sekcji. Pora zastanowić się, jak stworzyć logikę, odpowiadającą za dodanie klas do poszczególnych sekcji naszego layoutu. Należy wziąć pod uwagę rozszerzalność naszej klasy bazowej. Z tego powodu rekomendujemy oparcie logiki określania i dostępu do regionów na podstawie metody getRegionNames() z klasy LayoutDefinition.

1. Najpierw dodajemy po jednym polu do naszego formularza dla każdego regionu:

/**
 * {@inheritdoc}
 */
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
  $form['wrapper_classes'] = [
    '#type' => 'textfield',
    '#title' => $this->t('Wrapper extra classes'),
    '#default_value' => $this->configuration['wrapper_classes'],
    '#description' => $this->t('Extra wrapper classes. Type as many as you want but remember to separate them by using a single space character.'),
  ];

  foreach ($this->getPluginDefinition()->getRegionNames() as $region_name) {
    $form['region_classes'][$region_name] = [
      '#type' => 'textfield',
      '#title' => $this->t('Extra classes for @region_name region', [
        '@region_name' => $region_name,
      ]),
      '#default_value' => $this->configuration['region_classes'][$region_name],
      '#description' => $this->t('Extra classes for the @region_name region wrapper. Type as many as you want but remember to separate them by using a single space character.', [
        '@region_name' => $region_name,
      ]),
    ];
  }

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

2. Używamy analogicznej pętli do zapisu wartości:

/**
 * {@inheritdoc}
 */
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
  parent::submitConfigurationForm($form, $form_state);

  $this->configuration['wrapper_classes'] = $form_state->getValue('wrapper_classes');

  foreach ($this->getPluginDefinition()->getRegionNames() as $region_name) {
    $this->configuration['region_classes'][$region_name] = $form_state->getValue(['region_classes', $region_name], '');
  }
}

3. Ostatnim krokiem będzie nadpisanie metody build i osadzenie naszych klas w odpowiednim obiekcie klasy Attributes.

/**
 * {@inheritdoc}
 */
public function build(array $regions): array {
  $build = parent::build($regions);
  $wrapper_classes = explode(' ', (string) $this->configuration['wrapper_classes']);
  $build['#attributes']['class'] = [...$wrapper_classes];

  foreach (array_keys($regions) as $region_name) {
    $region_classes = explode(' ', (string) $this->configuration['region_classes'][$region_name]);
    $build[$region_name]['#attributes']['class'] = [...$region_classes];
  }

  return $build;
}

Po naszych najnowszych zmianach powinniśmy ujrzeć nowe pole Extra classes for first region, w którym możemy podać listę klas, które chcemy użyć.

Formularz edycji sekcji w Drupalu, w którym możemy dodać klasy i własną etykietę administracyjną

Należy pamiętać, że region pojawi się wyłącznie w przypadku, w którym nie będzie pusty. Z tego powodu dodaliśmy testowo blok zawierający tytuł node’a. Zobaczmy, czy klasy są widoczne w HTML-u.

Wycinek kodu HTML zawierający tagi i atrybuty sekcji dla Layout Pluginu

Tworzenie różnych wariantów sekcji

Kod został napisany w na tyle generyczny sposób, że dodanie sekcji z inną liczbą regionów wymaga od nas wyłącznie definicji regionu oraz szablonu. Dodajmy więc nową sekcję zawierającą dwa regiony.

Najpierw dodajemy definicję:

# nazwa pliku: custom_layout_builder.layouts.yml

layout_custom_one_column:
  label: '[CUSTOM] One column'
  category: 'Custom layouts'
  path: layouts/custom_onecol_section
  template: layout--custom-onecol-section
  class: '\Drupal\custom_layout_builder\Plugin\Layout\CustomLayoutClassBase'
  default_region: first
  icon_map:
    - [first]
  regions:
    first:
      label: First
layout_custom_two_columns:
  label: '[CUSTOM] Two columns'
  category: 'Custom layouts'
  path: layouts/custom_twocol_section
  template: layout--custom-twocol-section
  class: '\Drupal\custom_layout_builder\Plugin\Layout\CustomLayoutClassBase'
  default_region: first
  icon_map:
    - [first, second]
  regions:
    first:
      label: First
    second:
      label: Second

A następnie przygotowujemy szablon:

# nazwa pliku: layout--custom-twocol-section.html.twig

{#
/**
 * @file
 * Default implementation for a custom layout onecol section.
 *
 * Available variables:
 * - content: The content for this layout.
 * - attributes: HTML attributes for the layout <div>.
 * - region_attributes: HTML attributes for the region <div>.
 * - settings: An array of configured settings for the layout.
 *
 * @ingroup themeable
 */
#}
{% if content %}
  <div{{ attributes }}>

    {% if content.first %}
      <div {{ region_attributes.first }}>
        {{ content.first }}
      </div>
    {% endif %}

    {% if content.second %}
      <div {{ region_attributes.second }}>
        {{ content.second }}
      </div>
    {% endif %}

  </div>
{% endif %}

Sekcja powinna być już dostępna.

Lista dostępnych sekcji dla niestandardowego układu w Layout Builderze

 

Formularz konfiguracji powinien dopasować się automatycznie do liczby regionów.

Widok formularza konfiguracji sekcji z polami dla klas i etykiety administracyjnej

Po konfiguracji formularza, a także dodaniu testowych danych, możemy zobaczyć wynik naszej operacji w HTML-u.

Fragment kodu HTML zawierający wyniki operacji związanych z sekcją

Moduł stworzony w ramach tego tutorialu jest dostępny na naszym koncie na GitHubie.

Dostosowanie Layout Buildera - podsumowanie

Layout Builder to świetne narzędzie, którego API pozwala na pełną dowolność. Jak zawsze w przypadku Drupala - if you can dream it, you can build it. Przykład pokazany w tym artykule to jedynie mały wycinek tego, co można osiągnąć. Jeżeli interesuje Cię szersze użycie API Layout Buildera, warto poczytać o module Bootstrap Layout Builder.

Potrzebujesz niestandardowych ustawień w swoim systemie? Zobacz, jak możemy Ci pomóc w ramach naszych działań związanych z tworzeniem stron na Drupalu.

3. Najlepsze praktyki zespołów programistycznych