Pasar al contenido principal
ID.R

idiazroncero.com

Diseño basado en componentes usando Single Directory Components (SDC) en Drupal

Desde hace poco, Drupal contiene el módulo experimental SDC (Single Directory Components). Este módulo quiere acercar el desarrollo front-end en Drupal al diseño basado en componentes, un paradigma que en los últimos años ha tomado gran fuerza, pero que no era fácil de implementar en versiones anteriores de Drupal.
Diseño basado en componentes usando Single Directory Components (SDC)

En esta guía explicaremos qué es y como funciona SDC, en qué te puede beneficiar, cómo se integra con otras herramientas internas y externas, cuál es su potencial y en qué modo podría incluso revolucionar el modo en el que trabajamos el front-end de Drupal.


Las versiones más recientes del core de Drupal 10 contienen el módulo experimental SDC (Single Directory Components). Este módulo quiere acercar el desarrollo front-end en Drupal al diseño basado en componentes, un paradigma que en los últimos años ha tomado gran fuerza (React, Vue, Web Components, Storybook, etc...) pero que no era fácil de implementar en versiones anteriores del core de Drupal.               

En esta guía se basa en mi charla de la pasada DrupalCamp de Sevilla 2023. Si prefieres el formato audiovidual, puedes ver la presentación aquí.

1) El camino hacia SDC

Existen dos grandes presiones que han derivado en la creación de Single Directory Components (de ahora en adelante, SDC).

Por un lado, una presión externa: en los últimos años, el mundo que rodea a Drupal se ha movido claramente a favor del diseño basado en componentes. Todos conocemos frameworks como React, Vue, Svelte; pero también herramientas como Storybook, metodologías como Atomic Design o incluso venerables herramientas como Bootstrap han inaugurado o se han movido en esta dirección.

Por otro lado, existe una presión también interna. Como ya comenté en la charla que di el año 2022 en Zaragoza, desde hace un tiempo en Drupal existen varios módulos y temas contribuídos que apuntan de una manera u otra hacia hacia el diseño basado en componentes. Algunos de ellos son Single File Components, UI Patterns, Components!, Emulsify o Radix.

SDC viene a ser la solidificación y concreción de una respuesta a estas necesidades en una propuesta oficial del Core de Drupal.

Pero antes de conocer la respuesta a un problema hemos de preguntarnos: ¿cuál es el problema?

1.1) El problema del desarrollo front-end en Drupal

Aparte de la curva de aprendizaje elevada, un problema específico del front-end de Drupal es que los sistemas que interfieren en el renderizado de una página están muy distribuidos entre diferentes capas y lugares del código.

Al final del día, lo que un desarrollador front-end necesita saber es quién está interfiriendo en el HTML, CSS y JS que acaba en el navegador

En Drupal, en muchas ocasiones, hay demasiadas capas interviniendo.

Esto dificulta el aprendizaje de los nuevos desarrolladores de Drupal y puede ser incluso complicado para perfiles más avanzados. Se producen con demasiada facilidad pérdidas de contexto, sobrescrituras indeseadas, acoplamientos.

Esta frustración resultará familiar a muchos desarrolladores de Drupal que se hayan enfrentado a un bug difícil de cazar: ¿Estará el problema en las plantillas? ¿Qué plantilla tiene precedencia? ¿Qué suggestions se están aplicando? ¿A nivel de módulo o de tema? ¿Hay una sobrescritura de librería en el info.yml? ¿Quien declara la librería? ¿Y si hay un hook que la altera? ¿Cuántos hooks están interviniendo, y en qué orden?

El diseño front-end en Drupal se beneficiaría de un modelo más simple
El desarrollo front-end en Drupal se beneficiaría de un modelo más simple

Todas las soluciones que preceden a SDC tienen un punto en común y es que intentan ordenar el desarrollo front-end en Drupal a partir de la abstracción de componentes.

El objetivo es que cada elemento de la interfaz de usuario potencialmente reutilizable e individualizable sea capaz de gestionarse a sí misma, esto es, gestionar su propio HTML, CSS y JS y ser invocada desde otros puntos del código.

De este modo se consigue ya un primer paso y nos acercamos al principio de responsabilidad única: todo lo que concierne a un componente dado debe de ser manejado en un mismo lugar. De este modo, cualquier cambio o actualización que se haga a nivel de componente debería de ser inmediatamente compartido por todas las instancias que hayan sido creadas a lo largo del proyecto:

Un cambio en el CSS del componente será aplicado inmediatamente a todas las instancias del mismo, independientemente de su contexto
Cualquier cambio en un componente se propaga inmediatamente por todas sus instancias

