Proseguiamo il nostro discorso su Astro e htmx. Nella lezione precedente abbiamo installato il tema predefinito di Astro. In questa lezione utilizzeremo gli stessi componenti, modificando il codice presente e aggiungendo il nostro.
Per prima cosa dovremo realizzare un form che permetta all'utente di creare nuovi task. Il form conterrà un elenco di pulsanti radio per le categorie e un campo di testo per la descrizione del task.

Un modulo per la trasmissione di richieste HTTP potenziato da htmx
Il modulo per la trasmissione dei dati
Apriamo il file /src/pages/index.astro e modifichiamo il frontmatter come segue:
---
import { db, Categories } from 'astro:db';
import Layout from '../layouts/Layout.astro';
const categories = await db.select().from(Categories);
---
- abbiamo importato il database e la tabella
Categoriesdaastro:db; - abbiamo importato il componente
Layout; - infine, il metodo
db.select()restituisce l'array completo di tutti i record della tabellaCategories.
Passiamo al markup. Per prima cosa, possiamo cambiare il titolo della pagina. Qui abbiamo solo eseguito una semplice modifica:
<h1>Welcome to <span class="text-gradient">Astro</span> & <span class="text-gradient">htmx</span></h1>
Eliminiamo, poi, gli elementi p e ul presenti e aggiungiamo il nostro codice all'interno di una div:
<div>
<h2>
Todos
<span>→</span>
</h2>
<form
id="addtask"
hx-post="/add-item"
hx-target="#todo-list"
hx-swap="beforeend"
hx-on::after-request="addtask.reset()"
>
<div>
<p>Select category</p>
{
categories.map(({ id, category }) => (
<input type="radio" id={category} name="category" value={id} />
<label for={category}>{category}</label><br />
))
}
</div>
<div>
<input type="text" placeholder="Add item" name="task" />
<button>Add item</button>
</div>
</form>
<ul
role="list"
class="link-card-grid"
id="todo-list"
hx-get="/list-items"
hx-trigger="load"
>
</ul>
</div>
Analisi del codice
Cominciamo dall'elemento form:
hx-post="/add-item"stabilisce il tipo di richiesta HTTP e l'endpoint dell'API;hx-target="#todo-list"stabilisce l'elemento del DOM in cui sarà iniettata la risposta. Nel nostro esempio, si tratta dellaul#todo-list;hx-swap="beforeend"stabilisce il punto in cui il codice della risposta deve essere inserito rispetto all'elemento target. Nel nostro esempio, prima del tag di chiusura;hx-on::after-request="addtask.reset()ci permette di inserire uno script in linea per rispondere agli eventi direttamente sull'elemento. In questo modo, il form sarà reimpostato dopo l'invio della richiesta.
Generazione dei pulsanti radio
Il passo successivo è la generazione dei pulsanti radio per la selezione della categoria:
{
categories.map(({ id, category }) => (
<input type="radio" id={category} name="category" value={id} />
<label for={category}>{category}</label><br />
))
}
La div successiva contiene un campo di testo e un pulsante per l'invio del modulo:
<div>
<input type="text" placeholder="Add item" name="task" />
<button>Add item</button>
</div>
Infine, la lista in cui sarà iniettato il markup contenuto nella risposta:
<ul
role="list"
class="link-card-grid"
id="todo-list"
hx-get="/list-items"
hx-trigger="load"
>
</ul>
hx-get="/list-items"invia una richiesta GET all'endpoint/list-items;hx-trigger="load"stabilisce l'evento che attiva la richiesta; in questo caso, è l'eventoload. Ad ogni caricamento di pagina, dall'elementoulpartirà una richiesta GET verso l'endpoint/list-items.
L'aggiunta di nuovi record
Il primo endpoint che creeremo ci consentirà di inserire i dati nel database. Nella cartella /src/pages, creiamo il file add-item.astro e cominciamo a scrivere il codice del frontmatter:
---
import { db, Todos, Categories, eq } from 'astro:db';
import Card from '../components/Card.astro';
let taskId = 0;
let title = '';
let body = '';
let done = false;
Abbiamo importato le risorse necessarie e dichiarato alcune variabili. Passiamo poi al codice che elabora la richiesta:
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const task = formData.get('task');
const catId = Number( formData.get('category') );
if (typeof catId === 'number' && typeof task === 'string' && task.length > 0 ) {
const lastRecord = await db.insert(Todos).values({ task, catId, done }).returning();
taskId = lastRecord[0].id;
body = lastRecord[0].task;
const result = await db.select().from(Categories).where(eq(Categories.id, lastRecord[0].catId));
title = result[0].category;
} else {
const response = new Response(null, {
status: 302,
statusText: 'Not found',
headers: {
'Location': '/error'
}
});
console.log("Errore nell'inserimento dei campi del form - " + response);
return response;
}
}
---
Astro.request.formData()accede ai dati del form;formData.get(...)restituisce un singolo campo;- la condizione che segue effettua una serie di verifiche e, in caso di esito positivo, produce una query
INSERTper aggiungere un nuovotaskalla tabellaTodos. Quindi una querySELECTrestituisce la categoria del record appena inserito; - in caso di errore, viene generato un redirect all'endpoint
/errore un avviso nella console del browser.
In questo esempio abbiamo ridotto il codice al minimo. Per gestire gli errori in modo più accurato, consigliamo di seguire la documentazione di Astro e di mdn.
Aggiungiamo ora il markup che sarà restituito ad ogni richiesta POST verso /add-item:
<Card
id={taskId}
title={title}
body={body}
done={done}
/>
I valori degli attributi dell'elemento Card sono forniti dalle variabili taskId, body, title e done generati nello script.
Il componente Card per la visualizzazione dei dati
Apriamo il file /src/components/Card.astro e sostituiamo il codice presente con quello che segue:
---
interface Props {
id: number;
title: string;
body: string;
done: boolean;
}
const { id, title, body, done } = Astro.props;
---
Qui abbiamo definito l'interfaccia Props e destrutturato le quattro proprietà del componente Card.
Passiamo poi al markup:
<li class=`link-card ${done === true ? 'done' : 'todo'}`>
<div>
<h2>
{title}
<span>→</span>
</h2>
<p>
{id} - {body}
</p>
<button
hx-patch="/update-item"
hx-vals=`{ "itemId": ${id} }`
hx-target="closest li"
hx-swap="outerHTML"
>
{done === true ? 'Undo' : 'Complete'}
</button>
<button
hx-delete="/delete-item"
hx-vals=`{ "itemId": ${id} }`
hx-confirm=`Are you sure you want to delete #${id} - ${body}?`
hx-target="closest li"
hx-swap="outerHTML swap:1s"
>
Delete
</button>
</div>
</li>
Ogni card contiene due pulsanti. Il primo pulsante invia una richiesta PATCH all'endpoint /update-item:
- l'attributo
hx-valstrasmette il parametroitemIdcon la richiesta AJAX; - il target della richiesta è l'elemento di lista più vicino al pulsante (
"closest li"), quindi illiche lo contiene; - lo swap è
outerHTML, il che vuol dire che sarà sostituito l'intero elemento; - l'operatore ternario ci permette di assegnare al pulsante un'etichetta diversa a seconda del valore di
done; - il secondo pulsante invia una richiesta
DELETEall'endpoint/delete-item.
Assegnazione dello stile
Tornando al tag di apertura <li>, qui viene assegnata dinamicamente la classe done/todo a seconda del valore di done. Questo ci permette di assegnare all'elemento uno stile diverso a seconda dello stato del task. Riscriviamo, quindi, lo stile dell'elemento Card:
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card.htmx-swapping {
opacity: 0;
transition: opacity 1s ease-out;
}
.link-card > div {
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card.done {
background-color: #00ff00;
}
.link-card.todo:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card.todo:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}

La card che mostra a video i dati del task
Nella prossima lezione creeremo gli endpoint che ancora mancano e vedremo come gestire gli errori di trasmissione del form.
Se vuoi aggiornamenti su Astro e htmx: trasmissione e inserimento dei dati inserisci la tua email nel box qui sotto: