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:

Interface HTMX com modelagem Pug

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.