Llegados a este punto, cualquier desarrolladora dirá: ¡pero esto ya se puede hacer en Drupal! Basta con usar cualquiera de los módulos citados anteriormente (Components!, por ejemplo). Incluso usando capacidades que ya están en Twig y en el Core de Drupal se podría conseguir.

Por tanto: ¿qué aporta SDC?

Vamos a intentar resolver la pregunta en tres grandes apartados: 

  1. Cómo funciona SDC
  2. Qué problemas resuelve ya mismo 
  3. Qué problemas ayudará a resolver en el futuro.

2) Cómo funciona SDC

Sólo necesitas usar Drupal Core en la versión 10.1.x, activar el módulo SDC (experimental) y crear una carpeta llamada components dentro del módulo o tema donde quieras dar de alta un componente para comenzar a trabajar con SDC.

No es obligatorio pero es altamente recomendado instalar el paquete drupal/core-dev para beneficiarse de la validación de componentes. Desde el punto de vista de la DX (developer experience, experiencia del desarrollador), es casi imprescindible.

Dentro de la carpeta components añadiremos un subdirectorio por cada componente. Por tanto, lo primero que podemos notar es que SDC opta por la estrategia de organizar los componentes en directorios, frente a otras estrategias como los Single File Componentes, también presentes en Drupal y popularizados por Vue.

Directorio de SDC

 

2.1) SDC puede ser extremadamente simple...

De entrada, SDC es conceptualmente muy simple: todos los activos (assets) deben de llamarse igual y estar agrupados bajo un mismo directorio — el cual, por cierto, no es obligatorio que siga la misma convención.

Te estarás preguntando por ese misterioso myComponent.component.yml. Este archivo (que más adelante) es de importancia capital ya que el mecanismo de auto-descubrimiento de componentes depende de él y además incluye muchas de las novedades de SDC.

De hecho, un componente podría perfectamente estar formado por un archivo *.twig y un *.component.yml , que son los únicos archivos requeridos.

Un componente minimalista, basado en un twig y un component.yml

Esta simple implementación ya nos permitirá acceder al componente con id mytheme:mycomponent

Los componentes se identifican no sólo por su nombre, sino también por el módulo o tema que los declara, es decir, namespace:component. Más adelante veremos usos de esto, como la sobrescritura de componentes.

2.2) ...o tan complejo como lo queramos hacer

Del mismo modo, SDC puede ser tan complejo como queramos. La idea es que todo lo relacionado con el componente viva dentro de un mismo directorio: documentación, archivos de Storybook, imágenes, archivos .scss o .ts, dependencias en un package.json...

Un componente de SDC complejo.
Un componente con cierta complejidad.

Recuerda que los componentes se pueden declarar tanto a nivel de módulo como de tema.

Autogeneración de librerías

Sólo con usar SDC de esta manera ya hemos obtenido una ventaja: el CSS y JS del componente serán añadidos al tema automáticamente, sin necesidad de dar de alta una library de Drupal.

Es decir, nos podemos desprender del attach_library('mytheme/mycomponent') y del $variables['#attached']['library][]. ¡Fantástico!

Esto tiene implicaciones positivas a nivel de rendimiento y mantenibilidad que veremos más adelante.

2.3) El archivo *.component.yml

Hasta aquí la única gran novedad es ese misterioso archivo mycomponent.component.yml del cual todavía no sabemos nada.

La realidad es que una gran parte del poder de SDC reside en este archivo.

Pero, al mismo tiempo, y para facilitar la implementación de SDC, este archivo puede incorporarse de modo gradual a nuestros componentes. Vamos a ver las particularidades del *.component.yml partiendo de su implementación mínima.

Un archivo vacío
# ¡No tengo nada que decir!

Para empezar, nuestro mycomponent.component.yml puede estar vacío. De hecho, es la implementación mínima necesaria para garantizar que nuestro componente será descubierto y dado de alta en Drupal.

Esto es una gran ayuda a la hora de migrar a SDC: puedes empezar por una definición vacía e ir aumentándola poco a poco.

Identificación del componente
name: Navbar
status: experimental
group: Navigation

La información inicial acerca del componente son sus metadatos más básicos: nombre, estado y grupo. Más adelante veremos para qué se pueden usar estos datos.

Por cierto: la declaración del componente utiliza el estándar json-schema.org. Es por ello que verás ciertas particularidades respecto a otros schema de Drupal, y es por ello también que es recomendable añadir una referencia al schema para ayudar a nuestros IDEs a proporcionar ayuda, autocompletado o indicación de errores.

Añadir el schema de SDC es así de fácil:

$schema: https://git.drupalcode.org/project/sdc/-/raw/1.x/src/metadata.schema.json
name: Navbar
status: experimental
group: Navigation
Tomar el control de la librería

