Utiliza este crédito gratuito para lanzar tus proyectos ahora en Digital Ocean, eres libre de gastarlo cuando quieras en los siguientes 60 días.
Índice del contenido
Tutorial de Django y HTMX, web apps modernas sin escribir JS
Tutorial de Django y HTMX, web apps modernas sin escribir JS
El otro día estaba probando una librería llamada htmx, que promete volver mucho más sencilla la creación de una web, sí, otra librería, pero con la diferencia de que esta no necesita que escribas Javascript (JS) y que, además, combina bastante bien con Django. Htmx basa todo su funcionamiento en escribir atributos en tus etiquetas HTML, que se encargan de abstraer todo el JS que corre tras bambalinas. El resultado es código formado únicamente por etiquetas HTML y sus atributos, sin código JS (bueno, quizá solo un poco). No, no tienes que abandonar todo el JS, tranquilo, también puedes combinar htmx con tus librerías favoritas y código vanilla JS.
¿Y cuantos kB le va añadir a mi proyecto? Casi nada, HTMX es bastante ligero, pesa alrededor de 10 kB gzipped o 30 kB minimizado y no tiene dependencias.
Htmx te permite manejar peticiones AJAX, transiciones de CSS, websockets y eventos mandados desde el servidor sobre cualquier tag de HTML que acepte atributos . Todas estas funcionalidades pueden ser desencadenadas por una serie triggers tales como que un elemento cargue, que aparezca en el viewport, un click, que el mouse entre (o salga), o incluso eventos que se activan cada cierto tiempo sin interacción del usuario. Además de peticiones GET y POST, HTMX permite hacer peticiones PUT, DELETE, PATCH, todo modificando atributos de etiquetas HTML únicamente.
Htmx no genera HTML, sino que delega esa tarea al servidor, por lo que en lugar de tener una endpoint con respuestas en JSON trabajaremos con endpoints que generarán directamente el código HTML y lo enviarán como respuesta, como si fuera SSR. Eso lo vuelve perfecto para combinarse con el sistema de plantillas que viene integrado en Django.
Pero supongo que quieres ver como luce el código ¿no? Mira este ejemplo tomado de la documentación:
<button hx-post="/clicked"
hx-trigger="click"
hx-target="#parent-div"
hx-swap="outerHTML">
Click Me!
</button>
Cuando un usuario haga click en este botón, realiza una petición HTTP POST a la url /clicked y usa el contenido de la respuesta para reemplazar al elemento con el id parent-div en el DOM
Esquema del funcionamiento de HTMX
Preparación del proyecto de Django y htmx
Para este tutorial he creado un repositorio en github, por lo que si quieres ahorrarte todo el boilerplate, puedes simplemente clonar todo el proyecto e ir siguiendo el código conforme lo voy escribiendo.
Recuerda que si no estás convencido de porque deberías usar Django revisa mi entrada donde explico las ventajas y desventajas de Django . Si no tienes idea de como se usa Django mejor empieza con la guia de Django
# Ejecuta esto para no escribir el código
git clone https://github.com/EduardoZepeda/djangohtmxtest.git
cd djangohtmxtest/
pip install
python manage.py migrate
python loaddata videogames
# Datos de inicio de sesión:
user: admin
password: contrasenaNoSegura
Instalación de modelos y dependencias de Django
Para empezar, vamos a crear un entorno virtual con el gestor de entornos virtuales pipenv . Tú puedes usar pip , poetry, conda o el que prefieras.
pip install django
Creemos un proyecto y una app con django-admin
django-admin startproject djangoHtmx
django-admin startapp videogameStore
Entra en la carpeta videogameStore y creemos los modelos que usaremos
from django.db import models
# Create your models here.
GENRES = (
("HOR","Horror"),
("RPG", "RPG"),
("ADV", "Adventure")
)
class Videogame(models.Model):
title = models.CharField(max_length=255)
description = models.TextField(max_length=255)
genre = models.CharField(choices=GENRES, max_length=3)
price = models.DecimalField(max_digits=20, decimal_places=2)
Ahora corramos las migraciones y creemos un súper usuario.
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
Dentro de la carpeta djangoHtmx modificaremos el archivo urls.py.
Vamos a crear las urls para la ruta home/ y posteriormente crearemos urls para nuestra app bajo la ruta videogame/
from django.contrib import admin
from django.urls import path, include
from .views import home
from videogameStore.urls import urlpatterns as videogameUrls
urlpatterns = [
path('', home, name="home"),
path('admin/', admin.site.urls),
path('videogame/', include(videogameUrls)),
]
Ahora creamos una carpeta templates en la raíz del proyecto y vamos a crear una plantilla base que llamaremos base.html y otra plantilla, que heredará de esta última, llamada home.html, la cual usaremos en la vista home.
mkdir templates
cd templates
touch base.html
touch home.html
Instalando htmx
Abrimos el archivo base.html y colocamos el siguiente código. Observa como para usar htmx basta con cargarlo desde unpkg usando una etiqueta script. En este tutorial estamos usando la versión 1.4.1 de htmx
Mantendremos una estructura muy simple para la plantilla base, con solo las etiquetas header, body y footer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ site.name }}{% endblock %}</title>
{% block meta %}
<meta name="theme-color" content="#333333">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="{{ site.name }}">
<meta property="og:type" content="website">
{% endblock meta %}
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
{% block header %}{% endblock header %}
{% block body %}{% endblock body %}
{% block footer %}{% endblock footer %}
</body>
</html>
Atributos más útiles de HTMX para Django
Abramos el archivo home.html. Extendemos de la plantilla base.html y reemplazamos la etiqueta body con una presentación simple y nuestro primer botón con Htmx.
{% extends "base.html" %}
{% block body %}
<div id="videogame-list">
<h1>Welcome senpai!</h1>
<p>Check out our summer discounts (✿◠‿◠)</p>
<button hx-get={% url "videogameList" %}
hx-trigger="click"
hx-target="#videogame-list"
hx-push-url="true"
hx-swap="outerHTML">
Go to discounts
</button>
</div>
{% endblock body %}
El botón va a tener cuatro atributos, cada botón le va a especificar una serie de comportamientos: hx-get, hx-trigger, hx-target, hx-push-url
hx-get
Aquí colocaremos la url a la que haremos la petición. Observa como incluso podemos generarla de manera dinámica usando la etiqueta {% url %} que provee django.
hx-get es uno de los atributos que se encargan de realizar peticiones AJAX, los otros son hx-post, hx-put, hx-patch y hx-delete, que realizan peticiones POST, PUT, PATCH y DELETE, respectivamente.
hx-trigger
Será la clase de evento que desencadenará la petición, un click en este caso. Sin embargo podemos usar otros eventos tales como mouseenter, mouseleave, keyup, etc.
Hay una serie de eventos especiales disponibles también:
- load: se activa cuando carga un elemento
- revealed: cuando un elemento se muestra en el viewport
- intersect: se desencadena cuando un elemento intersecta con el viewport
- every ns: cada cierto tiempo (ej. every 2s, every 10s)
Si no especificamos un evento la librería tomará ciertos eventos de manera predeterminada para realizar la petición AJAX.
- Los campos de formulario desencadenarán la petición con el evento change
- Los formularios con el evento submit
- El resto de elementos con un click
Estos eventos cuentan con modificadores que retardan un evento o impiden que se ejecute más de una vez.
- changed: realiza la petición solo si el elemento cambió
- delay: retrasa la ejecución de la petición
- throttle: igual que delay, pero rechaza nuevos eventos si no ha pasado el tiempo especificado
- from: permite escuchar el evento desde otro elemento. Recibe un selector CSS (Ej. #id)
hx-target
Indica el elemento en donde queremos cargar la respuesta que obtendremos a la dirección que escribimos en hx-get, si no especificamos ninguno se usará el que hizo la petición. En el ejemplo se ha especificado la id del elemento padre. Si se omite este valor se reemplazará el elemento que hizo la petición, es decir, el que posee el atributo hx-get.
hx-swap
Indica el elemento donde se colocará el resultado de la petición. Tenemos varias opciones:
- outerHTML: sustituyendo a la etiqueta
- innerHTML: en el interior de la etiqueta
- afterbegin: antes del primer hijo de la etiqueta
- beforebegin: antes de la etiqueta especificada
- beforeend: después del último hijo de la etiqueta
- afterend: después de la etiqueta
- none: en ningún lugar
De manera predeterminada es innerHTML
hx-push-url
Le indica a htmx que debe reemplazar la url del navegador por la url que especificamos en hx-get.
hx-boost
Si agregamos este atributo a la etiqueta body, todos los enlaces (anchors) que se encuentren en el interior van a tratarse como si se tratara de una SPA, se hará la petición y se reemplazará el body actual por el de la respuesta. Otorgando esa sensación de transición suave, como si estuvieras usando la API de Javascript para cambiar las urls.
¿Pero y la etiqueta header? Pues ciertamente alguien tuvo el mismo problema y se desarrolló una extensión para el header también, puedes verla el enlace en el repositorio de github de las extensiones de HTMX
Generación de HTML para htmx
El HTML que usaremos será exactamente el mismo que generaría Django en una petición normal usando su sistema de plantillas, ya sea usando el método render o con vistas genéricas.
Definamos primero nuestras vistas en la app videogameStore. Creemos el archivo urls.py y, dado que usaremos vistas genéricas, colocamos las nombres y llamamos a su método as_view()
from django.urls import path
from .views import ListVideogames, VideogameDetail
urlpatterns = [
path('list/', ListVideogames.as_view(), name="videogameList"),
path('<int:pk>/', VideogameDetail.as_view(), name="videogameDetail"),
]
Hecho esto, vamos a crear las vistas. La vista de ListView se encargará de devolver los objetos del modelo Videogame. context_object_name se encargará de asignarle el nombre que usaremos para acceder a este modelo en las plantilla listVideogames.html, o sea “videogames”.
DetailView nos permite mostrar un solo objeto y usaremos la variable “videogame” para acceder al objeto en la plantilla videogameDetail.html.
from django.shortcuts import render
from django.views.generic import ListView, DetailView
from .models import Videogame
class ListVideogames(ListView):
model = Videogame
template_name = "listVideogames.html"
context_object_name = "videogames"
class VideogameDetail(DetailView):
model = Videogame
template_name = "videogameDetail.html"
context_object_name = "videogame"
Ahora crearemos las plantillas que necesitamos para que nuestras vistas funcionen
Primero videogameDetail.html
<div id="videogame-detail">
<h2>{{videogame.title}}</h2>
<p>{{videogame.description}}</p>
<small>{{videogame.price}}</small>
<button hx-get={% url "videogameList" %}
hx-trigger="click"
hx-target="#videogame-detail"
hx-swap="outerHTML"
hx-push-url="true">
Return to list
</button>
</div>
Después listVideogames.html
<div id="videogame-list">
{% for videogame in videogames %}
<a
hx-get={% url "videogameDetail" videogame.pk %}
hx-trigger="click"
hx-target="#videogame-list"
hx-swap="outerHTML"
hx-push-url="true">
<h2>{{videogame.title}}</h2>
<p>{{videogame.description|truncatewords:15}}</p>
<button>Read more</button>
</a>
{% endfor %}
</div>
Observa como, en ambos casos, el HTML generado por Django en la url a la que apunta hx-target reemplazará el div con el id videogame-list tras haberse efectuado un click sobre el elemento.
Proceso de interacción entre django y htmx
Si nuestro código funciona, tras hacer click en el botón de home se realizarán los siguientes pasos:
- Htmx realizará una petición GET a la url videogame/list/
- Django consultará la base de datos, obtendrá la información y la pasará al sistema de renderizado de plantillas
- El sistema de plantillas de Django renderizará el template listVideogames.html y lo retornará como respuesta
- Htmx tomará el contenido de la respuesta y reemplazará la etiqueta que tiene el id #videogame-list con esta
- Htmx modificará la url del navegador para que apunte a videogame/list/
Mandando parámetros con Htmx
Cualquier elemento que provoque una petición va a incluir su valor (inputs). Si este elemento es un formulario, htmx incluirá el contenido de todos los inputs que se encuentren dentro de las dos etiquetas form. Vamos a crear un formulario para probar esto.
Primero agreguemos una vista para crear un videojuego en nuestro archivo views.py
from django.views.generic import ListView, DetailView, CreateView
class VideogameCreate(CreateView):
model = Videogame
fields = ["title", "description", "genre", "price"]
template_name = "createVideogameForm.html"
Recuerda dotar del método get_absolute_url a nuestro modelo Videogame. Ya que si nuestra petición es exitosa, querremos que django nos redirija al nuevo objeto creado.
class Videogame(models.Model):
# ...
def get_absolute_url(self):
return reverse('videogameDetail', args=[str(self.id)])
Y coloquemos la url en nuestro archivo de urls.py
from .views import ListVideogames, VideogameDetail, VideogameCreate
url_patterns = [
# ...
path('create/', VideogameCreate.as_view(), name="videogameCreate"),
]
Entremos a la carpeta templates y creemos la plantilla createVideogameForm.html que especificamos en nuestra vista genérica.
Asi mismo, nota como he eliminado el atributo method. Si nuestra petición es correcta, el videojuego se agregará y nos redirigirá a la url que especificamos en el método get_absolute_url que creamos más arriba.
<div id="videogame-list">
<form
hx-post={% url "videogameCreate" %}
hx-target="#videogame-list"
hx-swap="outerHTML">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save">
</form>
</div>
Colocaré un botón en la lista de videojuegos, listVideogames.html para que nos redirija a la vista de creación de páginas.
Esto es importante porque si accedemos a una url diferente a la de home, que es donde se carga el script de htmx, no tendremos acceso a las funcionalidades.
Solucionaremos este problema más adelante.
{% endfor %}
<a
hx-get={% url "videogameCreate" %}
hx-trigger="click"
hx-target="#videogame-list"
hx-swap="outerHTML"
hx-push-url="true">
<button>Submit a videogame</button>
</a>
</div>
Mira como queda funcionando
Mira la imagen de abajo, htmx envió de manera automática el contenido de todos los campos que englobaba la etiqueta form como parte de la petición POST hecha a /videogame/create/
Excluyendo parámetros con htmx
Es posible excluir parámetros por medio del atributo hx-params. Para usarlo simplemente lo colocamos en la etiqueta form o la que estemos usando.
hx-params recibe las siguientes posibles opciones
- *: para incluir todos
- none: para no incluir parámetros
- not : para excluir una lista se parámetros separados entre comas
- : para incluir solo los parámetros que aparecen en esta lista separada por comas
{% endfor %}
<a
hx-get={% url "videogameCreate" %}
hx-trigger="click"
hx-target="#videogame-list"
hx-swap="outerHTML"
hx-push-url="true">
hx-form="*"
<button>Submit a videogame</button>
</a>
</div>
Encabezados especiales que manda htmx con cada petición
Hay otra cosa que sucedió y no te dije, cuando enviamos el formulario; htmx agregó algunos encabezados a nuestra petición.
Cada que se realiza una petición con Htmx, se envían encabezados extras al servidor. Basta con que te metas a las herramientas de desarrollador para que veas lo que enviamos en la última petición.
Se envió la url por medio del encabezado HX-Current-URL, el elemento objeto que se intercambiará por medio HX-Target, y un parámetro llamado HX-Request, que es siempre igual “true”, que le indica al servidor que hicimos la petición con htmx. HX-Trigger, se envía si especificamos una id en la etiqueta que desencadena la petición. ¿Y para que me sirven estos encabezados? Pues puedes recuperarlos con django y usarlos como mejor te convenga en tus vistas.
Renderizando head, body y html de manera condicional con HTMX
¿Recuerdas que teníamos el problema de que si accedíamos directamente a las url, sin pasar por home, no se cargaba htmx? Pues ahora que sabemos que tenemos estos encabezados, podemos usarlos para que el sistema de plantillas incluya la etiqueta head, html y body solamente cuando accedamos directamente a la ruta.
{% if not request.META.HTTP_HX_REQUEST %}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ site.name }}{% endblock %}</title>
{% block meta %}
<meta name="generator" content="Htmx">
<meta name="theme-color" content="#333333">
<meta name="msapplication-TileColor" content="#BBBBBB">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="{{ site.name }}">
<meta property="og:type" content="website">
{% endblock meta %}
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
{% endif %}
{% block header %}{% endblock header %}
{% block body %}{% endblock body %}
{% block footer %}{% endblock footer %}
{% if not request.META.HTTP_HX_REQUEST %}
</body>
</html>
{% endif %}
Y ahora en todas nuestras plantillas que solo retornan HTML, podemos hacer que extiendan de base.html y colocar el contenido dentro de la etiqueta {% body %}
{% extends "base.html" %}
{% block body %}
<div id="videogame-list">
{% for videogame in videogames %}
<a
hx-get={% url "videogameDetail" videogame.pk %}
hx-trigger="click"
hx-target="#videogame-list"
hx-swap="outerHTML"
hx-push-url="true">
<h2>{{videogame.title}}</h2>
<p>{{videogame.description|truncatewords:15}}</p>
<button>Read more</button>
</a>
{% endfor %}
<a
hx-get={% url "videogameCreate" %}
hx-trigger="click"
hx-target="#videogame-list"
hx-swap="outerHTML"
hx-push-url="true">
<button>Submit a videogame</button>
</a>
</div>
{% endblock body %}
Ahora puedes acceder directo a las url y conservar la funcionalidad de htmx.
Renderizando condicional con HTMX en las vistas de Django
Lo anterior puede ser bastante complicado si tus plantillas son complejas, pero hay otras opciones. Por ejemplo puedes generar tu nombre de plantilla dinámicamente si has recibido una petición originada con HTMX, ¿te acuerdas que te dije de los headers o cabeceras especiales?
class YourGenericView(ListView):
def get_template_names(self):
if self.request.META.get("HTTP_HX_REQUEST"):
return ["partials/_template.html"]
return [self.template_name]
Te he mostrado solo lo básico de htmx combinado con django recuerda visitar la documentación oficial para ver el resto de cosas que tiene para ofrecer, como CSS transitions, websockets y SSE.