Descubriendo Alpine.js, el framework liviano alternativa a jQuery

alpinejs.jpg
Solucionex
12
Nov 20

Alpine.js es una librería javascript creada por Caleb Porzio, también creador del componente Livewire para Laravel. Está inspirada en otros frameworks como AngularJS, VueJS o TailwindCSS que nos permite enriquecer nuestro lenguaje HTML con propiedades declarativas y reactivas de una manera fácil, rápida y ligera, como alternativa a frameworks como React.js y Vue.js que con su crecimiento empiezan a requerir de gestores de tareas para facilitarnos su manejo.

 

¿Qué es eso de propiedades declarativas y reactivas?

Cuando hablamos de declarativa y reactiva nos referimos a paradigmas de programación, en otras palabras, una manera de clasificar diversos enfoques o planteamientos de cómo construir nuestro código. 

Resumiendo, diremos que existen dos grandes familias dentro de esta clasificación: la programación imperativa y la programación declarativa. Su principal diferencia es que en la primera el programador tiene que definir el cálculo por el cual llegar a un resultado, mientras que en la última el programador define las propiedades que va a tener el resultado pero no define el cálculo.

Dentro de la familia de la programación declarativa nos encontramos otro paradigma, el de la programación reactiva. Este paradigma se define como aquel que nos permite trabajar sobre un flujo de datos, finitos o infinitos, de forma asíncrona, detectar cambios y definir una reacción a estos cambios.

Esta propiedad reactiva está muy presente en todos los nuevos frameworks javascript con los que trabajamos, dándonos la oportunidad gracias a la reactividad de construir interfaces de usuario más avanzadas, de manera más rápida y simple.

 

¿Qué puedo hacer con Alpine.js?

Para utilizar Alpine, es tan simple como agregar al final de nuestro <head> el enlace a su CDN:

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.3/dist/alpine.min.js" defer></script>

Ahora ya podemos empezar a desarrollar nuestra aplicación web, pero antes un poco de teoría:

Alpine nos ofrece 14 directivas y 6 propiedades mágicas.

Por directivas vamos a entender que son una serie de funciones que podemos agregar a nuestro código HTML para hacerlo "más inteligente" y nos permita emplear esta propiedad reactiva en nuestra aplicación. Nosotros únicamente vamos a utilizar las siguientes:

  • x-data: Nos permite declarar un nuevo scope (espacio de trabajo) del componente

  • x-show: Muestra, o no, un elemento dependiendo de un resultado booleano.

  • x-bind: Asigna el valor de un atributo a partir del resultado de una expresión javascript.

  • x-model: Mantiene la entrada del elemento sincronizado con los datos del componente.

  • x-on: Adjunta un evento a un elemento y ejecuta una expresión javascript cuando se emite el evento.

  • x-text: Actualiza el texto que contiene un elemento.

  • x-for: Permite crear tantos nodos en el DOM como elementos contenga un array dado.

Nuestra aplicación de ejemplo va a consistir en una planilla de horas que nos permita llevar el control del tiempo dedicado a cada tarea en la que hemos estado trabajando durante la jornada laboral.

Para construir esta aplicación vamos a hacer uso de Alpine.js, por supuesto, pero también de TailwindCSS como framework CSS. Para integrar todo lo anterior, solamente vamos a hacer uso de un archivo HTML, nuestro index.html, sin gestores de paquetes ni dependencias.

Si lo prefieres puedes descargarte y analizar el código en mi repositorio, en este post solamente remarcaré los fragmentos de código más importantes.

En el <head> añadimos los enlaces CDN relativos a TailwindCSS y Alpine.js.

<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.3/dist/alpine.min.js" defer></script>

Y al final de nuestro <body> crearemos nuestro <script> con todo el código que necesitaremos para implementar la lógica con Alpine.js.

En nuestro <body> añadimos la directiva x-data, que definirá el scope de nuestro componente Alpine, en este caso utilizaremos un único componente para el total de la aplicación y la directiva x-init para inicializar nuestra aplicación antes del montaje del componente.

<body class="bg-white font-sans flex flex-col h-100 min-h-sreen" x-data="app()" x-init="init()">

En nuestro <script> vamos a definir lo siguiente:

  • El componente app().
  • El objeto newEntry que nos servirá de modelo para crear una nueva entrada.
  • El array de objetos data que almacenará todas las entradas.
  • La función init() para inicializar ciertas variables antes de lanzarse el componente.
    <script>
        function app(){
            return {
                newEntry: {
                    'from': '',
                    'to': '',
                    'subject': '',
                },
                data: [],
                init() {
                    if(!this.data.length > 0){
                        let now = new Date()
                        this.newEntry.from = this.formatTime(now,15)
                        now.setHours(now.getHours() + 1)
                        this.newEntry.to = this.formatTime(now,15)
                    }else{
                        let entries = this.data[0].entries
                        this.newEntry.from = entries[entries.length-1].to
                        this.newEntry.to = this.formatTime(now, 15)
                    }
                },
                (...)
            }
        }
    </script>

Volvemos al markup y añadimos tres elementos inputs. Cada uno de los inputs tendrán la directiva x-model que los enlazará con una variable, en este caso el objeto newEntry. Además añadimos dos directivas de eventos de teclado y ratón, @keydown.enter y @click.prevent, esto es, cada vez que presionamos sobre la tecla INTRO o hagamos click sobre el botón Añadir, llamaremos a la función add() que se encargará de actualizar el array data con nuestra nueva entrada newEntry.

Nuestro markup se verá tal que así:

<div class="py-8 flex container mx-auto items-end" @keydown.enter="add()">
    <div class="input-group flex flex-col">
        <label for="from" class="uppercase font-bold text-gray-800 text-xs">Desde</label>
        <input  x-model="newEntry.from" required class="p-2 bg-gray-200 text-gray-800 focus:outline-none text-xl appearance-none h-12" type="time" name="from" id="from" step="900">
    </div>
    <div class="input-group flex flex-col">
        <label for="to" class="uppercase font-bold text-gray-800 text-xs">Hasta</label>
        <input  x-model="newEntry.to" required class="p-2 bg-gray-200 text-gray-800 focus:outline-none text-xl appearance-none h-12" type="time" name="to" id="to" step="900">
    </div>
    <div class="input-group flex flex-col flex-auto">
        <label for="to" class="uppercase font-bold text-gray-800 text-xs">Tarea</label>
        <input x-model="newEntry.subject" required class="p-2 bg-gray-200 text-gray-800 flex-auto focus:outline-none text-xl h-12" type="text" name="subject" id="subject" placeholder="Introduce una tarea">
    </div>
    <button @click.prevent="add()" type="submit" class="bg-red-500 hover:bg-red-400 text-white p-2 focus:outline-none h-12">Añadir</button>
</div>

Mientras que la función add():

add(){
    let now = new Date()
    let year = now.getFullYear()
    let month = (now.getMonth() + 1).toString().padStart(2,'0')
    let day = now.getDate().toString().padStart(2,'0')
    let date = year+'-'+month+'-'+day
    if(this.data.length > 0 && this.data[0].date == date){
        let newEntry = {
            'from': this.newEntry.from,
            'to': this.newEntry.to,
            'subject': this.newEntry.subject
        }
        this.data[0].entries.unshift(newEntry)
    }else{
        let timesheet = {
            'date': date,
            'entries': [
                {
                    'from': this.newEntry.from,
                    'to': this.newEntry.to,
                    'subject': this.newEntry.subject
                }
            ]
        }
        this.data.unshift(timesheet)
    }
    this.newEntry.from = this.newEntry.to
    this.newEntry.top = this.formatTime(new Date(),15)
    this.newEntry.subject = ''
    return 0
},

Ahora toca mostrar las entradas que vamos guardando en nuestro array data. Para ello añadimos la etiqueta <template> que es la que soportará la directiva x-for. Vamos a utilizar dos bucles, uno para recorrer las hojas de cada día y otra para pintar las entradas de cada hoja diaria. En el caso que el array data venga vacío, utilizaremos la directiva x-show para mostrar un mensaje diciendo que no hay entradas registradas. Veamos el markup:

<template x-show="data.length > 0" x-for="timesheet in data" :key="timesheet">
    <section class="container mx-auto py-8">
        <h2 x-text="timesheet.date" class="font-bold text-gray-700 text-xl"></h2>
        <table class="my-5">
        <template x-for="entry in timesheet.entries">
            <tr>
                <td class="p-2 text-xl"><input class="w-6 h-6 align-middle" type="checkbox" name="" id=""></td>
                <td class="p-2 text-xl"><span x-text="entry.from"></span></td>
                <td class="p-2 text-xl"><span x-text="entry.to"></span></td>
                <td class="p-2 text-xl"><span x-text="entry.subject"></span></td>
            </tr>    
        </template>
        </table>
    </section>
</template>
<section x-show="data.length == 0" class="container mx-auto py-8 text-center">
    Actualmente no hay ningún registro almacenado
</section>

Y con esto, a falta de persistencia, tenemos una aplicación súper simple que nos permite gestionar las tareas que vamos completando a lo largo de nuestra jornada laboral.

 

Echamos un vistazo al ecosistema de Alpine.js

Si Alpine.js se nos queda corto, existen una serie de herramientas que nos permiten extender alguna de sus funcionalidades:

Para estar informado de las novedades entorno a esta librería/mini-framework no viene mal no dejar de consultar Awesome Alpine.

 

¿Es Alpine.js una alternativa a jQuery? El enfoque Utility-first