Decíamos antes que una de las ventajas de SDC es que declara y carga por nosotros la library del componente. Es decir, nuestro CSS y JS son añadidos automáticamente, sin que tengamos que hacer nada.

No obstante, esto es sólo válido para los archivos mycomponent.css y mycomponent.js. ¿Qué ocurre si queremos...

  • ...declarar dependencias?
  • ...añadir más archivos CSS / JS?
  • ...usar otros nombres?

En ese caso, podemos tomar el control de la librería usando el archivo mycomponent.component.yml

name: Navbar
status: experimental
group: Navigation
libraryOverrides:
  css:
    theme:
      assets/css/theme.css : {} # Añadimos un archivo CSS adicional
  dependencies: # Añadimos dependencias con librerías del Core
    - core/drupal
    - core/once

Los valores de mycomponent.component.yml serán unidos (merge) con la librería autogenerada, por lo que no es necesario volver a declarar aquí los archivos mycomponent.(css|js).

Definir el schema de nuestro componente: props y slots

Hora de entrar en harina: lo más importante de nuestro componente es el contrato que firmamos con él.

Dicho de otro modo: SDC permite acotar y definir con precisión qué valores (inputs) acepta y cuáles son sus condiciones:

name: Navbar
props:
  type: object
  required: [type]
  properties:
    type:
      type: string
      description: Badge type
      enum: [primary, secondary, info, danger]
slots:
  content:
    title: Alert content
    description: The alert content. 

En este ejemplo nuestro componente Navbar  acepta un prop llamado type que es una cadena de texto (string) y que acepta sólo tres valores: primary, secondary, info o danger. Este type es obligatorio (required)

Además, nuestro componte declara un slot llamado content.

¿Vamos muy rápido? No te preocupes: antes de continuar vamos a clarificar a qué nos referimos con los conceptos de prop y slot:

Pero... ¿qué narices son los props y slots ?

Esta diferencia conceptual no proviene de Drupal, sino que ha sido tomada prestada de Vue.js y aparece, de un modo u otro, en muchas librerías que usan diseño basado en componentes. Si quieres una descripción más completa, te recomiendo acudir a la documentación oficial

Aquí haremos una diferenciación breve y funcional.

Props

Los props son los valores que puede tomar un componente, cuya naturaleza y estructura es conocida de antemano.

Es decir, los props pertenecen al mundo de lo conocido, lo controlado, lo definido. A través de ellos definimos y permitimos modificar la estructura y comportamiento del componente.

De los props conocemos, por ejemplo, su tipado (¿es una cadena de texto?  ¿un array? ¿un número?), así como si es requerido o no y si tiene un número acotado de valores posibles.

Slots

Por su naturaleza, los props no pueden renderizar ni HTML arbitrario ni otros componentes anidados.

Esta capacidad queda reservada para los slots, que declaran zonas donde tenemos permiso para tomar el control de su renderizado. Es decir: inserte aquí lo que quiera. Ese «lo que quiera» suele ser el HTML arbitrario o los componentes anidados que no podíamos pasar como props.

Un ejemplo de props vs slots

Veamos con un ejemplo ilustrativo.

Dado el siguiente componente (una especie de card), los elementos marcados en violeta serían props y el elemento naranja sería un slot

Ejemplo de componente con cinco props y un slot

Los props se distinguen porque son los que definen la estructura y variantes del componente. En este caso, podemos fácilmente imaginar que:

  • title es un string que será renderizado dentro de un heading o similar
  • dismissable es un booleano que usaremos para controlar si se renderiza el botón de cierre
  • border_width es un número entero que podría tomar un valor restringido a entre 1 y 3
  • cta_url y cta_text son dos strings que usamos para crear un button (¡y que a su vez podría ser otro componente de SDC!)

Sin embargo, en los componentes de tipo card es muy normal que no sepamos (ni debamos saber) qué es lo que se va a renderizar dentro de la card. Es decir, la zona definida por content podría ser cualquier cosa: texto plano, texto con HTML, imágenes, otros componentes...

Es por eso que content se declara como un slot, una zona de libre uso.

Definición de props y slots en el archivo *.component.yml

Manos a la obra: vamos a definir nuestro componente. Empecemos por los props, que podrían ser algo así (consideremos el CTA como obligatorio para simplificar):

(...)
props:
  type: object
  required: [title, cta_url, cta_text]
  properties:
    title:
      type: string
      description: Badge type
    dismissable:
      type: boolean
      default: false
    border_width:
      type: number
      default: 1
      enum: [1, 2, 3]
    cta_url:
      type: string
    cta_text:
      type: string
    attributes:
      type: Drupal\Core\Template\Attribute

Observamos lo siguiente:

  • Hemos indicado que los props obligatorios son title, cta_url y cta_text
  • Es posible indicar valores por defecto (default).
  • Es posible restringir los valores usando enum
  • Es posible indicar como type objetos de Drupal como el archiconocido Attribute

 Por su parte, definir slots es mucho más sencillo ya que basta con indicar su existencia:

slots:
  content:
    title: Card content
    description: The card content.

¡Socorro! ¡Demasiada información!

¡Lo siento! Esta parte es la más densa, pero dentro de poco comenzarás a ver los beneficios. No obstante, no te agobies: tienes a tu disposición una documentación completa acerca del archivo *.component.yml, incluyendo un ejemplo anotado y un drush generate de componentes.

Sobrescritura de componentes

Por último en el archivo *.component.yml es donde se produce también la sobrescritura de unos componentes por parte de otros:

name: Navbar
status: experimental
group: Navigation
props: {}
slots: {}
replaces: “mymodule:mycomponent”

En este caso, estamos indicando que queremos reemplazar con nuestro componente el componente con id mymodule:mycomponent (recuerda que un componente se identifica por su nombre y por el módulo o tema que lo declara).

Es importante tener en cuenta que sólo los temas pueden sobrescribir componentes, y que sólo se puede sobrescribir componentes con el schema definido en mycomponent.component.yml.

Fork!
La sobrescritura de componentes en SDC se hace mediante fork, esto es: 

  1. Clona el componente que deseas sobrescribir en tu tema (recuerda: sólo los temas pueden reemplazar componentes)
  2. Realiza los cambios deseados
  3. Indica en el *.component.yml a qué componente estás sustituyendo usando replaces

2.4) Cómo implementar un componente

Todo esto está muy bien, pero es el momento de bajarlo a tierra: dado un componente con slots y props, ¿cómo lo implementaríamos en Drupal?

La respuesta es de nuevo sencilla: usando herramientas que ya conoces de sobra: los {% include %} y {% embed %} de twig.

Vamos a partir de un ejemplo sencillo: el componente badge de Bootstrap (ver documentación):

Esto es una badge!

Una versión simple de este componente podría implementarse en twig de esta manera:

{% set classes = [
    'badge',
    'text-bg-' ~ color,
]%}
<div class="{{ classes | join( )}}">
  {{ content }}
</div>

Como puedes ver, este componente no contiene ningún drupalismo. Nada de node.getLabel() o de field_dismissable.value. No está acoplado a ninguna implementación particular y por tanto es fácilmente reutilizable: de hecho, podríamos utilizarlo fuera de Drupal.

Desacoplar los componentes de Drupal lo máximo posible es uno de los objetivos de SDC, y ayudará a la reutilización e incluso a la generación de librerías de componentes compartidas.

Del código anterior es fácil inferir cuáles son sus props:

