JavaScript & Drupal 101 TUTORIAL HANDBOOK TOTAL MAX POWER 2000
(Amor al primer behavior)
You can read an english translation of this guide here: https://www.therussianlullaby.com/blog/guide-how-to-integrate-javascript-in-drupal-8-9/
ÍNDICE DE APARTADOS
-----------------------
1- Introducción
2- JavaScript y Drupal: conceptos básicos
3- Como incluir JavaScript en Drupal
3.1- Preparando el escenario: crear un módulo custom
3.2- El concepto de librería (library)
3.2.1- Secuencia para definir librerías
3.2.2- Librerías para cargar en <head>
3.2.3- Librerías como recursos externos
3.2.4- Librerías y dependencias
3.3- El archivo JavaScript
3.4- Añadiendo librerías JavaScript
3.4.1- Uso de propiedad #attached en Render Arrays
3.4.2- Librerías en una plantilla Twig
3.4.3- Librerías de manera global para un Theme
3.4.4- Inclusión de librerías mediante Hooks
4- Un poco más de JavaScript en Drupal
4.1- Estructura y pautas: IIFE
4.2- Paso de parámetros en IIFE
4.3- Pasando valores de PHP a JavaScript: drupalSettings
4.4- Modificando el HTML renderizado
4.4.1- Contador de visitas basado en Web Storage
5- Drupal y el viejo jQuery
5.1- Viaje rápido por las claves de uso de jQuery
5.2- Disponibilidad de jQuery en nuestra versión de Drupal
5.3- Usar una versión jQuery diferente de las disponibles
6- Drupal Behaviors
6.1- Anatomía de un behavior
6.2- El objeto global "Drupal"
6.3- Comportamientos en Drupal
7- JavaScript sin JavaScript: #ajax, #states
7.1- (Breve) Introducción a la gestión AJAX en Drupal
7.2- Elementos renderizados con la propiedad #states
8- Posibles problemas y probables soluciones
8.1- Ejecución ralentizada por mal uso de "context"
8.2- Cargas de JavaScript fuera de contexto
9- Enlaces y lecturas recomendadasÍNDICE DE EJERCICIOS
------------------------
Ej.1 - Preparar un módulo custom como escenario para pruebas
Ej.2 - Definir nuestra nueva librería
Ej.3 - Definir nuestro fichero JavaScript inicial
Ej.4 - Añadiendo librerías a nuestro código en Drupal
Ej.5 - Pasando valores al formato IIFE
Ej.6 - Transferir valores a través de drupal.Settings
Ej.7 - Contador de visitas custom con JavaScript
Ej.8 - Modificaciones basadas en jQuery
Ej.9 - Ventana de diálogo a partir del objeto global "Drupal"
Ej.10 - Board de imágenes de Unsplash vía API con Drupal.behaviorOtros artículos relacionados que podrían interesarte:
-----------------------------------------------------
Composer y Drush en el contexto de Drupal 8
Form API(I): Comprender, crear y modificar formularios en Drupal 8
Form API(II): Modificando formularios en Drupal8 mediante form_alter
Form API(III): Caso práctico de modificación de formulario Drupal 8
Drupal Fast Tips (I) - Using links in Drupal 8
Drupal Fast Tips (II) - Prefilling fields in forms
Drupal Fast Tips (III) - The Magic of '#attached'
Docker, Docker-Compose and DDEV - Cheatsheet
OpenEuropa - The European Commission in Drupal 8
Development environments for Drupal with DDEV
1- Introducción
Siempre termino observado que (en mi caso) el problema nunca es el folio en blanco y saber como comenzar, si no mas bien, como terminar. Donde poner el cierre, donde ubicar el punto final y de manera complementaria, hasta donde debo llegar en tal o en cual asunto. Comienzo siempre con una idea-fuerza central que se va abriendo y en un punto de la escritura, alcanza un escenario de ramas que resulta inmanejable.
Por eso seguramente este articulo tiene regiones donde ni siquiera yo estoy realmente satisfecho del alcance, pero prometo mejorarlo bien ampliandolo o completandolo con mas articulos.
¿Que es esto?
Este articulo que saldra proximamente en formato git-book listo para descargar en .pdf o imprimir, no es especificamente un tutorial sobre Drupal. Tampoco es un manual sobre JavaScript, aunque pudiera parecerlo. De hecho este tocho te deja justo donde tus habilidades en la programacion JavaScript deberian empezar. Donde llegues depende en gran medida de ti.
Muy bien, pero entonces ¿Que es esto exactamente? Puedes creer que de lo que se trata es de plasmar en una secuencia incremental (de menos a mas) las relaciones entre Drupal y JavaScript, realizando zoom sobre las interesecciones de ambas tecnologias y los protocolos aceptados por la comunidad Drupal como la manera de interconectar ambas. Este articulo, en realidad, solo va de eso. De jugar a ambos lados, de conectar y volver a jugar. De probar e insistir.
¿Para quien?
En el dia a dia he observado las dificultades de aprender Drupal “por las bravas”, es decir, siendo junior y haber sido lanzado a tareas directas con relativa complejidad. Si ademas de considerar el tooling actual de proyecto, le sumamos el ingles como idioma vehicular, a algunos compañeros y compañeras el asunto se les hace bola -y con toda justificacion, dado que no han sido formados previamente-. Yo creo que este articulo va para todos ellos y todas ellas. Para que puedan aprender de manera grata y practicar con algo que resulte suficientemente comodo. Para que sientan algo menos de desolacion y para disfrutar aprendiendo (lo que estoy seguro que puede conseguirse bajo ciertas condiciones).
Para todos los demas tambien puede servir: para los compañeros y las compañeras que a dia de hoy siguen dando soporte a Drupal 7 pero quieren ver como se han actualizado ciertas pautas. Para los seniors de Drupal 8 a quienes se les olvido donde estaba la caja de herramientas. Para aquellas personas que tienen que intervenir un legacy y no saben bien como empezar a poner orden. Va por todos nosotros, en ese momento en el que tenemos serias dudas. Para complementar, listados de documentacion y referencias convenientemente curadas, listas para consultar y tener a mano en nuestros marcadores. Tan importante es lo que te expongo como lo que te enlazo.
Siempre he animado a mis equipos a contribuir de la mejor manera que encuentren (hasta que terminemos de discernir bajo que categoria moral ubicaremos el software libre y su universo propio como mercancia) y entre todas las formas, siempre les he pedido que escriban, que publiquen, que compartan experiencias. Seria un gran error no dar el ejemplo suficiente (ya pueden denostarme por demasiadas cosas), asi que en ultima instancia, supongo que esto va como mensaje de animo a mis compañeros y compañeras del dia a dia.
Por lo demás, decir que Medium dice que el artículo tiene unas once mil palabras y tardaría casi una hora en leerse. Espero que todo esto pueda resultarle útil a alguien. Es el espíritu inicial.
Aquí tienes disponible todo el código de los ejemplos que se usan a lo largo de este artículo, agrupado como un módulo custom de Drupal, disponible para la descarga: https://gitlab.com/davidjguru/drupal-custom-modules-examples/-/tree/master/javascript_custom_module+
2- JavaScript y Drupal: conceptos básicos
Si es tu primer acercamiento a la intersección entre Drupal y JavaScript e incluso puede que sea tu primera aproximación a Drupal y su mundo, conviene que previamente repases en este apartado, en el que vamos a comentar algunos términos y nombres que usaremos a lo largo de todo el tutorial.
Así podrás saber de que estamos hablando en cualquier momento del manual y seguirás los casos, ejemplos y ejercicios con más facilidad.
- Drupal: Nuestra plataforma tecnológica de referencia en este contexto. Algo a medio camino entre el framework y el CMS, software libre descargable e instalable desde aquí: https://www.drupal.org/+ En este tutorial viajamos subidos a lomo de un Drupal, así que es bueno conocerlo minimanente.
- Render Array: Es una pieza clave de Drupal para “pintar” en pantalla. Son arrays multidimensionales que deben cumplir ciertas reglas usando diversas propiedades para modelar los elementos que serán renderizados. Los elementos que normalmente dibujamos están descritos aquí: https://api.drupal.org/api/drupal/elements/8.8.x+ La mayor parte de las relaciones entre Drupal y JavaScript las realizaremos desde render arrays de Drupal, por lo que está muy bien conocerlos y aprender su formato declarativo.
- JavaScript: Un lenguaje de programación que actualmente se ha diversificado tanto como para ser la base de muchos frameworks, librerías y herramientas de moda. A día de hoy ejecutable tanto en cliente como en servidor. En este contexto usaremos el llamado “Vanilla JavaScript”, es decir, el código artesano propio fuera de plataformas JS. https://developer.mozilla.org/es/docs/Web/JavaScript/Guide/Introducci%C3%B3n+ En este tutorial, aunque no sea un manual avanzado de JavaScript, usaremos este lenguaje en diversos apartados, por lo que es genial que lo conozcas un poco.
- Immediately-invoked Function Expressions(IIFE): También llamada función “autoejecutable”, es un formato específico para declarar funciones JavaScript de manera que se ejecuten tal y como son declaradas, en cuanto se definen. Las expresiones de función ejecutadas inmediatamente (IIFE por su sigla en inglés) son funciones que se ejecutan tan pronto como se definen. https://flaviocopes.com/javascript-iife/+ En este artículo probamos a integrar JavaScript en Drupal a través de este formato, así que sería óptimo que al menos, conocieses el concepto.
- AJAX: Son las siglas de JavaScript Asíncrono + XML, una combinación de tecnologías que facilita que se realicen peticiones parciales (más ligeras que peticiones completas) del cliente al servidor, lo que acarrea mejoras de velocidad y rendimiento. https://developer.mozilla.org/es/docs/Web/Guide/AJAX+ Aunque es un tema complejo y extenso, nos aproximaremos a las posibilidades de implementación de AJAX en Drupal.
- DOM: El Document Object Model es la estructura arborea (en forma de árbol) que representa todo el código HTML que se usa en la representación de la web que estamos visitando. https://developer.mozilla.org/es/docs/Glossary/DOM+ En este post vamos a realizar modificaciones y operaciones sobre elementos HTML, por lo que aprenderemos a realizar cambios sobre el DOM desde Drupal.
- jQuery: Es una mítica librería basada en JavaScript para facilitar (Teoricamente) manipulaciones del DOM. En Drupal (todavía) mantiene una presencia muy extensa, así que mejor llevarnos bien con ella. https://developer.mozilla.org/es/docs/Glossary/jQuery+ Vamos a ejecutar código jQuery en el contexto de Drupal.
3- Como incluir JavaScript en Drupal
Vamos a practicar con la inclusión de código JavaScript en nuestro proyecto. Para ello, crearemos un nuevo módulo custom e iteraremos sobre él aportándole funcionalidad basada en JavaScript mientras tratamos los conceptos más importantes en los siguientes apartados.
3.1- Preparando el escenario: Crear un módulo custom
Para empezar, definamos el nuevo módulo custom con el que trabajaremos. No sé que contexto tienes respecto a Drupal, así que te anoto aquí una secuencia de enlaces con los que puedes ponerte al día. Necesitarás un entorno XAMP+ con servidor web, base de datos y un Drupal desplegado y listo para usarse.
- Aquí podrás conocer las herramientas básicas Composer y Drush: https://medium.com/drupal-y-yo/composer-y-drush-en-el-contexto-de-drupal-9883d2cfb007+
- Aquí podrás ver como lanzar la instalación de Drupal mediante Composer y Drush en pocas instrucciones: https://gitlab.com/snippets/1897782+
- Aquí podrás ver como crear un módulo custom para Drupal usando Drupal Console: https://gitlab.com/snippets/1898128+
- Si quieres ir más rápido, abre VirtualBox en tu equipo y descarga / instala la máquina virtual ya configurada que describí aquí: https://medium.com/@davidjguru/drupal-workshops-2019-2020-184d980862c7+
Esta VM tiene todo preparado e incluso un módulo custom Hello World listo. Revisa la descripción del artículo.
Ej. 1: Preparar un módulo custom como escenario para pruebas
En el caso de que ya tengas un sitio Drupal disponible para pruebas, simplemente teclea esto desde consola estando dentro de tu proyecto y Drupal Console se encargará de crear el nuevo módulo:
// Using Drupal Console with params.
drupal generate:module \
--module="Custom Module for JavaScript" \
--machine-name="javascript_custom_module" \
--module-path="modules/custom" \
--description="This is a custom generated module for JavaScript." \
--core="8.x" \
--package="Custom" \
--module-file \
--no-interaction
También puedes descargarte este módulo básico creado para ejemplos desde mi repositorio de Gitlab: https://gitlab.com/davidjguru/drupal-custom-modules-examples/tree/master/basic_custom_module+.
O hacer git clone del repositorio completo de módulos custom de ejemplos: https://gitlab.com/davidjguru/drupal-custom-modules-examples+.
Este módulo es sencillo y útil para nuestras primeras pruebas: una vez activado, se encarga simplemente de crear una ruta /basic/custom con un controlador que responde creando y devolviendo un render array de Drupal con un sencillo markup para HTML. Con esto podemos empezar a probar.
A continuación vamos a generar algunos contenidos de manera autómatica para nuestro escenario de ejercicios / pruebas. Podemos renombrarlo si queremos, para particularizarlo un poco más (yo pasaré a nombrarlo javascript_custom_module
para evitarme confusiones con otros módulos de prueba.
Vamos a instalar, activar y generar un set de comentarios aleatorios dentro de nuestra plataforma. Para ello usaremos el módulo Devel+ y su submódulo Devel Generate+ para crear contenido de pruebas, añadiendo comandos y subcomandos nuevos a Drush+. Usaremos Composer y Drush desde el interior de la carpeta del proyecto en consola, tecleando:
composer require drupal/devel
drush en devel devel_generate
drush genc 10 5 --types=article
¿Conoces Composer? ¿tienes algo de experiencia con Drush? si la respuesta es no, te invito también que pases por este artículo anterior (tiene ya algo de tiempo) acerca de estas dos herramientas básicas para el día a día con Drupal: https://medium.com/drupal-y-yo/composer-y-drush-en-el-contexto-de-drupal-9883d2cfb007+
Con estas instrucciones anteriores antes le pedimos a devel-generate que cree diez nodos de tipo artículo (por defecto en Drupal) con un set de comentarios de 0 y 5 por nodo. Ahora tenemos diez nodos iniciales para construir nuestro escenario inicial de ejercicios:
A continuación, vamos a reordenar lo que originalmente devolvía este Controlador de ejemplo. Hasta ahora era simplemente un mensaje de texto, pero ahora vamos a añadirle una tabla con comentarios asociados al usuario actual. Para ello vamos a realizar una consulta la base de datos usando el servicio database
, extraremos los valores devueltos y los procesaremos lanzándolos al sistema de renderizado en forma de tabla. Para la consulta filtrada por los datos del usuario actual a través del servicio current_user
.
Veamos, ahora la clase del controlador quedaría así:
Lo que una vez habilitado el módulo de pruebas (usando Drupal Console) drupal moi javascript_custom_module
generará la ruta /javascript/custom
a través del controlador y renderizará en pantalla la siguiente tabla:
Con este paso, ya tenemos preparado el escenario inicial y podemos pasar a realizar ejercicios directamente con JavaScript.
3.2- El concepto de librería (library)
Trabajar tanto con CSS como con JS a partir de Drupal 8 se ha transformado en una manera estandarizada. En versiones anteriores de Drupal había que usar funciones concretar para añadir recursos CSS o JS.
Como ya expuse en este snippet+, tenías que usar cosas como drupal_add_html_head()+ para añadir nuevos tags HTML, drupal_add_js()+ para incorporar JavaScript o la función drupal_add_css()+ para sumar más hojas de estilo.
3.2.1- Secuencia para definir librerías
A partir de Drupal 8, la secuencia de inserción de librerías se ha homogeneizado, y consiste en cumplir estos tres pasos:
- Crear los ficheros CSS/JS.
- Definir una librería que incluya estos ficheros.
- Añadir dicha librería a un render array típico de Drupal.
Pero en este caso vamos a invertir los pasos 1 y 2: primero veamos como crear la librería y luego hablaremos del propio fichero JavaScript en sí, que podría ser algo más complejo.
Ej. 2: Definir nuestra nueva librería
Veamos: En nuestro módulo custom, incluiremos un nuevo fichero nombre_modulo.libraries.yml para definir estas nuevas dependencias, en este caso, un fichero basic_custom_module.libraries.yml con el siguiente contenido:
// Case 1: Basic library file with only JavaScript dependencies.
module_name.library_name:
js:
js/hello_world.js: {}// Ejemplo
custom_hello_world:
js:
js/hello_world.js: {}
Todas las librerías se declararán, como regla de estilo, en el mismo fichero .libraries.yml, donde de manera agrupada por funcionalidades o uso, iremos describiendo de la forma que vemos arriba todas las librerias que vayamos necesitando en nuestro proyecto.
Aquí puedes ver varios ejemplos de definición de librerías para Drupal:
Sobre la declaración de librerías, podemos añadir un par de curiosidades que mola saber:
3.2.2- Librerías para cargar en head
Por defecto, todas las librerías tenderán a cargarse en el footer: para evitar operar sobre elementos del DOM (Document Object Model) que no se hayan cargado todavía, los archivos JS se incluirán en el final del DOM. Si por alguna razón necesitas cargarlo en el inicio, entonces, puedes declararlo explicitamente mediante el parámetro /valor “header: true”:
js_library_for_header:
header: true
js:
header.js: {}js_library_for_footer:
js:
footer.js: {}
3.2.3- Librerías como recursos externos
Estamos viendo ejemplos de creación de librerías custom propias, pero también es posible declarar en el fichero .libraries.yml de nuestro módulo custom el uso de alguna librería externa que esté disponible vía CDN, repositorio externo.
Es posible solicitar a Drupal el uso de una librería externa para incorporarla a nuestro proyecto, tal y como podemos ver en el ejemplo del uso de backbone.js en el core de Drupal, creada por terceros, incorporada a Drupal y declarada coherentemente con sus datos externos:
Por cierto, en dicho fichero core.libraries.yml podrás consultar todos los recursos JavaScript declarados a nivel del core de Drupal: algunos los usaremos a lo largo de este artículo.
Pero el uso de backbone no deja de ser un asunto local, por lo que debemos preguntarnos ¿es posible usar una librería directamente desde remoto? veamos que dice la documentación oficial de Drupal en ese sentido:
“You might want to use JavaScript that is externally on a CDN (Content Delivery Network) to improve page loading speed. This can be done by declaring the library to be “external”. It is also a good idea to include some information about the external library in the definition.”
O sea que podemos hacer esto:
angular.angularjs:
remote: https://github.com/angular/angular.js
version: 1.4.4
license:
name: MIT
url: https://github.com/angular/angular.js/blob/master/LICENSE
gpl-compatible: true
js:
https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js: { type: external, minified: true }
Tal y como se recoge aquí: https://www.drupal.org/docs/8/creating-custom-modules/adding-stylesheets-css-and-javascript-js-to-a-drupal-8-module#external+
3.2.4- Librerías y dependencias
Es posible que dentro de nuestro código JavaScript, en su propio fichero .js, necesitemos usar otra librería de terceros para nuestras funcionalidades. Bien, en ese caso, podemos declarar librerías con dependencias siguiendo un esquema básico proveedor/recurso o vendor/library.
Veamos un ejemplo en el que pretendemos usar un efecto hide/show. Como tales animaciones están disponibles en la librería jQuery y esta se encuentra integrada en Drupal (lo veremos más adelante), entonces en lugar de crear esas funciones declaramos la dependencia y podremos usarlas:
js_library_hide_show:
js:
js/my_custom_javascript_library.js: {}
dependencies:
- core/jquery
Además, hay un set de opciones que puedes usar como atributos para particularizar todavía más el uso de tus nuevas librerías CSS / JavaScript:
3.3- El archivo JavaScript
El siguiente paso será definir ese fichero JavaScript que hemos declarado como recurso dentro de la nueva librería anterior.
Ej. 3: Definir nuestro fichero JavaScript inicial
Para eso, creamos una carpeta /js y dentro nuestro fichero hello-world.js
para nuestra nueva librería con una pequeña acción, solo saludar por Consola:
(function () {
'use strict';
// Put here your custom JavaScript code.
console.log ("Hello World");
})();
Así que la estructura interna del módulo custom para pruebas debería quedar así:
3.4- Añadiendo librerías JavaScript
El paso que tenemos pendiente es el de relacionar la nueva librería con su fichero JavaScript .js asociado al contexto en el que debe funcionar ¿no? bien para eso vamos a plantear un caso base y luego vamos a añadir más casos probables, dado que en Drupal es posible adjuntar librerías JavaScript de varias formas, según necesitemos usarlas en nuestro código.
Pero veamos primero el caso base para nuestro caso: #attached.
3.4.1- Uso de propiedad #attached en Render Arrays
De un lado, tenemos los sempiternos Render Arrays en Drupal, es decir, los arrays cargados con propiedades, valores, parámetros y demás que usamos para enviar al sistema de renderizado de Drupal para que lo transforme todo y termine pintando HTML renderizable en un navegador.
Por otro lado, tenemos una propiedad llamada “#attached” que nos ofrece un conjunto de sub-propiedades ya definidas que nos permiten adjuntar recursos de diferentes naturaleza a cualquier render array que estemos usando (una respuesta de controlador, un build de formulario, etc):
- Library -> $render_array[‘#attached’][‘library’]
- drupalSettings (from PHP to JavaScript) -> $render_array[‘#attached’][‘drupalSettings’]
- Http_Header -> $render_array[‘#attached’][‘http_header’]
- HTML Link in Head -> $render_array[‘#attached’][‘html_head_link’]
- HTML Head -> $render_array[‘#attached’][‘html_head’]
- Feed -> $render_array[‘#attached’][‘feed’]
- Placeholders -> $render_array[‘#attached’][‘placeholders’]
- HTML Response Placeholders -> $render_array[‘#attached’][‘html_response_attachment_placeholders’]
Volveremos sobre algunos de estos casos en apartados siguientes. Para más info sobre el procesamiento de adjuntos por parte de las clases PHP correspondientes, visita la documentación oficial de la API de Drupal+.
Ej. 4: Añadiendo librerías a nuestro código en Drupal
De momento, ahora nos es suficiente con acudir al fichero de la clase .php del controlador y modificar el render array que es devuelto por este, incluyendo el #attached con nuestra nueva librería:
// Ruta: javascript_custom_module/src/Controller/
// Fichero: CommentsListController.php
// Función: gettingList()// Antes (línea 42):
$final_array['welcome_message'] = [
'#type' => 'item',
'#markup' => $this->t('Hello World, I am just a text.'),
];// Ahora (línea 42):
$final_array['welcome_message'] = [
'#type' => 'item',
'#markup' => $this->t('Hello World, I am just a text.'),
'#attached' => [
'library' => [
'javascript_custom_module/js_hello_world_console',
],
],
];// Forma:
$attachments['#attached']['library'][] = 'module/library';
Y tras este cambio, si probamos a reinstalar nuestro módulo custom y limpiamos caché ( drupal mou javascript_custom_module
+ drupal moi javascript_custom_module
que ya incluye la limpieza de caché), ya podemos ver desde la consola de Navegador el resultado de la ejecución de nuestro primer código JavaScript:
¡Hemos realizado nuestra primera interacción con JavaScript en Drupal!
Bien, ahora vamos a seguir añadiendo nuevos casos de incorporación de JS, y luego volveremos sobre este mismo caso inicial para seguir iterando y observando más y más funcionalidades disponibles.
Al hilo de este simple ejercicio inicial, podemos comprobar el funcionamiento de métodos básicos de JavaScript como una ventana de alert o una de confirmación a través de la integración de librerías mediante la propiedad #attached
:
3.4.2- Librerías en una plantilla Twig
Para añadir librerías a una plantilla Twig dentro de nuestro proyecto, bien sea para un plantilla custom dentro de un módulo propio o bien sea en una plantilla Twig específica del Theme que estamos usando, la cargaremos a través de la función de Twig attach_library()
que permite añadir directamente a plantilla:
{% block salute %}
{% if salute_list is not empty %}
{{ attach_library('custom_module_name/library_name') }}
<div class="salute__wrapper layout-container">
{{ parent() }}
</div>
{% endif %}
{% endblock salute %}
Aunque lo cierto es que puede causar problemas en el renderizado (que no llegue a tiempo para cargarse en el ciclo de procesamiento de render arrays del sistema de Render que se pone en marcha a la hora de “pintar” una página) si se añade a la plantilla global html.html.twig
. Este es un debate iniciado hace tiempo: https://www.drupal.org/node/2398331#comment-9745117+ y que además está sometida a debate de cara a modificar la manera de cargar las librerías en un futuro inmediato: https://www.drupal.org/project/drupal/issues/3050386+ .
Lo dicho: cuidado con la plantilla en la que lo usas que podría no funcionar y pon atención a cambios que puedan venir en nuevas versiones de Drupal.
3.4.3- Librerías de manera global para un Theme
Para declarar tu librería como dependencia global para tu Theme o tu módulo custom, basta incluirla en el fichero declarativo del recurso *.info.yml
usando la propiedad libraries
:
# resource.info.yml
libraries:
- module/library
En cualquier caso y al igual que en el apartado anterior, existen debates acerca de la evolución de esto desde hace tiempo atrás y algunas medidas que se supone serán tomadas para versiones próximas: https://www.drupal.org/node/1542344+
El consejo sigue siendo el mismo: Poned atención a los posibles cambios.
3.4.4- Inclusión de librerías mediante Hooks
También es posible añadir nuevas librerías custom en nuestro contexto Drupal, específicamente antes del momento de renderizar páginas ya existentes, a través de hooks de pre-procesamiento, como el hook_page_attachments()
, que sigue manteniendo la forma ya vista de añadir recursos:
// Forma:
$attachments['#attached']['library'][] = 'module/library';
Como esquema de ejemplo:
/**
* Implements hook_page_attachments().
*/function custom_page_attachments(array &$attachments) {$attachments['#attached']['library'][] = 'module/library';
}
Otra opción en hooks es la función hook_preprocess_HOOK()
que según su documentación, facilita a los módulos preprocesar variables de theming para diversos elementos. Veamos un par de ejemplos:
/**
* Implements hook_preprocess_HOOK() for menu.
*/
function theme_name_preprocess_menu(&$variables) {
$variables[‘#attached’][‘library’][] = ‘theme/library’;
}
La ejecución de este hook anterior hará que Drupal vaya a buscar menu.html.twig
y realizará el añadido de la librería diferenciada.
Además este recurso puede usarse de manera genérica (por ejemplo, para todas las páginas):
/**
* Implements hook_preprocess_HOOK() for page.
*/
function custom_theming_preprocess_page(&$variables) {
$variables['#attached']['library'][] = 'module/library';
}
En este caso se recomienda especificar metadatos para facilitar el cacheo del nuevo cambio, específicamente si el funcionamiento de agregación de la nueva librería depende de condiciones, por ejemplo:
/**
* Implements hook_preprocess_HOOK() for page with conditions.
*/
function custom_theming_preprocess_page(&$variables) {
$variables['page']['#cache']['contexts'][] = 'route';
$route = "entity.node.preview"; if (\Drupal::routeMatch()->getRouteName() === $route) {
$variables['#attached']['library'][] = 'module/library';
}
}
Y para recursos más concretos:
/**
* Implements hook_preprocess_HOOK() for maintenance_page.
*/
function seven_preprocess_maintenance_page(&$variables) {
$variables[‘#attached’][‘library’][] = ‘theme/library’;
}
4- Un poco más de JavaScript en Drupal
Vamos a aproximarnos mejor a las reglas de uso e integración de código JavaScript en un proyecto Drupal.
4.1- Estructura y pautas: IIFE
Lo primero que debería llamar nuestra atención es el hecho de que la estructura del fichero de extensión .js que hemos introducido en nuestro proyecto a través de la carpeta /js tiene la siguiente estructura:
(function () {
'use strict';
// Put here your custom JavaScript code.
console.log ("Hello World");
})();
En Drupal, todo nuestro código JavaScript se integrará dentro de una función de closure, como un wrapper del código basado en el patrón IIFE, esto es, el modelo “Immediately Invoked Function Expression (IIFE)” (Funciones que son invocadas inmediatamente), usado como una estructura útil por tres cuestiones clave:
- En primer lugar, permite una ejecución inmediata (o auto-ejecución).
- Por otro lado, limita el alcance de las variables internas: no altera otros códigos JavaScript presentes en el proyecto.
- El context execution de la IIFE se crea y se termina destruyendo automáticamente: libera espacio en memoria, y lo libera rápido.
¿Cómo se consigue esto? Bien yo creo que podemos comprender el modelo IIFE de una manera intuitiva en cuatro pasos. Veamos:
- Podemos crear una función en JavaScript con normalidad:
function myFunction() {
// Here your JavaScript code.
}
2. Esta función puede tener un nombre o bien no tenerlo (ser una funcion anonima) pero debe ser asignada a una variable:
// Funcion con nombre:
function myFunction(){ // Here your JavaScript code. } -> Correcto// Función anónima asignada a una variable:
var myFunction = function() { // Here your JavaScript code. } -> Correcto// Función anónima sin asignación a variable:
function() { // Here your JavaScript code. } -> Error JavaScript
Así que JavaScript no nos permite ejecutar la función, ya que tras la palabra clave “function” se pone a esperar un nombre que no encuentra.
3. Esto puede evitarse introduciendo la función anónima entre paréntesis (bueno en realidad solo con anteponerle un signo ya serviría pero adoptamos este consenso de los paréntesis como pauta de estilo). Así se consigue que el motor de JavaScript la considere una expresión, o Function Expresion (en lugar de Function Statement, con nombre):
(function() {
// Here your JavaScript code.
})
4. La función queda en memoria pero nadie la está usando. ¿Cómo la ejecutamos? Bien, podemos usar el paréntesis final para llamar a su ejecución:
(function() { // Here your JavaScript code. })()- Lo que es solo una pauta de estilo, dado que esto también sirve:(function() { // Here your JavaScript code. }())- Hemos pasado el paréntesis invocador al interior de la expresión.
De hecho, si introducimos parámetros en los paréntesis de ejecución, la función los tratará con absoluta normalidad. Veremos un ejemplo más adelante a través de un pequeño ejercicio (Ej. 5: Pasando valores al formato IIFE).
Además al ser una función anónima, admite ser usada como una “función flecha” (arrow function):
(() => { // Here your JavaScript code. // })()
Estas últimas son las formas que puede adquirir nuestro código JavaScript en Drupal. Recordad que sea cual la sea la pauta de estilo que escojamos, siempre necesitamos cumplir dos pautas fundamentales:
- Que se construyan de manera compartimentada, sin que se “contamine” ningún objeto global, es decir, el espacio global de ejecución (que las variables solo vivan dentro de su función, como un bloque de código privado).
- Que se ejecuten inmediatamente, sean destruidas y las mismas no puedan volver a ser ejecutadas (si se vuelve a cargar una página, se vuelven a solicitar de nuevo).
Tened en mente:
4.2- Paso de parámetros en IIFE
- Vamos a realizar modificaciones sobre el renderizado HTML de nuestro Drupal a través de nuestro módulo custom, para lo que primero debemos asignar un selector propio al elemento que queremos modificar.
Ej. 5: Pasando valores al formato IIFE
Empezamos por volver al fichero de la clase controladora y añadir dos nuevas propiedades del sistema de renderizado de elementos de Drupal: #prefix
y #suffix
que permiten enmarcar un elemento HTML dentro de otras etiquetas HTML. En este caso queremos añadir un id propio al elemento.
// Línea 42.
$final_array['welcome_message'] = [
'#type' => 'item',
'#markup' => $this->t('Hello World, I am just a text.'),
'#prefix' => '<div id="salute">',
'#suffix' => '</div>',
'#attached' => [
'library' => [
'javascript_custom_module/js_hello_world_console',
],
],
];
2. A continuación creamos un nuevo fichero .js (‘iife_salute_example.js’)con una función en formato IIFE. A esta función le pasaremos una cadena de texto a modo de saludo para nuestros usuarios (‘Dear User’), y decláraremos el parámetro de entrada en su definición (‘parameter’).
(function (parameter) {
'use strict';
// Get the HTML element by it ID.
let element = document.getElementById("salute");
console.log(element);
// Add to the HTML the new string using the parameter.
element.innerHTML += "Salute, " + parameter;
// Creating and adding a line for the HTML element.
var hr = document.createElement("hr");
console.log(hr);
element.prepend(hr);
})('Dear User');
Introducimos algunos cambios con JavaScript puro, como por ejemplo añadir un texto al mensaje del elemento HTML, tomando el valor de la cadena de texto pasada por parámetro. Luego también ponemos una línea divisoria sobre el elemento, a modo de separador.
3. Añadimos el nuevo fichero a los recursos de la librería que ya habíamos definido anteriormente:
js_hello_world_console:
js:
js/hello_world_console.js: {}
js/iife_salute_example.js: {}
Y así, si limpiamos caché drush cr
y recargamos la ruta /javascript/custom
en navegador, podremos ver los nuevos cambios realizados mediante JavaScript:
Por consola vemos las respuestas que hemos anotado y el despertar asíncrono de la llamada de tres segundos que pusimos en el ejercicio anterior.
4.3- Pasando valores de PHP a JavaScript: drupalSettings
Hemos visto en el apartado anterior como pasar valores a esa IIFE dentro de la revisión de la estructura y el funcionamiento de este formato de código JavaScript y ahora vamos a detenernos en una construcción muy particular que está disponible para que realicemos conexiones entre nuestro código ejecutable en servidor (PHP) y nuestro código ejecutable en cliente (JavaScript) dentro de Drupal: hablemos de drupalSettings.
Pensemos en implementar un saludo algo más particularizado a la persona usuaria que visite nuestra url /javascript/custom
. Queremos extraer datos acerca de la identidad del visitante para poder darle un saludo algo más personal. Nosotros podemos sacar esta información dentro de nuestro controlador a través del servicio current_user
, que nos ofrece métodos para obtener esta información. Queremos llevar esta información al código que se ejecuta en el cliente, así que lo transferiremos a JavaScript. Podemos transferirlo todo a través de drupalSettings, una sub-propiedad disponible para la propiedad #attached
, que se recibe del lado de JavaScript a través del objeto drupalSettings, que tendrá los valores disponibles como nuevas propiedades.
Ej. 6: Transferir valores a través de drupal.Settings
Vamos a crear un nuevo fichero JavaScript para un saludo más particular, llamado hello_world_advanced.js
Por un lado, extreamos la información y añadimos la nueva librería desde el lado PHP:
// We're adding the new resources to the same welcome element.
$final_array['welcome_message']['#attached']['library'][] = 'javascript_custom_module/js_hello_world_advanced';$final_array['welcome_message']['#attached']['drupalSettings']['data']['name'] = $this->current_user->getDisplayName();$final_array['welcome_message']['#attached']['drupalSettings']['data']['mail'] = $this->current_user->getEmail();
Por otro lado, obtenemos la información desde el lado JavaScript:
(function () {
'use strict';
// Recovering the user name and mail from drupalSettings.
let element = document.getElementById("salute");
let user_name = drupalSettings.data.name;
let user_mail = drupalSettings.data.mail;
// Add to the HTML the new strings.
element.innerHTML += "Update-> You are the user: " + user_name +
" with mail: " + user_mail;
})();
Añadiendo la librería drupalSettings (en el core de Drupal) como una nueva dependencia, podemos empezar a conectar variables entre PHP y JavaScript. Modificamos nuestro fichero de definición de librería para definir unnuevo recurso que haga uso de esta dependencia:
js_hello_world_advanced:
js:
js/hello_world_advanced: {}
dependencies:
- core/drupalSettings
Así podemos ver los nuevos valores cargados tanto desde el renderizado web como desde el propio objeto global drupalSettings a través de la consola:
4.4- Modificando el HTML renderizado
Vamos a usar este apartado para ampliar funcionalmente nuestro módulo custom para JavaScript implementando algunas funcionalidades sencillas e interesantes, para seguir practicando con JavaScript en el contexto de Drupal y normalizar el uso de este en nuestros proyectos.
4.4.1- Contador de visitas basado en Web Storage
Veamos…¿Conocéis el concepto de “Web Storage”? bueno, resumidamente, es una pequeña API de HTML+ disponible en los navegadores modernos para almacenar información internamente a través de dos mecanismos: Session Storage (para información mantenida solo en el contexto de la sesión de la página abierta) y Local Storage (para persistir información hasta que la retiremos de manera explícita).
(https://developer.mozilla.org/es/docs/Web/API/API_de_almacenamiento_web+)
Aquí puedes testear la disponiblidad y capacidad (normalmente en torno a 5MB) de tu navegador para web Storage (Local y Session): http://dev-test.nemikor.com/web-storage/support-test/+
Ej. 7: Contador de visitas custom con JavaScript
Bien, vamos a crear un pequeño contador de visitas persistente para informar a la persona usuaria del número de veces que ha cargado nuestra ruta custom /javascript/custom/
.
En primer lugar, pedimos los valores actuales:
// Asking for the localStorage parameter value if exists.
let visit_value = localStorage.getItem('visit_number');
console.log("LocalStorage - current value: " + visit_value);
// Same but for the sessionStorage.
let session_value = sessionStorage.getItem('session_number');
console.log("SessionStorage - current value: " + session_value);
Luego a continuación comprobamos si estan ya creados e inicializados. Si están a null, los creamos y cargamos con un valor inicial igual a uno. Si ya existen los incrementamos y los cargamos de nuevo actualizados. Aprovechamos para mostrarlos a través de la consola:
// Testing the localStorage visit value.
if(visit_value === null) {
// If null we'll create the initial value.
localStorage.setItem('visit_number', 1);
console.log("LocalStorage: " +localStorage.getItem('visit_number'));
}else {
// If not null we'll increment the current value.
localStorage.setItem('visit_number', ++visit_value);
console.log("LocalStorage: " + localStorage.getItem('visit_number');
}
// Same for sessionStorage.
if(session_value === null) {
// If null we'll create the initial value.
sessionStorage.setItem('session_number', 1);
console.log("Session: " + sessionStorage.getItem('session_number'));
}else {// If not null we'll increment the current value.
sessionStorage.setItem('session_number', ++session_value);
console.log("Session: " + sessionStorage.getItem('session_number'));
}
Finalmente, aprovechamos para mostrar los valores del contador en el HTML de la página:
// Add to the HTML the counter value.
element.innerHTML += "<br>" + "Total visits: " +
localStorage.getItem('visit_number');element.innerHTML += "<br>" + "Total visits during this session: " +
sessionStorage.getItem('session_number');
Lo que al recargar la dirección, ya mostrará los valores de registro a través de a API Web Storage:
5- Drupal y el viejo jQuery
Según su propia misión, “the purpose of jQuery is to make it much easier to use JavaScript on your website” . Y así lleva muchos años. Es, en resumen, una librería JavaScript creada para ofrecer una manera estandarizada (o algo así) para interactuar con los elementos del Document Object Model (DOM) de la manera más sencilla y directa posible.
jQuery tiene, en el momento de escribir estas líneas, unos catorce años de vida desde su primera versión publicada y un uso extensivo a lo largo y ancho de todos los sitios web publicados en Internet. Sin caer en holy wars tecnológicas, nos limitaremos a asumir que sigue estando presente (por ahora) en el desarrollo de Drupal y que varias versiones y formatos de jQuery se ofrecen dentro de la plataforma. Veremos como usarla y como relacionarnos con ella de manera (relativamente) eficiente.
5.1- Viaje rápido por las claves de uso de jQuery
Como este articulo no es en si mismo un tutorial de jQuery y temo que al final la extension del mismo supere las seis mil palabras, vais a disculparme que no me detenga mucho aqui. jQuery requiere otro manual de la misma (o mayor) extension. Así que vamos a dar un poco de contexto a través de algunas claves básicas y seguiremos camino. Atentis.
Remember:
- En jQuery, $ es un alias para jQuery.
- Normalmente, jQuery inicia cuando el documento está cargado por completo, a través de la instrucción:
$(document).ready(function(){ // }
. - jQuery ofrece miles de formas de interactuar con elementos HTML, desde selectores a través del id del elemento (#id), de su clase CSS (.class), de nombres de etiquetas HTM (“div”), o valores de atributos (name = value). La lista y sus opciones es interminable y conviene tenerla algo probada: https://api.jquery.com/category/selectors+
- Con el manejo de sus selectores, podrás realizar cambios a varios niveles en tu HTML: estilos CSS, añadir/alterar/retirar elementos, agregar efectos visuales, realizar callbacks y peticiones Ajax. Para todo esto te servirá jQuery.
Y no olvides contemplar las recomendaciones de buen uso de jQuery: http://lab.abhinayrathore.com/jquery-standards+
5.2- Disponibilidad de jQuery en nuestra versión de Drupal
En Drupal 8 cambió el sistema de carga de librerías y recursos, haciendo que nada (o casi nada) se cargase por defecto. Esto, entre otras cosas, pasa porque jQuery no se incluye en cada páginas a no ser que lo solicites como dependencia para tu recurso (una dependencia de librería para tu módulo o theme, declarada como ya hemos visto).
En este momento, todas las librerías relacionadas con jQuery están declaradas de antemano pero solo estarán pre-cargadas si las necesitas. Estas librerías pueden localizarse en el archivo /core/core.libraries.yml:
Donde a partir de la línea 350 del fichero puede verse el listado de librerías de jQuery asociadas al core de Drupal. Como podéis ver, hay multitud de librerías jQuery declaradas, algunas para ser solicitadas como dependencias de manera explícita en recursos custom (modulos o themes) y otras para consumo interno, ya que en ocasiones, Drupal usa por debajo plugins jQuery para construir elementos como botones, pestañas de navegación y otros recursos.
Aquí tienes una gráfica preparada en 2015 por @nod_+ acerca del uso extensivo que Drupal hace de jQuery:
http://read.theodoreb.net/2015/viz-drupal-use-of-jquery.html+
5.3- Usar una versión jQuery diferente de las disponibles
Supongamos que por alguna necesidad específica del proyecto, necesitamos usar una versión de jQuery diferente de las que están soportadas dentro de nuestra versión de Drupal, ¿Qué hacer? (se preguntó el sabio). Bien, podemos sumarla como recurso a nuestro proyecto sin problemas a través de las pautas que ya conocemos:
jquery-custom:
remote: https://github.com/jquery/jquery
version: "2.2.4"
license:
name: MIT
url: https://github.com/jquery/jquery/blob/2.2.4/LICENSE.txt
gpl-compatible: true
js:
js/jquery-2.2.4.min.js: { minified: true, weight: -20 }
Y luego podemos sobreescribir la dependencia desde la declaración de esta en el fichero my_custom_resource.info.yml:
libraries-override:
# Replace the entire library.
core/jquery: my_custom_resource/jquery-custom
Ej. 8: Modificaciones basadas en jQuery
Vamos a realizar un par de ejercicios usando jQuery en nuestro módulo custom para pruebas de JavaScript.
1- Cargar texto Lorem Ipsum vía Ajax
Tras las pruebas anteriores con JavaScript, si vamos cerrando todas las ventanas que aparecen, nos quedaremos en nuestra ruta /javascript/custom
a solas con nuestra tabla de resultados de consulta de comentarios asociados al usuario actual, lo que era:
Vamos a aportarle un texto introductorio a la página a través del consumo de una API externa que nos proveerá de párrafos Lorem Ipsum de prueba.
Declararemos la nueva dependencia en el habitual fichero *.libraries.yml:
js_playing_with_jquery:
js:
js/playing_with_jquery: {}
dependencies:
- core/jquery
En este caso probaremos a cargar la nueva librería a través de un hook de tipo hook_page_attachments()
dentro del fichero javascript_custom_module.module
:
/**
* Implements hook_page_attachments().
*/
function javascript_custom_module_page_attachments(array &$attachments) {
// Getting the current route name.
$route_name = \Drupal::routeMatch()->getRouteName();
// Load the library only if match with the selected page by route.
if (strcmp($route_name, 'javascript_custom.hello') == 0) {
$attachments['#attached']['library'][] = 'javascript_custom_module/js_playing_with_jquery';
}
}
Y en la carpeta js/ crearemos el nuevo fichero playing_with_jquery.js
, en el que vamos a volcar toda la manteca.
Empecemos por aportarle algo de texto introductorio a la página. Para ello realizaremos una petición a la web baconipsum (https://baconipsum.com+) a través de su API, para lo que usaremos la función de jQuery $.getJSON()
que maneja tres parámetros: una dirección URL, unos datos para construir la solicitud y una función de callback para el caso de tener éxito en la petición. Esto en si es un wrapper proporcionado por jQuery para gestionar como una petición HTTP de verbo GET una solicitud en formato JSON (https://api.jquery.com/jQuery.getJSON+).
Veamos que haremos: En primer lugar añadiremos un nuevo contenedor HTML para los textos (<div id=’bacon-text’>), luego realizaremos la solicitud, recorreremos los resultados y cargaremos un nuevo párrafo (<p>) dentro del contenedor recién creado.
(function ($) {
'use strict';
$(document).ready(function(){
console.log("The Playing with jQuery script has been loaded");
$('#block-bartik-page-title').append("<div id='bacon-text'></div>");
$.getJSON('https://baconipsum.com/api/?callback=?',
{ 'type':'meat-and-filler', 'start-with-lorem':'1', 'paras':'4' },
function(baconTexts) {
if (baconTexts && baconTexts.length > 0)
{
$("#bacon-text").html('');
for (var i = 0; i < baconTexts.length; i++)
$("#bacon-text").append('<p>' + baconTexts[i] + '</p>');
$("#bacon-text").show();
}
});
});
})(jQuery);
Pero vamos a darle algo de movimiento gracias a las bizarr errrr…dinámicas funciones proporcionadas por jQuery. Vamos a replantear un poco este script inicial para hacer una carga progresiva de los párrafos de bienvenida Bacon Ipsum.
En primer lugar, pondremos un botón. Hemos churreteado demasiado ya la página renderizada y vamos a dejar limpia la vista antes de jugar con bacon.
// Creating the new elements just a div and a button.
$('#block-bartik-page-title').append("<input type='button' id='getting-bacon' class='btn-bacon' value='Bacon' />");
$('#block-bartik-page-title').append("<div id='bacon-text'></div>");
A continuación, le añadiremos a ese botón un evento de tipo click para que al ser pulsado, se ponga a procesar bacon:
// Adding a click event to the former button.
$('#getting-bacon').click(function () {// Processing bacon.
});
Por si ya tuviésemos bacon previo cargado, nos encargamos de limpiar un poco el div:
// Hidding the block for the next load.
$("#bacon-text").hide();
Y pasamos a procesar nuestro bacon:
// Getting values in JSON format.
$.getJSON('https://baconipsum.com/api/?callback=?',
{'type': 'meat-and-filler', 'start-with-lorem': '1', 'paras': '4'},
function (baconTexts) {
// We're in the callback function for success in JSON request.
if (baconTexts && baconTexts.length > 0) {
$("#bacon-text").html('');
// Loop into the received items.
for (var i = 0; i < baconTexts.length; i++) {
// Creating the naming keys.
var bacon_id = "bacontext_" + i;
var new_bacon = "<p" + " id='" + bacon_id + "'" + ">" + baconTexts[i] + "</p>";
// Add the new element to the div mother.
$("#bacon-text").append(new_bacon);
}
}
});
Para dinamizar un poco el asunto, le añadimos una de las animaciones menos ponzoñ…emm…más discretas de jQuery con un mensaje de confirmación para consola, la función .slideDown() https://api.jquery.com/slideDown/#slideDown-duration-complete+, que desplaza verticalmente el contenido de arriba a abajo:
// Show the div mother show in a progressive way.
$("#bacon-text").slideDown(7000, function(){
console.log("New bacon has been loaded");
});
Y al recargar todo, vemos el renderizado completo de todo el JavaScript de la página:
Aquí tenéis el código en forma de gist:
6- Drupal Behaviors
En este viaje que estamos realizando, ya sabemos como integrar JavaScript en nuestros módulos y proyectos, como crear interacciones, pasar parámetros entre PHP (servidor) y JavaScript (cliente), integrar jQuery en nuestras dependencias y como colofón a todo, para preparar el último paso que debería integrar todo lo anterior, debemos hablar acerca del concepto de “Drupal Behaviors”.
¿Qué es un Behavior? Es la manera organizada que nos ofrece Drupal para añadir e indexar comportamiento basado en JavaScript, mediante la extensión de un objeto propio hook_behavior
que a su vez forma parte de otro objeto global Drupal
6.1- Anatomía de un Behavior
Vamos a revisar la estructura básica funcional del Behavior en sí mismo, ya que este formato deviene en la forma esencial de la integración de JavaScript en Drupal y nos conviene, en primer lugar, conocer sus partes. Veamos.
Echemos un vistazo:
- namespace: Un Drupal behavior tiene que tener un nombrado específico y único de cara a ser localizado, identificado, ejecutado y retirado. Pasará a formar parte del objeto Behaviors y quedará indexado ahí. En este caso simplemente es nombrado como “namespace”.
- attach: Es la función a ejecutar en cuanto el Behavior sea cargado. Para las ejecuciones de Behaviors, se recorrerán los comportamientos indexados y para cada uno se llamará a su función “attach”, haciendo cada una lo que deba realizar.
- detach: Al igual que en el momento de añadir, se aporta una función a ejecutar en el momento de retirar el behavior del registro de comportamientos.
- context: Es una variable donde se carga el trozo de la página que se está transformando. En una carga inicial de la página, será el DOM completo, en operaciones AJAX será la pieza HTML correspondiente. Esta variable nos ayuda a afinar más con nuestras operaciones, por lo que debemos tener claro como manejarla.
- settings: Esta variable que vemos en la captura de pantalla es usada para transferir valores desde el código PHP a JavaScript y hacerlos disponible en la forma que ya vimos anteriormente desde nuestro código. Para ello debemos declarar como dependencia de nuestra librería JavaScript la
core/drupalSettings
. - trigger: La variable
trigger
que se pasa a la función asociada adetach
representa la condición para que se realice la desactivación del behavior, donde se admiten algunas causas:
- unload: Este es el motivo por defecto, significa que el elemento del context ha sido eliminado del DOM.
- serialize: Para formularios con AJAX, en los que se usa esta variable para enviar el propio formulario como contexto.
- move: El elemento ha sido desplazado de su posición en el DOM desde su ubicación inicial.
7. jQuery: En este caso, este punto representa simplemente el paso de parámetros a la IIFE, normalmente (jQuery, Drupal) como dependencias integradas disponibles para nuestro código.
6.2- El objeto global “Drupal”
Tal y como se recoje en la documentación oficial de Drupal, Drupal.behaviors es un objeto que en sí mismo forma parte del objeto JavaScript global “Drupal”, creado para toda la instancia de Drupal que se esté ejecutando. Esto era un concepto ya usado y explotado en versiones anteriores de Drupal, con algunos aspectos que se mantienen en el tiempo.
El principal: que los módulos que quieran implementar JavaScript deben hacerlo añadiendo lógica al Objeto Drupal.behaviors
. Veamos como, y conozcamos el origen de Behaviors: el objeto global “Drupal”.
Si conoces el concepto de “Objeto” en JavaScript, sabrás que se trata de una manera avanzada de manejar datos en JavaScript, y básicamente, consiste en una colección desordenada de información relacionada: tipos de datos primitivos, valores en propiedades, métodos…todo diseñado bajo una estructura básica de pares clave: valor (key: value).
// Ejemplo simple de Objeto JavaScript.let drupal_event= {
name: 'Drupal Camp Spain 2020',
location: 'Málaga',
original_location: 'Barcelona',
established: '2010',
displayInfo: function(){
console.log(`${drupal_event.name} was established in
${drupal_event.established} at
${drupal_event.original_location}`);
}
}drupal_event.displayInfo();
Este objeto anterior es perfectamente ejecutable en la consola JavaScript de vuestro navegador, y funcionará de la manera esperada:
Aquí podrás encontrar más información sobre el funcionamiento de los objetos en JavaScript+.
Los objetos en JavaScript pueden recorrerse, modificarse, eliminarse y sobre todo (por los motivos que nos ocupan ahora), pueden ampliarse. Esto es justo lo que ocurrirá con nuestro nuevo amigo, el objeto global “Drupal”, un recurso existente -siempre- en cualquier sitio Drupal instalado a partir de la librería drupal.js
presente en la ruta /core/misc/
:
Aquí en la imagen anterior vemos el fichero (un script fundamental en Drupal), que sirve tanto como para proporcionar de manera centralizada diversas APIs de JavaScript en Drupal como para facilitar un namespace común para agrupar todas las extensiones que se le realizarán al objeto global. De hecho si llamáis al objeto global Drupal, podréis ver el contenido base que trae:
De todo el listado anterior, tal vez sea Drupal.behaviors y sus métodos relacionados (attachBehaviors, detachBehaviors) los más importantes para nosotros ahora, aunque debemos reseñar algunas utilidades interesantes:
- La función Drupal.t, que equivale a la función de traducción t() para traducciones en Drupal: http://read.theodoreb.net/drupal-jsapi/Drupal.html#.t+
- La pequeña API Drupal.dialog, que simula el elemento de ventana de dialogo de HTML5: http://read.theodoreb.net/drupal-jsapi/Drupal.html#.dialog#~dialogDefinition+
- Drupal.theme, para invitar a procesar cualquier respuesta HTML que deba someterse a theming (es extendido por multitud de librerias de contrib, como el ckeditor): http://read.theodoreb.net/drupal-jsapi/Drupal.theme.html+
Bueno ya hemos visto un poco de teoría para ganar contexto…es el momento de practicar un poco. Ampliemos lo que ya sabemos hacer con un nuevo ejercicio:
Ej. 9: Ventana de diálogo a partir del objeto global “Drupal”
Vamos a tomar como referencia la APIdialog de Drupal para construir una ventana en nuestro proyecto a través de nuestro módulo custom.
Para empezar, vamos a registrar una nueva librería en nuestro módulo custom javascript_custom_module
, dentro del fichero javascript_custom_module_libraries.yml
que ahora quedaría así :
js_hello_world_console:
js:
js/iife_execution_example.js: {}
js/hello_world_console.js: {}
js/iife_salute_example.js: {}
js_hello_world_advanced:
js:
js/hello_world_advanced: {}
dependencies:
- core/drupalSettings
js_custom_dialog_window:
js:
js/custom_dialog_window: {}
dependencies:
- core/drupal
- core/jquery
- core/drupalSettings
A continuación cargamos la nueva librería como #attached
en nuestro render array devuelto por el controlador, a partir de la línea 55 en el fichero CommentsListController.php
:
$final_array['welcome_message']['#attached']['library'][] = 'javascript_custom_module/js_custom_dialog_window';
Y preparamos una ventana modal muy básica, basada en JavaScript puro. Este diálogo solo tendrá un mensaje simple y un botón para interactuar, en el que incluiremos un cambio de estilo sobre el elemento que contiene el mensaje.
Veamos el nuevo fichero custom_dialog_window.js
:
(function () {
'use strict';
// Put here your custom JavaScript code.
// First creating and initialising the new element.
let new_tag = document.createElement("P");
new_tag.setAttribute("id", "my_p");
new_tag.innerHTML = "Hello World from a custom Dialog Window.";
// Then we'll creating a new modal window.
Drupal.dialog(new_tag, {
title: 'Custom Dialog Window',
buttons: [{
text: 'Change colour',
click: function() {
let change_colour = document.getElementById("my_p");
change_colour.style.backgroundColor = "red";
}
}]
}).showModal();
})();
Podéis repasar todo el JavaScript asociado al objeto global “Drupal” gracias a la magnifica documentación que Théodore Biadala (@nod_+) publicó en su momento acerca de la Drupal JavaScript API: http://read.theodoreb.net/drupal-jsapi/index.html+
6.3- Comportamientos en Drupal
En un apartado anterior, ya vimos como ejecutar jQuery en nuestro código. Sabemos además que es importante consultar si el documento (DOM) ha sido ya cargado por completo antes de empezar a realizar acciones. Básicamente:
(function ($) {
'use strict';
$(document).ready(function() {
// Put here your jQuery code.
});
})(jQuery)
Pero pensemos detenidamente en esta ejecución: se realizará cuando el DOM haya sido cargado por completo (en un momento inicial), pero no realizará ajustes tras una carga parcial del DOM (por ejemplo, tras una ejecución AJAX que modifique solo una porción del DOM).
(function ($, Drupal ) {
'use strict';
// Put here your custom JavaScript code.
Drupal.behaviors.unsplash_connector = {
attach: function (context, settings) {
console.log("Loaded Unsplash Behavior");
},
detach: function (context, settings, trigger) {
// JavaScript code.
}
}
})(jQuery, Drupal);
Esto al ejecutarse realizará varias llamadas de impresión en Consola (en este caso, hasta tres veces):
¿A que es debido esto? bueno, como podemos comprobar usando breakpoints en la consola de depuración de JavaScript de nuestro navegador phavorito, la carga de behaviors por parte del objeto global Drupal se realiza varias veces durante el proceso de carga de un simple enlace: en este caso se produce una carga “total” del DOM y varias recargas “parciales” a través de AJAX. En cada caso se hace un procesamiento de behaviors a través del método:
Drupal.attachBehaviors (línea 17, librería drupal.js)
que carga una función que recorre todos los comportamientos y los va ejecutando según su contexto y sus parámetros:
El siguiente paso, es poner algo de control en la ejecución de la instrucción, pasándola de un modo activo (que escriba en consola justo al cargar) a un modo reactivo (que escriba solo cuando se realice una interacción):
(function ($, Drupal ) {
'use strict';
// Put here your custom JavaScript code.
Drupal.behaviors.unsplash_connector = {
attach: function (context, settings) {
$('#unsplash_section', context).click(function() {
console.log("Loaded Unsplash Behavior");
});
},
}
})(jQuery, Drupal);
Así que ahora hemos colocado sobre el selector ID de nuestro mensaje de bienvenida un evento de control de click, que al pulsar cargue el mensaje en consola:
Con este pequeño ejemplo anterior, hemos visto como añadir una pequeña funcionalidad basada en evento (click). Pasemos a hacer algo más entrenido.
Ej. 10: Board de imágenes de Unsplash vía API con Drupal.behavior
Vamos a implementar una funcionalidad que opere consumiendo una API externa a través de Drupal Behavior.
Vamos a practicar con una idea algo más avanzada (y más bonica): nos conectaremos a la API pública para aplicaciones de un servicio online de stock de imágenes desde un nuevo Drupal Behavior y desde ahí le realizaremos solicitudes de imágenes que luego mostraremos desde un board de imágenes custom en nuestro Drupal.
¿Qué necesitamos? bien para esta receta necesitaremos los siguientes ingredientes:
- Una cuenta para acceso de aplicaciones a la API de Unsplash, puedes hacerla aquí (https://unsplash.com/developers+) y extraer por un lado la direccion URL de peticiones (https://api.unsplash.com/search/photos)y tu clave de acceso privada para realizar peticiones. Registrate como persona usuaria de la API, da de alta una nueva app y extrae la clave:
- Una nueva librería JavaScript dentro de nuestro módulo custom con su propio fichero .js para almacenar este Behavior.
- Un set de ruta nueva declarada en el fichero de routing, una clase controladora nueva y un método que genere un render array de respuesta.
Para facilitar las siguientes integraciones, vamos a añadir al render array un par de propiedades (#prefix, #suffix) para añadir un nuevo <div> con id propio = unsplash (ver la imagen anterior).
Ahora con estos ingredientes, comenzamos. En primer lugar creemos el esqueleto de nuestro Behavior y definamos lo que solo queremos que se cargue una vez ( y no se recargue con AJAX):
(function ($, Drupal) {
'use strict';
Drupal.behaviors.getting_unsplash_items = {
attach: function(context, settings) {
$(context).find("#unsplash").once('unsplashtest').each(function() { // All our new functionality. }); }
};
})(jQuery, Drupal);
Recuerda: el término que le aportamos a jQuery.once() es totalmente aleatorio y no repetible, solo para que trackee a nivel interno que la acción ya ocurrió.
Primera parte: Creamos un mensaje de bienvenida y dos botones: uno para iniciar un proceso de búsqueda de imágenes y otro para limpiar el board de imágenes generado a partir de la búsqueda y la obtención de resultados.
// Adding the buttons through jQuery.
$("#unsplash", context).append("<button type='button' id='load_button'>Load Images</button>" );
$("#unsplash", context).append("<button type='button' id='clean_button'>Clean Board</button>" );
// Adding an event listener to the new buttons using jQuery.
$('#load_button').click(function() {
// In case of click we will clean the former message.
$("#message", context).remove();
// In case of click we will call to the prompt window.
processingKeywords();
});// Adding a second event listener to the clean button.
$('#clean_button').click(function() {
// In case of click we will clean the written former message.
$("#message", context).remove(); // And we will remove the entire image board too.
$("#image-board").remove();
});
Como vemos en una de las llamadas anteriores, el proceso de búsqueda de imágenes a partir de la introducción de una palabra clave comienza a ser delegado a funciones, empezado por la función processingKeywords(), con la que lanzamos un prompt para capturar la keyword y nos aseguramos de comprobar si se están aceptando términos vacíos:
function processingKeywords(){
let message = '';
let option = prompt("Please write a keyword for search: ", "boat");
if(option == null || option == ""){
// Null option without response.
message = "Sorry but the keyword is empty.";
// Render in screen the message from the prompt.
$("#unsplash", context).append("<br/><p id='message'>" + message + "</p>");
}else {
// Valid answer launches request.
message = "Ok, we're searching..." + option;
// Render in screen the message from the prompt.
$("#unsplash", context).append("<br/><p id='message'>" + message + "</p>");
// Launching external request with some delay with arrow format.
setTimeout(() => {
gettingImages(option);
}, 4000);
}
}
Y llamamos a la función responsable de gestionar las peticiones, gettingImages(), con la palabra clave introducida como parámetro. Usaremos async / await para evitar problemas de variables no inicializadas en caso de que el servicio se retrase un poco. Igualmente le damos un poco de delay a la llamada de la siguiente función para darle calidad a la película.
async function gettingImages(keyword){// Loading basic values Access Key and End Point.
const unsplash_key = 'YOUR APP KEY';
const end_point = 'https://api.unsplash.com/search/photos'; // Building the petition.
let response = await fetch(end_point + '?query=' + keyword + '&client_id=' + unsplash_key);
// Processing the results.
let json_response = await response.json(); // Getting an array with URLs to images.
let images_list = await json_response.results;
// Calling the createImages method.
creatingImages(images_list);
}
Y al final invocamos a la función que tomará la lista de direcciones de imágenes a partir de la que construiremos las etiquetas HTML correspondientes:
function creatingImages(images_list) {
// If a former image board exists we will delete it.
$("#image-board", context).remove();
// Creating a new image board as frame for the images.
$("#unsplash", context).append("<section id='image-board'> </section>");
// We will add some CSS classes for styling the image board.
$("#image-board").addClass("images-frame");
// Now we will set the received images inside the new board.
for(let i = 0; i < images_list.length; i++){
const image = document.createElement('img');
image.src = images_list[i].urls.thumb;
document.getElementById('image-board').appendChild(image);
}
// When finished we will put a border for the image board.
$(".images-frame").css({'background-color': '#babb8f', 'border': '5px solid #1E1E1E'});
}
Nota: Si buscas información sobre el uso de jQuery.once(), recuerda la transición en su uso desde Drupal 7 a Drupal 8 para el paso de funciones como parámetro ->
// Ejemplo Drupal 7
$(context).find(".element").once("random-key", function () {});// Ejemplo Drupal 8
$(context).find(".element").once("random-key").each(function () {});
Y así, si acudimos en nuestro Drupal de pruebas a la ruta:
http://drupal.localhost/unsplash/service
Ya tendremos disponible el nuevo tablero de imágenes obtenido desde la API de Unsplash y construido desde un Drupal Behavior.
Aquí tenéis disponible el código completo del Behavior que acabamos de implementar:
7- JavaScript sin JavaScript: #ajax, #states
Era necesario, al menos, hacer una reseña sobre estas areas de conocimiento donde el JavaScript es de manejo y ejecución indirecta. Está pero no se ve. El asunto es tan extenso y puede alcanzar un nivel que requeriría más artículos sobre el tema, por lo que me limitaré a hacer una reseña de algunas claves y lanzar el “to be continued…” para más adelante (bajo riesgo de que este artículo no viese la luz nunca).
7.1- (Breve) Introducción a la gestión AJAX en Drupal
La API de Ajax en Drupal (aquí puedes consultarla: https://api.drupal.org/api/drupal/core%21core.api.php/group/ajax/8.8.x+) contiene un conjunto de clases, eventos, recursos y posibilidades tan extenso como para poder hacer varios artículos de la extensión de este mismo solo acerca del uso de Ajax. Debido a las limitaciones respecto a la extensión de este tutorial, nos centraremos en algunas claves básicas, dejando para más adelante la posibilidad de preparar un artículo sobre cuestiones más avanzadas.
Podemos usar, a un nivel básico, Ajax para tres fórmulas bien conocidas:
- En enlaces: usando la clase ‘use-ajax’ en un enlace, podremos darle tratamiento Ajax.
- En elementos de formularios: Podemos sumar eventos Ajax a nuestros elementos de formulario mediante el uso de la propiedad #ajax dentro de la definición de un render array.
- En botones de formularios: añadiendo la clase ‘use-ajax-submit’ en la declaración del elemento, realizaremos una llamada con Ajax.
7.2- Elementos renderizados con la propiedad #states
La propiedad #states está disponible para su uso dentro de render arrays de Drupal y asignada a un elemento de formulario, permite añadir ciertas condiciones al comportamiento de dicho elemento, habilitando cambios de manera dinámica.
En realidad, la propiedad #states termina gestionándose desde la librería JavaScript drupal.states disponible para cargar como dependencia en la forma core/drupal.states, que apunta a la ruta donde se encuentra la librería /core/misc/states.js dentro de Drupal, aunque no es necesario realizar una carga explícita de la misma, ya que el sistema de renderizado que maneja los Render Arrays comprueba la existencia de la propiedad y en caso afirmativo, asigna directamente la librería JavaScript.
El uso de esta propiedad permite crear elementos dentro de un formulario que puedan alterar su estado -show, hide, disable, enable, etc.-en base a condiciones tanto propias del elemento como de otro elemento distinto del formulario (que al clickar uno se oculte otro, por ejemplo) y usando sintaxis de jQuery a la hora de declarar los selectores.
La mecánica consiste en que declararemos acciones desde nuestro lado y Drupal desde su lado proporcionará todo el Javascript/JQuery necesario para que esas acciones declaradas ocurran sobre la marcha. Todo se inicia con el uso de #states como una propiedad más a la hora de declarar el elemento del formulario, y a partir de ahí Drupal se encarga de añadir el Javascript necesario para cambiar elementos a través de la función drupal_process_states+, que se queda deprecated a partir de Drupal 8.8 y pasa a formar parte de la clase FormHelper: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21FormHelper.php/function/FormHelper%3A%3AprocessStates/8.8.x+
(aunque mantiene la misma mecánica).
States que pueden aplicarse en condiciones remotas (origen):
empty, filled, checked, unchecked, expanded, collapsed, value.States que pueden aplicarse sobre un elemento (destino):
enabled, disabled, required, optional, visible, invisible, checked, unchecked, expanded, collapsed.
La estructura básica de un state es la de un array multidimensional con la forma siguiente:
[
STATE1 => CONDITIONS_ARRAY1,
STATE2 => CONDITIONS_ARRAY2,
...
]
Donde un array de condiciones, a su vez, es otro array que almacena las condiciones previstas para el cambio de estado de ese elemento, mediante el esquema de uso de condiciones en #states:
'#states' => [
'STATE' => [
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
...
],
)],
Y en el siguiente bloque de código vamos a ver un ejemplo de uso de #states. En el contexto de un Formulario creado con la Form API de Drupal, hacemos que un tipo de campo textfield “Name”, reaccione al cambio de estado de una opción checkbox anterior. Si dicho checkbox es pulsado, entonces haremos que nuestro campo permanezca invisible.
$form['name'] = [
'#type' => 'textfield',
'#title' => t('Name:'),
'#weight' => 1,
'#states' => [
'invisible' => [
':input[name="newcheck"]' => ['checked' =>TRUE],
],
],
];
8- Posibles problemas y probables soluciones
8.1- Ejecución ralentizada por mal uso de “context”
En nuestros behaviors debe viajar, siempre, el contexto de ejecución de este. Es muy importante de cara al rendimiento, dado que facilita la localización de selectores HTML.
Esto puede verse con otro sencillo ejemplo, con lo que podemos observar la importancia de manejar la variable “context”: como ya hemos visto en apartados anteriores, en este valor siempre se almacena el objeto o parte de este que acaba de cambiar (al inicio en la primera carga, el DOM completo, luego en sucesivas llamadas AJAX será cada pieza HTML modifica). El no controlar esto, puede hacer que en cada ejecución de un behavior se busque un selector por todo el documento en lugar de su zona concreta, lo que puede ralentizar la ejecución del sitio web.
Así, un behavior definido tal como este:
(function ($) {
'use strict';
Drupal.behaviors.usingcontext = {
attach: function(context, settings) {
$("#unsplash").append('<p>Hello world</p>');
}
};
}(jQuery));
Generará la siguiente respuesta:
Tres ejecuciones (una por cada carga: 1 total DOM + 2 parciales AJAX).
En realidad, tendrá un comportamiento similar a este (dado que irá a buscar el selector a lo largo de todo el documento):
(function ($) {
'use strict';
Drupal.behaviors.usingcontext = {
attach: function(context, settings) {
$(document).find("#unsplash").append('<p>Hello world</p>');
}
};
}(jQuery));
Ahora bien, si le facilitamos el trabajo a jQuery de la mejor manera posible, conseguiremos un comportamiento más eficiente:
(function ($) {
'use strict';
Drupal.behaviors.usingcontext = {
attach: function(context, settings) {
$(context).find('#unsplash').append('<p>Hello world</p>');
}
};
}(jQuery));
Esta versión solo ejecuta el .append() una sola vez, dado que:
1. Se localiza el selector la primera vez, donde context = DOM completo.
2. No se vuelve a localizar el selector, ya que context = Pieza HTML AJAX.
Y dentro de nuestras opciones tenemos disponible el uso de jQuery.once() tal y como vimos en apartados anteriores, que tiene un funcionamiento similar a través de un selector random que añadimos para que pueda hacer el seguimiento interno de carga:
(function ($) {
'use strict';
Drupal.behaviors.usingcontext = {
attach: function(context, settings) {
$('#unsplash').once('cacafuti').append('<p>Hello world</p>');
}
};
}(jQuery));
Si además combinamos el uso de jQuery.once() con la segmentación propia a través de la variable “context” entonces obtenemos un funcionamiento más optimizado:
(function ($) {
'use strict';Drupal.behaviors.usingcontext = {
attach: function(context, settings) {
$(context).once().find('#unsplash').append('<p>Hello</p>');
}
};
O también podemos usar:
$('#unsplash', context).append('<p>Hello world</p>');
Lo importante realmente es que aprendamos a manejar la variable “context” para alivar la carga de trabajo de JavaScript ;-).
Aquí tenéis un set de pruebas de renderizado sobre Drupal Behaviors para que observéis el funcionamiento en pantalla:
8.2- Cargas de JavaScript fuera de contexto
Otro caso que hemos visto con cierta frecuencia al heredar un proyecto legacy (o relativamente nuevo pero sin respetar las pautas adecuadas), es el de cargas de librerías JavaScript destinadas solo a una página en concreto a lo largo de todo el sitio web (esto ocurre más de lo que pensamos).
Alguien pasó por el proyecto, recibió la tarea, googleo, resolvió la tarea como buenamente pudo y luego llegó la siguiente persona…así, cuando abres la consola del navegador, todo es un mar de warnings y errores de color rojo alertando de cargas de JavaScript que no se pueden realizar, dependencias que no se pueden resolver, o selectores que no localizan los elementos que deben. Es el momento de localizar las importaciones de nuestros recursos: cuáles son las librerías custom de JavaScript que se usan en el proyecto, dónde se están registrando y como se están añadiendo.
Entran en juego ahí tus habilidades para usar los buscadores de tu IDE de trabajo y localizar comportamientos mediante la consola, para cotejar:
- Cuáles están siendo creados.
- Cuáles están siendo ejecutados en ese momento.
Descubrirás algunas librerías que han sido añadidas al Theme en general y que en realidad solo deben ser añadidas por #attached solo a tal o cual página.
9- Enlaces y lecturas recomendadas
Fundamentos de JavaScript
Funciones en JavaScript y formato IIFE
- https://developer.mozilla.org/es/docs/Glossary/IIFE+
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript#Functions+
- https://medium.com/@vvkchandra/essential-javascript-mastering-immediately-invoked-function-expressions-67791338ddc6+
JavaScript y Drupal
- http://www.jaypan.com/tutorial/high-performance-javascript-using-drupal-7s-javascript-api+
- https://sqndr.github.io/d8-theming-guide/javascript/behaviors.html+
- https://stackoverflow.com/questions/3941426/drupal-behaviors+
- Ejemplo de Drupal.dialog: https://gist.github.com/devudit/dcdb76502975a13dd7c623cecc04f509+
- https://es.slideshare.net/ymbra/el-universo-javascript-en-drupal-8+
- https://stackoverflow.com/questions/34025396/how-to-open-a-modal-in-drupal-8-without-using-a-link+
- API de JavaScript a partir del objeto global “Drupal”: http://read.theodoreb.net/drupal-jsapi/+
jQuery