Muchos son los frameworks y librerías javascript, o el propio javascript nativo, los que se postulan como alternativa al veterano y últimamente denostado jQuery, pero ¿Realmente existe un rival tan completo que cubra todo lo que nos aportaba y aporta la API de jQuery?

Hasta el momento, en cuanto a recorrer el DOM el propio javascript nativo, o vainilla, ya implementa muchas funcionalidades como el document.querySelector(). De la misma manera para el uso de peticiones y respuestas a APIs, ya tenemos la Fetch API. Tenéis más ejemplos de conversiones de código en esta fantástica Cheat sheet for moving from jQuery to Vanilla Javascript escrita por Tobias Ahlin.

¿Pero qué aporta Alpine.js? El plus que nos da tanto Alpine como otras librerías de carácter reactivo, es que con sus directivas nos permite automatizar y ahorrar mucho código sobre elementos del DOM que antes teníamos que definir desde javascript con jQuery y ahora lo hacemos directamente en nuestro markup. Otro ejemplo, es el de las animaciones CSS que gracias a sus directivas x-show.transition y x-transition, nos permite definirlas y aplicarlas desde el propio HTML.

Otra alternativa es hacer uso de las clases Transition y Animation de TailwindCSS, que ya nos provee de transiciones y animaciones CSS predefinidas que podemos utilizar como clases en nuestros elementos del DOM.

Pese a que es un hecho que jQuery ya puede reemplazarse tanto con código javascript nativo como con nuevas librerías y frameworks, me gustaría matizar que más que hablar de X framework como alternativa a jQuery, hablaría del concepto Utility-first tanto en CSS como en JS y lo que esto nos aporta a la hora de crear un markup enriquecido.

En resumidas cuentas, podemos definir el concepto Utility-first como un enfoque en el que pasamos de definir CSS y JS orientado a un elemento del DOM, a definir ese elemento del DOM con pequeñas clases CSS o funciones JS de bajo nivel. Os pongo un ejemplo:

Hasta ahora, lo normal era definir un elemento del DOM o una clase, y darle estilos desde nuestro CSS:

<h1 class="section-title">No usamos utility-first</h1>

h1.section-title{
    font-size: 32px;
    color: red;
    font-family: sans-serif;
}

El enfoque Utility-first nos permite reutilizar clases CSS de bajo nivel para dar estilos directamente desde markup:

<h1 class="text-2xl text-red-500 font-sans">Usando utility-first</h1>

.text-2xl{
    font-size: 32px;
}

.text-red-500{
    color:red;
}

font-sans{
    font-family: sans-serif;
}

¿Y qué nos aporta este enfoque? Rapidez, mucha rapidez.

 

Prototipos de alta calidad con Alpine.js

La existencia de una herramienta como Alpine.js que nos permite crear interfaces con interacciones reales de una manera muy rápida y simple, junto al enfoque Utility-first que ha vuelto a poner TailwindCSS sobre la mesa de los frameworks CSS, hacen que el desarrollo de pequeñas aplicaciones se complete en mucho menos tiempo, teniendo como resultado una manera de afrontar el código de nuestras interfaces de usuario realmente ágil.

Es en este enfoque de agilidad en el desarrollo donde creo que se le puede sacar mucho partido a este tipo de mini-frameworks, sobre todo de cara a startups o proyectos en los que el cliente está presente desde el minuto cero y en los que se necesita de un prototipo más "palpable" que el generado en una presentación Powerpoint o en un prototipo generado con herramientas de diseño como Adobe Illustrator, Sketch, Figma, Invision, etc.

El hecho de generar prototipos basados en código nos aporta:

  • Ahorra la necesidad de tener en plantilla un perfil únicamente de diseño en pequeñas organizaciones, dando vía libre a la existencia de nuevos perfiles del sector que aúnen diseño y desarrollo, por ejemplo, los devsigners.
  • Nos ahorramos la fase de transición entre la propuesta de diseño y maquetación, asegurando que el resultado cumplirá con los requisitos de diseño y dejando atrás puntos críticos del tipo "en diseño queda muy bonito, pero pasarlo a CSS es un calvario".
  • Ofrecemos un producto mínimo viable de fase 0 a nuestro cliente con el que puede hacerse una idea "real" del resultado final, dándole libertad de navegación, experiencia de usuario y percepción de la interfaz.
  • Generamos código válido que puede reutilizarse y que en el peor de los casos servirá de semilla a nuestro equipo de desarrollo frontend.

Lo bueno de estas librerías, minis, micros o mini-frameworks, es que a cada uno nos aporta un nuevo enfoque, una nueva manera de descubrir puntos críticos que hasta el momento no se habían detectado en nuestras organizaciones y ponerles una solución.

Y vosotros ¿Por qué le daríais una oportunidad a Alpine.js?

Más sobre Alpine.js

Si estás interesado en Alpine.js y en su ecosistema, te dejo los siguientes enlaces: