Creando atributos directamente en Twig

El objeto attributes es uno de los helpers más conocidos y usados en el front-end de Drupal. Básicamente, cualquier tipo de array de renderizado puede incluir este objeto con el que gestionar los atributos HTML a insertar en un elemento: classes, ids, data-attributes, aria-attributes, microdata, etcétera.

Expresar los atributos del elemento en un objeto aparte nos da dos grandes beneficios:

  • Permite alterar los atributos antes del renderizado. Por ejemplo, si un módulo añade tres clases que colisionan con tu CSS, es sencillo retirarlas. Muchísimo más que si recibiéramos un HTML ya formateado, del tipo <div class="my-class my-second-class">.
  • Permite exponer toda una serie de métodos para el manejo de atributos con una sintaxis mucho más sencilla eintuitiva: attributes.addClass(), attributes.hasClass(), attributes.removeAttributes(), etc...

No obstante, hasta Drupal 8.3 el objeto attributes provenía del back-end y casi siempre era único (para el envoltorio principal). Afortunadamente, en Drupal 8.3 se introdujo la función create_attribute() que permite crear un objeto al vuelo directamente en Twig, del siguiente modo:

{% set my_attribute = create_attribute() %}
<div{{ my_attribute.addClass(['mis', 'clases']).setAttribute('id', 'mi-ID') }}>
  {{ content }}
</div>

De entrada no parece gran cosa, pero adquiere un poder excepcional si estamos sacando todo el jugo posible a Twig (deberíamos) y utilizamos embed o include , quizá junto con algún modulo como Components!.

Podríamos entonces tener un componente base como éste:

{% set my_attributes = create_attribute() %}
{% set classes = [
  'teaser'
] | merge(add_classes) %}


<article{{my_attributes.addClass(classes).removeClass(remove_classes)}}>
  <h4 class="teaser__title">{{ title }}</h4>
  <div class="teaser__metadata">
    <div class="teaser__category">{{ category }}</div>
    <div class="teaser__tag">{{ tag }}</div>
  </div>
</article>

No es el objeto de este artículo entrar en el diseño basado en componentes, pero baste decir que el objetivo es ser lo más independiente de Drupal posible: por eso los nombres son genéricos como {{ title }} en lugar de campos concretos como {{ content.label }}

Este archivo puede ser llamado desde diferentes templates desde los cuales se pasará la información deseada para cada contexto, además de cualquier configuración adicional expuesta:

<!-- En el template de un nodo: -->

{%
  set classes = [
    'node',
    'node--type-' ~ node.bundle|clean_class,
  ]
%}

<div{{attributes}} /> <!-- Estos son los attributes provenientes del render array desde el back-end -->
  {% include "@my-components/node/teaser.twig" with {
    add_classes : classes,
    category: content.field_section,
    title: content.field_display_title,
    tag: content.field_tag_destacada
  }%}
</div>
<!-- En el template de una entidad custom con un esquema de datos totalmente diferente: -->

{%
  set classes = [
    'my-entity',
    'my-entity--type-' ~ node.bundle|clean_class,
  ]
%}

<aside{{attributes}} /> <!-- Estos son los attributes provenientes del render array desde el back-end -->
  {% include "@my-components/node/teaser.twig" with {
    add_classes : classes,
    remove_classes: ['teaser'],
    category: content.category,
    title: label,
    tag: content.field_info
  }%}
</section>

Como se puede observar, de este modo se produce una separación de responsabilidades bien clara:

  • El componente gestiona el marcado final independientemente de dónde venga y añade una clase propia (teaser) pero deja al usuario espacio para añadir sus propias clases (add_classes) e incluso para eliminar otras (remove_classes).
  • Los templates de Drupal mapean la información del componente con campos y valores concretos y además se benefician de las configuraciones expuestas para añadir sus propias clases e incluso eliminar otras que sean innecesarias.

Esta división de responsabilidades tan flexible hubiera sido infinitamente más costosa sin la ayuda de create_attributes() ya que las operaciones de mapeo de clases, merge y eliminación hubieran requerido en cada caso tediosas iteraciones y hubieran abierto la puerta a errores.

En su lugar, el uso de attributes generados al vuelo exclusivos del template nos permite dinamizar los atributos, exponer configuraciones para su sobrescritura y dividir fácilmente las responsabilidades entre los diferentes niveles del front-end sin tener que tocar una línea de PHP.