Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Astro e htmx: trasmissione e inserimento dei dati

Astro e htmx: come realizzare un form che permetta all'utente di creare nuovi task e supportare il trasferimento di dati
Astro e htmx: come realizzare un form che permetta all'utente di creare nuovi task e supportare il trasferimento di dati
Link copiato negli appunti

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

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 Categories da astro:db;
  • abbiamo importato il componente Layout;
  • infine, il metodo db.select() restituisce l'array completo di tutti i record della tabella Categories.

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> &amp; <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>&rarr;</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 della ul#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'evento load. Ad ogni caricamento di pagina, dall'elemento ul 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 nuovo task alla tabella Todos. Quindi una query SELECT 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>&rarr;</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 parametro itemId con la richiesta AJAX;
  • il target della richiesta è l'elemento di lista più vicino al pulsante ("closest li"), quindi il li 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));
}

La card che mostra a video i dati del task

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.

Ti consigliamo anche