props:
  type: object
  required: [color, content]
  properties:
    content:
      type: string
    color:
      type: string
      enum: ['primary, 'secondary', 'light', 'dark']
    classes:
      type: array

La única particularidad es que hemos añadido un enum para limitar las opciones de color.

La implementación de este componente en nuestro código es tan fácil como utilizar un {% include %} referenciando directamente al componente por su ID mytheme:badge:

{% include "mytheme:badge" with {
  color: 'primary',
  content: 'Esto es una badge!',
  classes: [
    'additional-class'
  ]
} %}
Hacer el componente más flexible

Este componente tiene un problema: estamos asumiendo que content siempre va a ser una cadena de texto, pero ¿qué ocurre si el usuario quiere añadir, por ejemplo, un pequeño icono?

<i class="fa fa-help"></i> Esto es una badge!

O, incluso, añadir un icono que es un componente a su vez:

{% include "mytheme:icon" with { icon: 'help' } %} Esto es una badge!

Con props no podemos, pero sí que podemos declarar un slot:

props:
  type: object
  required: [color]
  properties:
    color:
      type: string
      enum: ['primary, 'secondary', 'light', 'dark']
    classes:
      type: array
      
# Declaramos el content como slot:
slots:
  content:
    title: badge content

A nivel de twig, los slots se implementan como un {% block %}:

{% set classes = [
    'badge',
    'bg-' ~ color,
]%}
<div{{ attributes.addClass(classes) }}>
  {% block content %}
    Put your content here...
  {% endblock %}
</div>

Y podemos incorporarlos a nuestro código utilizando {% embed %} y pasando los props mediante with y los slots como bloques.

{% embed "mytheme:badge" with {
  color: 'primary'
} only %}
​
  {% block content %}
    <i class="fa fa-help"></i>
    <span>My content</span>
  {% endblock %}
​
{% endembed %}

Puesto que estamos usando twig, nos podemos beneficiar de las capacidades que ya conocemos. Por ejemplo, dentro de un block nada nos impide anidar otro componente, que a su vez podría anidar otros tantos:

{% embed "mytheme:badge" with {
  color: 'primary'
} only %}
​
  {% block content %}
    {# Embedded component #}
    {% include "mytheme:icon" with {
      icon: 'help'
    } only %}
    <span>Esto es una badge!</span>
  {% endblock %}
​
{% endembed %}
Implementación como array de renderizado

Por último, hay que decir que los componentes de SDC también pueden ser implementados como arrays de renderizado, utilizando el nuevo tipo component:

$build['badge'] = [
  '#type' => 'component',
  '#component' => 'mytheme:bagde',
  '#props' => [
    'color' => 'primary',
  ],
  '#slots' => [
    'content' => [
      '#type' => 'html_tag',
      '#tag' => 'span',
      '#value' => t('Esto es una badge!'),
    ]
  ]
]

Este uso quizá no sea de tanto interés para su uso en temas, pero desde luego puede ser muy interesante a la hora de desarrollar módulos contribuidos, field formatters, etc.

2.5) ¿Y el hook de preprocesado del componente?

Llegado a este punto, y a la vista de lo familiar que resulta todo, es posible que estés esperando la aparición de un hook_preprocess_mycomponent que permita alterar y masajear los props y slots que recibe el component antes de llegar al archivo twig.

La respuesta es que no hay hooks de preprocesado.

¿Por qué? Puedes leer (y participar) en esta issue, pero podemos citar los siguientes motivos:

  • Las alteraciones, masajeados de datos, procesados... normalmente pertenecerán a un momento anterior, puesto que suelen ser específicos de Drupal. Es decir, si tienes que manejar el objeto node y sus métodos como node.get(field_name), normalmente te verás obligado a hacerlo en un paso anterior al componente, manteniendo al componente desacoplado de las particularidades de Drupal y de tu proyecto:
  • Cualquier lógica necesaria que quede por implementar a nivel de componente se podrá llevar normalmente a twig.
  • SDC nace con la idea de mantener todo lo necesario para renderizar un componente dentro de un directorio. Crear un hook abriría la puerta a que este sea invocado desde una miríada de sitios, reintroduciendo el problema del que intentamos huir.

2.6) Ok, pero... ¿qué hay en realidad de nuevo?

Llegados a este punto alguien podría declararse en estado de escepticismo y decir: no has respondido a mi pregunta, ¿qué hay realmente de nuevo en SDC? 

  • Organizar en directorios es algo que ya se pude hacer con components! o incluso manualmente.
  • Usar {% include %} y {% embed %} para abstraer y reutilizar componentes no es algo de SDC, sino de twig. Sólo se ha añadido un cómodo id namespace:component
  • La carga automática de libraries está muy bien pero:
    • Esto es algo que ya hacían temas como Radix
    • La complejidad que sale por la puerta, entra por la ventana: puede que ya no tengamos que mantener el *.libraries.yml, pero a cambio tenemos que encargarnos del *.component.yml.

Vamos a intentar responder analizando los problemas que SDC soluciona hoy y, sobre todo, los que podrá solucionar en el futuro:


3) Los problemas que soluciona SDC hoy

3.1) Un modelo mental más simple

El primer beneficio obvio de SDC es que organizar los componentes en directorios reduce la carga cognitiva y es un avance respecto a un modelo en el que hay una gran cantidad de capas y subsistemas interviniendo a diferentes niveles y obstaculizando la reutilización, la generación de estándares compartidos y, en general, el trabajo del desarrollador front-end.

3.2) Mejor rendimiento por defecto (sin depender de libraries.yml)

En mi experiencia, existen dos grandes modelos a la hora de manejar los activos CSS y JS en Drupal: 

  1. Generar un gigantesco bundle que incluya todo el CSS y JS del sitio. Es mucho más rápido, pero es una mala práctica que tiene como consecuencia cargar muchos archivos innecesarios y suele provocar quejas de las herramientas de análisis, como el temido Lighthouse/Pagespeed Insights de Google.
  2. Componentizar todo y declarar una library por componente, siguiendo de hecho las mejores prácticas de Drupal. Es ideal, puesto que las librerías sólo se adjuntan cuando son invocadas, pero introduce problemas de mantenibilidad.

La realidad es que Drupal propone claramente el modelo 2, pero en muchos casos se sigue viendo el modelo 1. El motivo lo podemos encontrar si acudimos al mismo core de Drupal: el archivo libraries.yml del tema Olivero tiene un tamaño considerable puesto que cada componente CSS, aunque incluya un simple archivo css, ha de ser tratado como una librería independiente.

Capibara triste porque tiene que mantener un libraries.yml un poco grande
Una capibara triste porque tiene que mantener un libraries.yml excesivamente grande

Demasiado trabajo para una tarea tan banal va contra el principio de máxima vagancia (si no existe tal principio, debería existir). 

SDC lo soluciona al cargar las librerías automáticamente, proporcionando una experiencia de desarrollador agradable desde el minuto uno: simplemente lanza tus archivos a un directorio, usa el componente y recibirás sólo y exclusivamente el css y js necesarios.

3.3) Validación, tipado y errores útiles

Otro pequeño gran paso adelante para el front-end de Drupal es el tipado, que nos permite por fin tener información útil acerca de nuestros errores o incluso prevenirlos antes de que ocurran. Esto es, SDC es también una herramienta contra las regresiones y el temido WSOD.

Pongamos como ejemplo el siguiente código twig:

{% if anArray | length > 5 %}
  {% set first = anArray | first %}
  {% for item in array %}
    {{ item }}
  {% endfor %}
{% endif %}

Como podemos ver, opera usando filtros de array y for loops sobre la variable anArray. Pero... ¿qué ocurre si pasamos un valor que no es un array?

{% include “@theme/mycomponent.twig” 
  with {
    anArray: 3
  }
%}

La respuesta es... nada. Y esto es el problema: en lugar de un error, dado que twig es un lenguaje permisivo 3 | length evalúa a 1. Este renderizado vacío es un error silencioso que puede ser desplegado sin que nadie se dé cuenta hasta que sea demasiado tarde.

Sin embargo, si usamos SDC y declaramos el tipo del prop en el mycomponent.component.yml

properties:
  anArray:
    type: array
    title: This is an Array

...cualquier tipo erróneo nos lanzará no sólo un error o excepción, sino que además será un error útil, esto es, con información concreta y precisa acerca del origen del problema y su solución. En este caso: Drupal\sdc\Exception\InvalidComponentException: [anArray] Integer value found, but an array or an object is required

3.4) Props obligatorios y enumerables

La fiesta de la validación no acaba aquí: puesto que los props pueden ser indicados como obligatorios y pueden tener enums, obtenemos una protección extra a la hora de evitar que un componente reciba un número insuficiente de valores — o valores que no podemos manejar.

Un ejemplo potencialmente peligroso es el siguiente: un componente de tipo button que permita definir la etiqueta HTML del componente en su prop tag: <button> o <a>:

<{{ tag }}{{ attributes.addClass(button_classes) }}>
  {{ content }}
</{{ tag }}>

Este componente tiene un grave problema: sin SDC, olvidarse de la tag daría lugar a un HTML malformado que, en ausencia de etiqueta, se renderizará como texto plano:

Un botón sin etiqueta HTML renderizado erróneamente como texto plano
Un botón sin etiqueta HTML renderizado erróneamente como texto plano

Si hubiéramos indicado que tag es un valor required usando SDC, en su lugar hubieramos obtenido un error útil: [tag] The property tag is required.

Incluso con SDC, es posible pasarle un valor no aceptable a tag. Por ejemplo: parachute. Este error es peor aún ya que twig felizmente renderizará la cadena de texto como una etiqueta HTML y los navegadores no informarán de ningún problema. Hasta que no se analize el código fuente, el error puede permanecer escondido durante años:

Una etiqueta HTML errónea, felizmente renderizada por twig y el navegador sin queja ninguna.
Una etiqueta HTML errónea, felizmente renderizada por twig y el navegador sin queja ninguna.

Sin embargo, bastaría con indicar un enum para obtener, de nuevo, una advertencia útil: [tag] Does not have a value in the enumeration ["button","a"].

Y todo ello con dos líneas de código en el mycomponent.component.yml:

props:
  type: object
  required:
    - tag
  properties:
    tag:
      type: string
      title: Tag
      default: 'button'
      enum: ['button', 'a']

4) Los problemas que solucionará: el futuro de SDC

Quizá lo más ilusionante de SDC es lo que está por llegar. 

En mi opinión, la comunidad front-end de Drupal ha acogido SDC con particular entusiasmo, y la prueba de ello es que ya hay múltiples integraciones en marcha, y eso que el módulo aún está en el camino a su versión Beta.

Vamos a repasar algunas de las integraciones ya existentes / en desarrollo y los caminos hacia los que apuntan, pero antes de ello hay que hablar de un aspecto fundamental de SDC que hemos dejado para el final.

4.1) La revolución silenciosa: un componente es un plugin

Así como suena: cada componente es, a nivel interno de Drupal, un plugin.

Uno de los aspectos más revolucionarios de SDC parte de algo tan sencillo como esto.

Esto significa que tenemos un servicio pluginManager que podemos usar. Significa que podemos fácilmente listar todos los componentes existentes, analizar sus metadatos, modificarlos, acceder a su schema. Significa que el propio SDC puede ser alterado, extendido, modificado por otros módulos.

Los componentes son plugins, y pueden ser listados usando un pluginManager
Los componentes son plugins, y pueden ser listados usando su pluginManager

Significa, sobre todo, que SDC tiende un puente entre el front-end y el back-end de Drupal que era muy necesario.

Hasta ahora, la mayoría de decisiones de arquitectura en el front-end de Drupal eran invisibles para el back-end. El hecho de que el view_mode de tipo teaser fuera renderizado por el componente card.twig y estilado por el archivo card.css; y que ese componente aceptara ciertos valores y fuera reutilizado en un punto y sobrecrito en otro era algo que sólo estaba en la cabeza del desarrollador

Con SDC, esto se acaba. Y tiene consecuencias inmediatas.

4.2) Mapeado directo de entidades a componentes

Un problema clásico de todos los intentos de componentizar el front-end de Drupal es que, al final, siempre hay que contar con una plantilla (template) como intermediario.

Es decir, por mucho que abstraigas tu componente en un mycomponent.twig, siempre ha de ser invocado desde un template, que al fin y al cabo es lo que conoce Drupal. 

De partida, SDC no soluciona esto, ya que el sistema de theme hooks y suggestions permanece inalterado. El resultado es una cierta repetición:

En SDC, como en otras soluciones, siempre es necesario pasar por una plantilla de Drupal
Capibara triste porque en SDC, como en otras soluciones, casi siempre es necesario pasar por una plantilla de Drupal.

Sin embargo, usando el módulo Single Directory Componentes: Display sí que es posible mapear directamente desde Drupal (modos de vista, campos) a componentes.

Es decir, es posible saltarse la plantilla como intermediario y elegir directamente qué componente debe encargarse de renderizar un modo de vista o campo directamente desde la interfaz de usuario:

Un view mode de Drupal siendo renderizado usando SDC
Un modo de vista de Drupal (teaser, por ejemplo) puede ser renderizado usando un componente de SDC

Es posible que a algunos esto os suene familiar: es algo que ya popularizó el módulo UI Patterns en su día. Buenas noticias para vosotros: la rama 2.0.x de UI Patterns utilizará SDC como base, en lugar de su propio sistema. Una feliz convergencia.

El poder de los plugins
Decíamos antes que el hecho de que los componentes sean plugins es una revolución, y aquí podemos ver un ejemplo: el módulo SDC Display puede mostrar un listado de todos los componentes, acceder a sus metadatos (nombre, descripción) y conocer sus props y slots.

¿Más fácil todavía? Bloques autogenerados a partir de componentes SDC

¿Podría ser mejor todavía la experiencia del desarrollador? ¡Claro que sí! Podríamos tener bloques de Drupal que se generaran automáticamente a partir de componentes.

Dado que el archivo *.component.yml ya contiene toda la información acerca de los props y slots que toma un componente, es posible generar bloques al vuelo.

Un bloque generado a partir de un componente
Un bloque generado a partir de un componente

Es lo que hace el módulo CL Blocks, un módulo preparado para el predecesor de SDC (Component Libraries), que todavía no es compatible con SDC pero está en camino de serlo.

Estos bloques puede ser utilizado, por ejemplo, en Layout Builder. Su formulario se genera a partir de la información presente en el propio *.component.yml (por ejemplo, un prop de tipo number dará lugar a un campo de formulario de tipo <input type="number">.

Revolucionando el flujo de trabajo de Drupal

En cualquier caso, sea a través de mapeado manual de componentes, mediante bloques autogenerados o cualquier otra soluciń futura, SDC acerca Drupal a un flujo de trabajo mucho más sencillo por el cual el desarrollador dispondrá automáticamente de herramientas para usar sus componentes desde la interfaz de administración de Drupal.

SDC permitirá un flujo de desarrollo más sencillo
Capibara feliz porque SDC permitirá usar componentes directamente desde la UI de Drupal

4.3) Integración con Storybook: tests y autodocumentación

Otra integración ya existente para Drupal y SDC es Storybook, a través de CL Server y el plugin de Storybook asociado.

CL Server activa un endpoint en Drupal que puede ser accedido por una instancia de Storybook configurada para ello.

Storybook mostrando un componente de SDC
Storybook mostrando un componente de SDC. El componente es renderizado por Drupal, por lo que los tests son fiables.

La configuración reviste algo de complejidad, pero el resultado es una guía de estilos / documentación / herramienta de testeo en vivo que es capaz de mostrar todos los componentes dados de alta en Drupal.

