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.
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
Categories
daastro: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'elementoul
partirà 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
INSERT
per aggiungere un nuovotask
alla tabellaTodos
. Quindi una querySELECT
restituisce la categoria del record appena inserito; - in caso di errore, viene generato un redirect all'endpoint
/error
e 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-vals
trasmette il parametroitemId
con la richiesta AJAX; - il target della richiesta è l'elemento di lista più vicino al pulsante (
"closest li"
), quindi illi
che 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
DELETE
all'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));
}
Nella prossima lezione creeremo gli endpoint che ancora mancano e vedremo come gestire gli errori di trasmissione del form.