Render API

La Render API è il sistema attraverso il quale Drupal trasforma dati strutturati, tipicamente un array, in output HTML.

Render array

Le proprietà più importanti per definire il modo in cui un render array può descrivere il proprio output sono:

  • #plain_text
  • #markup
  • #type
  • #theme

Quando il contenuto viene marcato come #plain_text significa che il valore deve essere trattato come testo e sottoposto a escaping HTML, è la scelta appropriata quando bisogna mostrare testo arbitrario senza markup.

Quando il contenuto viene marcato come #markup significa che nel contenuto sono permessi alcuni tag (non sono consentiti <script> e <style>), da utilizzare solo per markup semplice:

// Esempi corretti
$build['message'] = [ '#markup' => '<p>Forecast unavailable.</p>', ];

$build['message'] = [ '#markup' => $this->t( //verificare la correttezza di questo esempio perchè non c'è nessun tag
    'Forecast for @location', 
    [ 
        '@location' => $location, 
    ], 
  ), 
];

// Esempio errato:
$build['message'] = [ '#markup' => '<p>Forecast for ' . $location . '</p>', ];

Quando il contenuto viene marcato come #type significa che il contenuto è un render element plugin, ad esempio una tabella, un link o una lista:

// Esempio di tabella
$build['forecast'] = [
     '#type' => 'table', 
     '#header' => [ 
        $this->t('Date'), 
        $this->t('High'), 
        $this->t('Low'), 
     ], 
     '#rows' => $rows, 
];

//Esempio di link
$build['forecast_link'] = [ 
    '#type' => 'link', 
    '#title' => $this->t('View full forecast'), 
    '#url' => Url::fromRoute( 
        'nome_modulo.forecast', 
        [ 
          'display' => 'extended', 
        ], 
    ), 
];

//Esempio di lista
$build['items'] = [ 
    '#theme' => 'item_list', 
    '#title' => $this->t('Weather alerts'), 
    '#items' => $items, 
];

Quando il contenuto viene marcato come #theme significa che l'elemento deve essere renderizzato tramite un theme hook:

return [ 
  '#theme' => 'nome_modulo_forecast', 
  '#forecast' => $forecast, // variabile definita nel theme hook
  '#display' => $display, // variabile definita nel theme hook
];

Il theme hook definisce:

  • quali variabili possono essere ricevute
  • i relativi valori predefiniti
  • il template Twig da utilizzare
  • eventuali funzioni o metodi di preprocess

Theme hook

Nel file src/Hook/ThemeHooks.php:

<?php 

declare(strict_types=1);

namespace Drupal\nome_modulo\Hook;

use Drupal\Core\Hook\Attribute\Hook;

/** 
* Implements hooks related to rendered output. 
*/ 
final class ThemeHooks {

    /** 
    * Implements hook_theme(). 
    */
    #[Hook("theme")]
    public function theme(
        $existing, 
        $type, 
        $theme, 
        $path): array
    {
        return [
            "nome_modulo_forecast" => [
                "variables" => [
                   "forecast" => null, 
                   "display" => null
                ],
                "template" => "nome-modulo-forecast", //nome del file
            ],
            "nome_modulo_forecast_block" => [
                "variables" => [
                    "location" => null,
                    "days" => [],
                    "display" => "summary",
                    "temperature_unit" => null,
                    "forecast_link" => [],
                    "error_message" => null,
                ],
                "template" => "nome-modulo-forecast-block", // nome del file
            ],
        ];
    } 

  /** 
  * Implements hook_preprocess_nome_modulo_forecast(). 
  */
    #[Hook("preprocess_nome_modulo_forecast")]
    public function preprocessForecast(array &$variables): void {
        $variables["#attached"]["library"][] = 
          "nome_modulo/forecast";
    } 

    /** 
    * Implements hook_preprocess_nome_modulo_forecast_block(). 
   */
    #[Hook("preprocess_nome_modulo_forecast_block")]
    public function preprocessForecastBlock(array &$variables): void {
        $variables["#attached"]["library"][] = 
          "nome_modulo/forecast";
    }
}

Nel file templates/nome-modulo-forecast.html.twig:

{# 
/** 
* @file 
* Default theme implementation for the full weather forecast. 
* 
* Available variables:
 * - forecast: Normalized forecast data, or null. 
* - display: Display mode, either "summary" or "extended". 
*/ 
#}

{% set forecast_days = 
    forecast and forecast.days is defined 
    ? forecast.days 
    : [] 
%}

<section
  class="nome-modulo-forecast"
  data-nome-modulo-forecast
  data-display="{{ display }}">

  {% if forecast and forecast_days is not empty %} 
  <header class="nome-modulo-forecast__header"> 
     <h2 class="nome-modulo-forecast__title"> 
         {{ 'Forecast for @location'|t({ 
             '@location': forecast.location 
            }) 
          }} 
      </h2> 
   </header>

 {% set first_day = forecast_days|first %}
  
<article class="nome-modulo-forecast__day nome-modulo-forecast__day--summary"> 
    <h3 class="nome-modulo-forecast__date"> 
       <time datetime="{{ first_day.date }}"> 
          {{ first_day.date }} 
       </time> 
     </h3>

    <dl class="nome-modulo-forecast__values"> 
      <div class="nome-modulo-forecast__value"> 
          <dt>{{ 'Weather code'|t }}</dt> 
          <dd>{{ first_day.weather_code }}</dd> 
       </div>

    <div class="nome-modulo-forecast__value"> 
         <dt>{{ 'High'|t }}</dt> 
         <dd> {{ first_day.high }} {{ forecast.temperature_unit }} </dd> 
     </div>

    <div class="nome-modulo-forecast__value"> 
        <dt>{{ 'Low'|t }}</dt> 
        <dd> {{ first_day.low }} {{ forecast.temperature_unit }} </dd> 
     </div> 
  </dl> 
</article>

{% if forecast_days|length > 1 %}
<div
  class="nome-modulo-forecast__details"
  data-forecast-details
  {% if display == "summary" %}hidden{% endif %}
>
  {% for day in forecast_days|slice(1) %}
  <article class="nome-modulo-forecast__day">
    <h3 class="nome-modulo-forecast__date">
      <time datetime="{{ day.date }}"> 
          {{ day.date }} 
       </time>
    </h3>

    <dl class="nome-modulo-forecast__values">
      <div class="nome-modulo-forecast__value">
        <dt>{{ 'Weather code'|t }}</dt>
        <dd>{{ day.weather_code }}</dd>
      </div>

      <div class="nome-modulo-forecast__value">
        <dt>{{ 'High'|t }}</dt>
        <dd>
             {{ day.high }} 
             {{ forecast.temperature_unit }}
        </dd>
      </div>

      <div class="nome-modulo-forecast__value">
        <dt>{{ 'Low'|t }}</dt>
        <dd>
          {{ day.low }} 
          {{ forecast.temperature_unit }}
        </dd>
      </div>
    </dl>
  </article>
  {% endfor %}
</div>

    {% if display == 'summary' %}
     <button
     class="nome-modulo-forecast__toggle" 
     type="button" aria-expanded="false" 
     data-forecast-toggle 
     data-show-label="{{ 'Show extended forecast'|t }}" 
     data-hide-label="{{ 'Hide extended forecast'|t }}"
    >
        {{ 'Show extended forecast'|t }}
     </button> 
    {% endif %} 
   {% endif %} 
{% else %}
<p class="nome-modulo-forecast__message"> 
    {{ 'The weather forecast is temporarily unavailable.' |t }} 
</p> 
{% endif %} 
</section>

Nel file templates/nome-modulo-forecast-block.html.twig:

{# 
/** 
* @file 
* Default theme implementation for the weather forecast block. 
* 
* Available variables: 
* - location: Human-readable location. 
* - days: Forecast days displayed by this block. 
* - display: Display mode. 
* - temperature_unit: Unit returned by the provider. 
* - forecast_link: Render array containing the full forecast link. 
* - error_message: Translatable error message, or null. 
*/ 
#}

<div 
   class=" nome-modulo-forecast-block nome-modulo-forecast-block--{{ display|clean_class }} " 
>
   {% if error_message %} 
      <p class="nome-modulo-forecast-block__message"> 
          {{ error_message }} 
      </p> 
    {% else %}
      {% if location %} 
        <h2 class="nome-modulo-forecast-block__title"> 
           {{ 'Forecast for @location'|t({ '@location': location }) }} 
         </h2> 
       {% endif %}

      {% if days is not empty %} 
         <ul class="nome-modulo-forecast-block__days"> 
             {% for day in days %} 
                 <li class="nome-modulo-forecast-block__day"> 
                     <time 
                        class="nome-modulo-forecast-block__date" 
                        datetime="{{ day.date }}" 
                         > 
                         {{ day.date }} 
                     </time>
                     <span class="nome-modulo-forecast-block__temperature"> 
                             {{ 'High: @high, low: @low' 
                                |t({ 
                                    '@high': day.high ~ ' ' ~ temperature_unit, 
                                     '@low': day.low ~ ' ' ~ temperature_unit 
                                 }) 
                             }} 
                       </span> 
                  </li> 
              {% endfor %} 
        </ul> 
      {% endif %}

     {% if forecast_link %} 
         <div class="nome-modulo-forecast-block__link"> 
              {{ forecast_link }} 
          </div> 
       {% endif %} 
     {% endif %} 
</div>

Librerie del modulo

Nel file nome_modulo.libraries.yml:

forecast: 
  css: 
    component: 
      css/forecast.css: {} 
    js: 
       js/forecast.js: {} 
  dependencies: 
    - core/drupal  # altrimenti non potremmo usare Drupal.t() e Drupal.formatPlural()
    - core/once  # fornisce solo la funzione once()

Per le librerie CSS queste possono essere di 5 categorie, importanti perchè determinano l'ordine di caricamento dei files:

  • base: reset ed elementi HTML di base
  • layout: struttura generale e griglie
  • component: componenti UI riutilizzabili
  • state: stati dinamici del componente
  • theme: aspetto puramente visivo

Nel file js/forecast.js:

(function (Drupal, once) {
  "use strict";
  Drupal.behaviors.nomeModuloForecastToggle = {
    attach(context) {
      once(
        "nome-modulo-forecast-toggle",
        "[data-nome-modulo-forecast]",
        context,
      ).forEach((forecast) => {
        const button = forecast.querySelector("[data-forecast-toggle]");
        const details = forecast.querySelector("[data-forecast-details]");
        if (!button || !details) {
          return;
        }
        const showLabel =
          button.dataset.showLabel || Drupal.t("Show extended forecast");
        const hideLabel =
          button.dataset.hideLabel || Drupal.t("Hide extended forecast");
        button.addEventListener("click", () => {
          const isExpanded = button.getAttribute("aria-expanded") === "true";
          button.setAttribute("aria-expanded", String(!isExpanded));
          details.hidden = isExpanded; // per indicare che i dettagli non devono essere esposti alle tecnologie assistive
          button.textContent = isExpanded ? showLabel : hideLabel;
          forecast.classList.toggle("is-expanded", !isExpanded);
        });
      });
    },
  };
})(Drupal, once);