¿Por qué CL Server?
Un problema clásico de las integraciones con Storybook es que usan twig.js para renderizar twig. 

Twig.js no se encuentra en paridad de capacidades con twig php, por lo que no es una herramienta fiable: pueden surgir discrepancias, los tests pueden arrojar falsos positivos / negativos o pueden haber diferencias visuales.

CL Server renderiza los componentes usando el propio Drupal, por lo que se garantiza que el renderizado en Storybook y el de Drupal son idénticos básicamente porque... son idénticos.

Storybook es una herramienta muy potente que va mucho más allá de una guía de estilo en vivo. Por ejemplo, contra los componentes Storybook es capaz de ejecutar tests. En este pantallazo podemos ver un test de accesibilidad:

Un test de accesibilidad de un componente de Drupal
Un test de accesibilidad de un componente de Drupal

Un concepto central de Storybook son las stories, siendo cada story un estado de un componente dado. En la imagen superior, por ejemplo, podemos ver la story del componente alert en modo dismissibile.

Es fácil identificar, a nivel de testado, las stories con scenarios.

Autodocumentación a partir de información extraída de un componente de SDC
Autodocumentación a partir de información presente en el directorio de un componente de SDC

Otra ventaja del uso de Storybook es que el componente se puede auto-documentar a partir de información presente en su propio directorio. Storybook es capaz de crear una página con toda la información presentada de una manera concisa y ordenada, incluyendo los props y slots del componente, el README o incluso el propio código fuente.

¡Atención!
Lo estamos pintando muy bonito, pero la realidad es que la integración entre Drupal y Storybook todavía está incompleta y no permite que Storybook sea capaz de leer e interpretar el mycomponent.component.yml, por lo que otro archivo (el mycomponent.stories.yml) tiene que ser añadido.

Existe un generador de Drupal (disclaimer: lo escribí yo) que convierte los archivos *.component.yml en *.stories.yml automáticamente. Ahí puedes ver también las diferencias entre los dos formatos.

Esta es una de las integraciones que más me han interesado, por lo que es posible que pronto escriba una extensión de este artículo explicando cómo realizar una integración lo más completa posible entre SDC y Storybook.

4.4) Y aún hay más...

Integración de WebComponents

El módulo Pokemon Card muestra lo sencillo que puede ser integrar un WebComponent en SDC, abriendo la puerta a la generación o integración de librerías de WebComponentes junto con otras herramientas como SDC Display.

Una de las cosas más interesantes es lo parecida que se vuelve la experiencia de desarrollo a React o Vue.

Integrar Pokemon Card es tan fácil como requerir la dependencia (composer require), escribir el siguiente código...

{% include "pokemon_card:pokemon_card with {
  name: "Gengar"
}%}

..y ¡voilà!. Un componente renderizado perfectamente que es capaz incluso de auto-gestionar su conexión con la API externa de donde extrae los datos.

¿Cómo de diferente es para el desarrollador esta experiencia de lo que estamos acostumbrados a ver en React y compañía? Las distancias se acortan.

<Pokemon name="Gengar" />
WebProfiler y CL Devel

El módulo WebProfiler ya ha añadido soporte para componentes, incluyendo una pequeña documentación de cada componente. Del mismo modo, CL Devel es capaz de generar tambiémn una autodocumentación de componentes basada en sus metadatos.

Webprofiler mostrando la información de un componente
Generadores

Para reducir la fricción a la hora de generar componentes y sus archivos (en especial el *.component.yml) existen ya al menos tres generadores que proporcionan comandos de drush:

...y lo que está por llegar.

Integración con UI Patterns, el módulo SDC Styleguide proporcionando una experiencia similar a Storybook pero en Drupal, temas del Core usando SDC... la comunidad de Drupal ha recibido con los brazos abiertos SDC y las iniciativas se multiplican.


5) En resumen

Single Directory Components es un pequeño gran añadido al desarrollo front-end en Drupal. Parte de su poder radica en su simplicidad y otra parte en el enorme alcance que algunos de sus cambios suponen, principalmente el hecho de que los componentes sean plugins.

Podemos decir que SDC hace todo lo siguiente:

  1. Acerca Drupal a las mejores prácticas front-end.
  2. Simplifica y elimina tareas repetitivas.
  3. Es fácil de implementar, puede ser implementado gradualmente y no es un «refactor»
de la capa de renderizado de Drupal sino un añadido.
  4. Tiende un puente entre el front-end y el back-end que era muy necesario.
  5. Añade capas de seguridad al desarrollo front-end
  6. Permitirá el desarrollo de mejores herramientas enfocadas a los desarrolladores
  7. Permitirá flujos de trabajo más sencillos para los site-builders
     
Back to top