Drupal 9 API + React + TailwindCSS

background-drupal-react-tailwind.jpg
Solucionex
22
Oct 21

Bienvenidos a un nuevo artículo. Antes de nada comentaros que venimos de aquí: API desde cero con Drupal 9. En este artículo vamos a trabajar sobre la base que se vió ahí, así que si aún no le habéis echado un vistazo os lo recomiendo encarecidamente.

El tema que vamos a tratar hoy va a ser la integración de la API que hicimos anteriormente con las vistas de Drupal. En este caso vamos a usar React como aplicación para consumir los datos de nuestro gestor de contenido. De esta manera podemos aprovechar todo el potencial que proporciona Drupal para la gestión de datos, relaciones, seguridad...; para construir nuesta aplicación independiente en la que presentaremos esos datos.

Además, para darle algo más de vidilla vamos a usar TailwindCSS para estilizar nuestros componentes y que quede agradable a la vista. No me enrollo más, os dejo con las versiones que estoy utilizando:

Versiones

  • TailwindCSS 2.2.15
  • React 17.0.7
  • Drupal 9.2
  • Yarn 1.22.10

Creación aplicación React

Vamos a proceder a crear nuestra app de React con el comando

yarn create react-app my-app

Instalación de TailwindCSS

La documentación oficial de TailwindCSS ya nos proporciona el método para instalarlo en nuestra aplicación de React. La peculiaridad es que también necesitamos instalar CRACO (Create React App Configuration Override) para poder sobreescribir la configuración de PostCSS. Ejecutamos estos comandos:

yarn add -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 yarn add @craco/craco

Modificamos la sección "scripts" del package.json y la dejamos así para que la apliación la procese craco en lugar de react-scripts.

"scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "react-scripts eject"
  },

 

Creamos nuestro archivo de configuración para craco en la raíz del proyecto, en el que cargaremos las dependencias de PostCSS. Debe quedarnos así:

// craco.config.js
module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
}

 

Ahora creamos la configuración de TailwindCSS. Para ello podemos lanzar un comando o crear manualmente el archivo tailwind.config.js en la raíz del proyecto.

Comando:

npx tailwindcss-cli@latest init

Código

// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Por último sólo nos queda añadir las directivas de tailwind al css.

/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Con esto ya podremos utilizar todas las clases de TailwindCSS en nuestra app.

Problema con el CORS

Aquí viene el primer problema. Cuando intentemos acceder a nuestra api desde otra url que no sea la misma donde está la api alojada tendremos un problema de CORS (Cross-Origin Resource Sharing), hablando claro, que todas las peticiones que vengan desde otro origen se bloquearán. Esto es una medida de seguridad para evitar ataques no deseados.

Para evitar esto, tendremos que añadir en la configuración de Drupal los hosts que vamos a permitir que accedan a nuestro contenido, las peticiones que pueden realizar, etc. Vamos a proceder a la configuración. Nos dirigimos a <raíz>/web/sites/default y copiamos default.services.yml y lo renombramos a services.yml. Ahora vamos a la sección donde se configura el cors y lo dejamos así:

   # Configure Cross-Site HTTP requests (CORS).
   # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
   # for more information about the topic in general.
   # Note: By default the configuration is disabled.
  cors.config:
    enabled: true
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: ['x-csrf-token','authorization','content-type','accept','origin','x-requested-with']
    # Specify allowed request methods, specify ['*'] to allow all possible ones.
    allowedMethods: ['*']
    # Configure requests allowed from specific origins.
    allowedOrigins: ['http://localhost:3000']
    # Sets the Access-Control-Expose-Headers header.
    exposedHeaders: true
    # Sets the Access-Control-Max-Age header.
    maxAge: 1000
    # Sets the Access-Control-Allow-Credentials header.
    supportsCredentials: true

 

En el parámetro "allowedOrigins" debéis indicar los hosts que queréris permitir. En mi caso es el localhost:3000.

Componentes de React

Vamos a crear un par de componentes que nos servirán para mostrar nuestros artículos. En el componente padre vamos a hacer una función en la que llamaremos a nuestra API y con el uso de los hooks useState y useEffect almacenaremos es información para más tarde ir recorriendo cada uno de los registros e ir pintado cada uno de los artículos.

ListItems

import React, { useEffect, useState } from 'react'
import { ListItem } from './ListItem';
export const ListItems = () => {
    const [items, setItems] = useState([]);
    const username = 'admin';
    const password = 'Pass@12345';
    const credrentials = `${username}:${password}`;
    const getItems = async () => {
        const url = 'https://drupal-api-test.ddev.site/api/v1/articles';
        const resp = await fetch(url, {
            mode: 'cors',
            headers: {
                'Authorization': 'Basic ' + Buffer.from(credrentials).toString('base64'),
                'Accept': '*/*'
            }
        });
        const data = await resp.json();
        setItems(data);
    }
    useEffect(() => {
        getItems();
    }, [])
    
    return (
        <div className="grid grid-cols-3 gap-6 max-w-screen-2xl mx-auto mt-8">
           {
               items.map(item => <ListItem key={item.nid} {...item} />)
           }
        </div>
    )
}

 

Aquí estamos haciendo uso de la API nativa fetch para realizar la petición hasta nuestro drupal. A ésta le pasaremos por los headers nuestros datos de autenticación codificados en base64. Lo ideal sería tener estos credenciales en un archivo de entorno pero para el ejemplo no quería complicarlo más.

AVISO: Es muy importante que en el hook useEffect dejemos el segundo parámetro como un array vacío ya que de lo contrario estaremos llamando a getItems() indefinidamente.

ListItem

import React from 'react'
export const ListItem = ({title, body, field_image, field_tags}) => {
    let imageUrl = `https://drupal-api-test.ddev.site/${field_image}`
    return (
        <div className="shadow-2xl bg-green-300 rounded-2xl overflow-hidden">
            <img src={imageUrl} alt="Imagen" className="h-48 w-full object-cover object-center"></img>
            <div className="p-6">
                <div className="flex flex-row gap-3 mb-3">
                    <span className="p-1 px-3 bg-indigo-400 rounded-2xl font-bold">Categoría 1</span>
                    <span className="p-1 px-3 bg-green-600 rounded-2xl text-white font-bold">Categoría 1</span>
                </div>
                
                <h2 className="text-xl font-bold">{title}</h2>
                <div dangerouslySetInnerHTML={{__html: body}}></div>
                
            </div>
        </div>
    )
}

Si queremos mantener nuestra estructura del WYSWYG de nuestro contenidos en drupal deberemos añadir el atributo dangerouslySetInnerHTML con el parámetro __html y la referencia al campo que queramos que no se escapen los caracteres html. De esta forma conservamos todo el formato de nuestro campo.

Por último, agregamos nuesto ListItems a nuestro App.js.

App.js

import './App.css';
import { ListItems } from './components/ListItems';
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <ListItems></ListItems>
      </header>
    </div>
  );
}
export default App;

¡Listo! Ya tenemos nuestro listado de artículos.