Na primeira metade deste artigo, configuramos uma pilha de desenvolvimento web e criamos um aplicativo de exemplo simples usando Bun, HTMX, Elysia e MongoDB. Aqui, continuaremos explorando nossa nova pilha enquanto limpamos e abstraímos a camada de acesso a dados do aplicativo de exemplo e adicionamos interações HTMX mais complexas. Também adicionaremos outro componente à pilha de tecnologia: Pug, um mecanismo de modelo JavaScript popular que funciona bem com HTMX e ajuda a configurar interações DOM.
O aplicativo de exemplo
Nosso aplicativo de exemplo atualmente consiste em um formulário e uma tabela. O formulário permite que os usuários insiram citações junto com seus autores, que podem então ser pesquisadas e exibidas usando a interface de usuário do aplicativo. Adicionei um pouco de CSS à interface para torná-la mais moderna do que deixamos na Parte 1:
Aqui está o código front-end para a interface atualizada:
<script src="https://unpkg.com/[email protected]"></script>
<form hx-post="/add-quote" hx-swap-oop="beforeend:#data-list" hx-trigger="every time">
<input type="text" name="quote" placeholder="Enter quote">
<input type="text" name="author" placeholder="Enter author">
<button type="submit">Add Quote</button>
</form>
<ul id="data-list"></ul>
<button hx-get="/quotes" hx-target="#data-list">Load Data</button>
Estamos usando HTMX para conduzir o processo de envio do formulário e carregamento de dados na tabela. Também limpei o back-end do aplicativo para que a conectividade do banco de dados agora seja compartilhada. Aqui está aquela parte src/index.ts:
import { Elysia } from "elysia";
import { staticPlugin } from '@elysiajs/static';
const { MongoClient } = require('mongodb');
// Database connection details
const url = "mongodb://127.0.0.1:27017/quote?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.8.0";
const dbName = "quote";
const collectionName = "quotes";
let client = new MongoClient(url, { useUnifiedTopology: true });
// Connect to the database (called only once)
async function connectToDatabase() {
try {
await client.connect();
} catch (error) {
console.error(error);
throw error; // Re-throw the error to indicate connection failure
}
return { client, collection: client.db(dbName).collection(collectionName) };
}
// Close the database connection
async function closeDatabaseConnection(client) {
await client.close();
}
O que estamos fazendo aqui é definir a URL do banco de dados como o endereço localhost padrão do MongoDB, junto com um banco de dados e um nome de coleção. Então, usamos um async função, connectToDatabase(), para conectar o cliente e retorná-lo conectado à coleção. Nosso código pode então chamar esse método sempre que precisar acessar o banco de dados e, quando terminar, pode chamar client.close().
Usando a conexão com o banco de dados
Vejamos como nossos endpoints de servidor usarão esse suporte de banco de dados. Para resumir, estou apenas mostrando o /quotes endpoint que orienta a tabela:
// Close the database connection
async function closeDatabaseConnection(client) {
await client.close();
}
async function getAllQuotes(collection) {
try {
const quotes = await collection.find().toArray();
// Build the HTML table structure
let html="<table border="1">";
html += '<tr><th>Quote</th><th>Author</th></tr>';
for (const quote of quotes) {
html += `<tr><td>${quote.quote}</td><td>${quote.author}</td></tr>`;
}
html += '</table>';
return html;
} catch (error) {
console.error("Error fetching quotes", error);
throw error; // Re-throw the error for proper handling
}
}
// Main application logic
const app = new Elysia()
.get("https://www.infoworld.com/", () => "Hello Elysia")
.get("/quotes", async () => {
try {
const { client, collection } = await connectToDatabase();
const quotes = await getAllQuotes(collection);
await closeDatabaseConnection(client);
return quotes;
} catch (error) {
console.error(error);
return "Error fetching quotes";
}
})
.use(staticPlugin())
.listen(3000);
console.log(
` Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Isso nos dá um back-end /quotes Endpoint GET que podemos chamar para obter os dados de cotações. O ponto final chama o getAllQuotes() método, que usa a coleção de connectToDatabase() para obter a matriz de citações e autores. Em seguida, gera o HTMX para as linhas.
Por fim, enviamos uma resposta contendo as linhas como HTMX e as linhas são inseridas na tabela.
Adicione o mecanismo de modelagem Pug
Criar manualmente a linha HTMX pode causar frustração e erros. Um mecanismo de modelagem nos permite definir a estrutura HTMX em um arquivo eterno com uma sintaxe limpa.
O mecanismo de modelagem HTML mais popular para JavaScript é o Pug. Usá-lo tornará a criação de visualizações no servidor muito mais fácil e escalonável do que incorporar o código JavaScript. A ideia básica é pegar nossos objetos de dados e passá-los para o modelo, que aplica os dados e gera HTML. A diferença aqui é que estamos gerando HTML em vez de HTML. Podemos fazer isso porque HTML é essencialmente HTML com extensões.
Para começar, adicione a biblioteca Pug ao projeto com: $ bun add pug.
Quando isso for concluído, crie um novo diretório na raiz do projeto chamado /views: ($ mkdir views)e adicione um novo arquivo chamado quotes.pug:
doctype html
h1 Quotes
table
thead
tr
th Quote
th Author
th Actions
tbody
each quote in quotes
tr(id=`quote-${quote._id}`)
td #{quote.quote}
td #{quote.author}
td
button(hx-delete=`/quotes/${quote._id}` hx-trigger="click" hx-swap="closest tr" hx-confirm="Are you sure?") Delete
#{quote._id}
Pug usa recuo para lidar com elementos aninhados. Os atributos são mantidos entre parênteses. Texto simples, como a palavra Excluir é fornecido como está. Tudo isso nos dá uma maneira compacta de descrever HTML e/ou HTMX. Consulte a página inicial do Pug para saber mais sobre sua sintaxe.
Observe que dentro de uma string, precisamos usar ${}. O #{} A sintaxe permite fazer referência a quaisquer objetos de dados que foram injetados no modelo. Isso é semelhante à interpolação de token em uma estrutura como React. A idéia básica é definir a estrutura geral do HTML/HTMX e, em seguida, fornecer variáveis ao modelo que são referenciadas com #{} e ${}.
Fornecemos as variáveis de volta ao servidor /quotes ponto final, que usa getAllQuotes():
import pug from 'pug';
//...
async function getAllQuotes(collection) {
try {
const quotes = await collection.find().toArray();
// Render the Pug template with the fetched quotes
const html = pug.compileFile('views/quotes.pug')({ quotes });
return html;
} catch (error) {
console.error("Error fetching quotes", error);
throw error; // Re-throw the error for proper handling
}
}
Então, pegamos as cotações do banco de dados, compilamos o modelo Pug e passamos as cotações. Em seguida, Pug faz o trabalho de juntar o HTML e os dados. O fluxo geral é:
- O pedido chega às
GET /quotes. - As cotações são recuperadas do MongoDB.
- O template Pug recebe as cotações.
- O modelo Pug renderiza as aspas como HTML e/ou HTMX.
- O HTML e/ou HTMX preenchido é enviado como resposta.
A tela resultante é mais ou menos assim:
Interações DOM: Excluindo uma linha
Agora precisamos fazer nosso botão Excluir funcionar. Simplesmente emitindo um delete solicitar e manipulá-la no servidor e no banco de dados é fácil de fazer com o que já vimos, mas que tal atualizar a tabela para refletir a mudança?
Existem várias maneiras de abordar a atualização. Poderíamos simplesmente atualizar a tabela inteira ou usar JavaScript ou HTMX para excluir a linha da tabela. Idealmente, gostaríamos de usar a última opção e manter tudo como HTMX.
Na nossa views/quotes.pug template, podemos usar HTML puro para excluir a linha:
tbody(hx-target="closest tr" hx-swap="outerHTML")
each quote in quotes
tr(id=`quote-${quote._id}`)
td #{quote.quote}
td #{quote.author}
td
button(hx-delete=`/quotes/${quote._id}` hx-trigger="click" hx-confirm="Are you sure?") Delete
As partes essenciais aqui são as hx-target=”closest tr” e hx-swap=”outerHTML” no tbody. (O hx-confirm permite que você forneça um confirm caixa de diálogo.) O hx-target diz para substituir o mais próximo tr ao elemento gatilho (o botão) com a resposta. O outHTML em hx-swap garante a remoção de todo o elemento da linha da tabela, não apenas seu conteúdo. No lado do servidor, retornamos um sucesso (HTTP 200) com corpo vazio, então o HTML simplesmente excluirá a linha:
async function deleteQuote(collection, quoteId) {
try {
const result = await collection.deleteOne({ _id: new ObjectId(quoteId) });
if (result.deletedCount === 1) {
return "";
} else {
throw new Error( "Quote not found");
}
} catch (error) {
console.error("Error deleting quote", error);
throw error; // Re-throw the error for proper handling
}
}
Aqui, estamos apenas começando a entrar em interações DOM mais envolventes. O HTMX também pode adicionar efeitos de transição simples às trocas em um cenário de exclusão de linha como o nosso. Você pode ver um exemplo na página inicial do HTML.
Conclusão
Embora este tutorial de duas partes incorpore tecnologias mais recentes, como Bun e Elysia, o componente mais notável é o HTMX. Isso realmente muda a forma como um aplicativo funciona em comparação com APIs JSON convencionais.
Quando combinado com um mecanismo de modelagem como o Pug e um banco de dados como o MongoDB, o trabalho de geração de UIs e tratamento de solicitações é tranquilo. À medida que o tamanho de um aplicativo aumenta, os recursos do Pug, como herança de modelo, também são úteis.
Para interações DOM, o HTMX apresenta funcionalidade flexível pronta para uso via hx-swap e hx-target. Para casos de uso mais complexos, você sempre pode recorrer ao JavaScript.
Em geral, toda essa pilha funciona bem em conjunto. Você também pode apreciar a velocidade do Bun sempre que precisar acessar a linha de comando para fazer algo como adicionar uma dependência.
Você pode encontrar o código deste tutorial em meu repositório GitHub.
