Na aula passada, começamos a explorar o HTMX demonstrando como transformar suas aplicações em SPAs (Single Page Application). Nessa aula, faremos mais exemplos e mostraremos funcionalidades mais avançadas dessa ferramenta. Também discutiremos o que são frameworks front-end (como o React) e explicaremos um pouco de seu funcionamento.
De forma básica, frameworks de front-end diferem de frameworks de back-end pois são voltados para o cliente ao invés do servidor. A principal funcionalidade desse tipo de framework é a atuaização da página com base nas mudanças dos dados.
Um exemplo de sua funcionalidade client-side é o like das redes sociais. Em um framework de front-end, ao clicar no like, instantanemente é atualizado o ícone do like sem que seja preciso acionar o back-end ou atualizar toda a página.
Frameworks de back-end, como o, FastAPI ou Django, utilizam Server-Side Rendering, ou seja, o servidor faz todo o processamento e envia o HTML completo para o browser. Já frameworks de front-end utilizam Client-Side Rendering: O servidor envia apenas um pouco de HTML e muito JavaScript para que o próprio browser possa construir a página a ser visualizada.
Alguns frameworks de front-end conhecidos são: React, Angular, Vue, ...
Muitos dos benefícios e malefícios dos frameworks de front-end quando comparados aos de back-end se assemelham a comparação de MPAs e SPAs. React e Angular são ótimos para criar SPAs, aplicações mais dinâmicas e com mais interatividade com o usuário, enquanto FastAPI e Django são melhores para aplicações que requerem muito processamento de dados.
Atualmente, é comum que aplicações web utilizem ambos os tipos de frameworks, aproveitando os pontos positivos de cada um para que o produto final seja capaz de performar em situações mais gerais.
HTMX não é um framework de front-end, apenas uma biblioteca de JavaScript. A versão 1.0 do HTMX foi lançada em 2020, então ainda é uma tecnologia nova que está se desenvolvendo mais a cada dia.
Nesse curso, optamos por ensinar HTMX ao invés de React, por exemplo, pois HTMX é notavelmente mais simples e fácil de utilizar do que qualquer framework de front-end. Veja abaixo um exemplo de código de React:
import React, { useState, useEffect } from 'react';
const ExemploRequisicao = () => {
const [conteudo, setConteudo] = useState(null);
const [carregando, setCarregando] = useState(false);
const buscarDados = async (idPagina) => {
setCarregando(true);
try {
const response = await fetch(`https://localhost:8000/home/${idPagina}`);
const data = await response.json();
setConteudo(data);
} catch (error) {
console.error("Erro ao buscar dados:", error);
setConteudo({ title: "Erro", body: "Não foi possível carregar a página." });
} finally {
setCarregando(false);
}
};
useEffect(() => {
buscarDados('pagina1');
}, []);
return (
<div>
<h1>Meu Site</h1>
<div>
<button onClick={() => buscarDados('pagina1')}>Página 1</button>
<button onClick={() => buscarDados('pagina2')}>Página 2</button>
</div>
<hr/>
{carregando ? (
<p>Carregando...</p>
) : conteudo ? (
<div>
<h2>{conteudo.title}</h2>
<p>{conteudo.body}</p>
</div>
) : (
<p>Nenhum conteúdo disponível.</p>
)}
</div>
);
};
export default ExemploRequisicao;
O código acima é uma adaptação do exemplo das páginas da aula passada. Veja abaixo o código utilizando HTMX:
<DOCTYPE html>
<html lang="pt-br">
<head>
<title>Meu Site</title>
<style>...</style>
<script src="HTMX"></script> <!-- Import do HTMX -->
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<nav>
<button hx-get="/home/pagina1"
hx-target="main">
Página 1
</button>
<button hx-get="/home/pagina2"
hx-target="main">
Página 2
</button>
</nav>
<hr>
<main>
<h2>Pagina 1</h2>
<p>Conteúdo da página 1</p>
</main>
</body>
</html>
O código com HTMX é mais simples de compreender e não contém nenhum JavaScript, apenas atributos no HTML. Não é preciso definir funções, variáveis nem nada do tipo, basta inserir os tributos hx e sua página se torna mais próxima de uma SPA. Dessa forma, julgamos que seria mais proveitoso ensinar HTMX do que algum framework front-end.
Mostraremos agora como o HTMX pode se comunicar com o banco de dados. Para isso, utilizaremos o exemplo da busca dinâmica. Criaremos uma lista de alunos e buscaremos os alunos salvos no banco de dados por nome.
Primeiramente devemos criar uma forma de adicionar alunos no banco de dados, e faremos isso utilizando requests HTMX. Utilizaremos como base o mesmo app da aula passada, um cabeçalho com dois botões para navegação de abas. Veja nosso código inicial.
# Arquivo Models.py
from typing import Optional
from sqlmodel import Field, SQLModel
class Aluno(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nome: str
Nos nossos modelos, criamos o modelo Aluno, que contém um id e o nome.
<!-- Arquivo index.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="HTMX"></script> <!-- Import do HTMX -->
<title>Meu Site</title>
<style>...</style>
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<button hx-get="/lista"
hx-target="main">
Lista de alunos
</button>
<button hx-get="/editarAlunos"
hx-target="main">
Editar lista de alunos
</button>
<hr>
<main hx-get="/lista"
hx-target="main"
hx-trigger="load">
</main>
</body>
</html>
No index.html, colocamos apenas nosso cabeçalho e os botões de navegação para as páginas com a lista de alunos e a de editar as informações dos alunos.
<!-- Arquivo lista.html -->
<input type="text" placeholder="Buscar...">
<div>
<ul>
{% for aluno in alunos %}
<li>{{ aluno.id }} - {{ aluno.nome }}</li>
{% else %}
<li>Não há alunos</li>
{% endfor %}
</ul>
</div>
No lista.html, temos uma tag input (para escrever o nome do aluno que queremos buscar) que ainda não funciona e um loop 'for' para listar todos os alunos.
<!-- Arquivo options.html -->
<form>
<input type="text" placeholder="Nome do Aluno" required>
<button type="submit">Salvar</button>
</form>
<br>
<div id="mensagem"></div>
Em options.html, colocamos um form (com input e o botão submit) que configuraremos para adicionar um aluno no banco de dados. A div em baixo será utilizada para uma mensagem de retorno.
# Arquivo main.py
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from Models import Aluno
from contextlib import asynccontextmanager
from sqlmodel import SQLModel, create_engine, Session, select
@asynccontextmanager
async def initFunction(app: FastAPI):
create_db_and_tables()
yield
app = FastAPI(lifespan=initFunction)
arquivo_sqlite = "HTMX2.db"
url_sqlite = f"sqlite:///{arquivo_sqlite}"
engine = create_engine(url_sqlite)
templates = Jinja2Templates(directory=["Templates", "Templates/Partials"])
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def buscar_alunos():
with Session(engine) as session:
query = select(Aluno)
return session.exec(query).all()
@app.get("/busca", response_class=HTMLResponse)
def busca(request: Request):
return templates.TemplateResponse(request, "index.html")
@app.get("/lista", response_class=HTMLResponse)
def lista(request: Request):
alunos = buscar_alunos()
return templates.TemplateResponse(request, "lista.html", {"alunos": alunos})
@app.get("/editarAlunos")
def novoAluno(request: Request):
return templates.TemplateResponse(request, "options.html")
Por fim, temos a main.py, que organiza os endpoints para os requests que queremos fazer.
Com esse código inicial, o index.html será renderizado ao acessarmos /busca e, a partir dele, temos os botões de redirecionamento para a página da lista de alunos (que é renderizada por automático em primeiro acesso) e para a página de edição de alunos.
A partir do código acima, faremos modificações para que o botão 'Salvar' em options.html salve um novo aluno no banco de dados com o nome declarado no input. Veja o código abaixo com as modificações necessárias.
<!-- Arquivo options.html -->
<form hx-post="/novoAluno"
hx-target="#mensagem"
hx-swap="beforeend">
<input type="text" name="nome" placeholder="Nome do Aluno" required>
<button type="submit">Salvar</button>
</form>
<br>
<div id="mensagem"></div>
Note que adicionamos os atributos hx-post no form e name no input.
O name="nome" faz referência a uma variável 'nome' que estará na função que definiremos no back-end. Ao fazer o request para o servidor, o HTMX enviará o que foi escrito no input dentro do corpo do request com o identificador 'nome'. Precisaremos avisar ao FastAPI que a variável 'nome' que ele procura não está na url, e sim no corpo do request.
Note também a presença do hx-swap, o atributo que especifica como vamos fazer a atualização da div alvo. Nesse caso, estamos utilizando o beforeend, que faz com que a resposta do servidor seja colocada como último filho da div 'mensagem'. Isso faz com que novas mensagens de confirmação de ação não sobrescrevam as antigas e tenhamos um "log" das nossas ações.
Contudo, ainda não temos o endpoit /novoAluno que é chamado pelo request post. Devemos criar essa função:
# Arquivo main.py
@app.post("/novoAluno", response_class=HTMLResponse)
def criar_aluno(nome: str = Form(...)):
with Session(engine) as session:
novo_aluno = Aluno(nome=nome)
session.add(novo_aluno)
session.commit()
session.refresh(novo_aluno)
return HTMLResponse(content=f"<p>O(a) aluno(a) {novo_aluno.nome} foi registrado(a)!</p>")
O Form(...) utilizado após o parâmetro da função é o que indica para o FastAPI onde ele deve buscar pela variável 'nome', como comentamos acima.
Agora, ao digitar um nome e pressionar o botão 'Salvar', o aluno será adicionado ao banco de dados e passará a ser renderiazdo na lista.
Para adicionar a opção de deletar um aluno do banco de dados, precisamos que seja informado o identificador (id) do aluno desejado. Utilizar o nome para localizar o aluno que desejamos não é ideal, pois podem existir estudantes com mesmo nome, mas os ids sempre serão distintos. Assim, podemos adicionar uma nova tag input em options.html
<!-- Arquivo options.html -->
<form hx-delete="/deletaAluno"
hx-target="#mensagem"
hx-swap="beforeend">
<input type="number" name="id" placeholder="ID do aluno" required>
<button type="submit">Deletar aluno</button>
</form>
Com esse form, podemos especificar um id de um aluno para que ele seja removido do banco de dados, mas ainda precisamos da função que fará a deleção do usuário. Veja o código dessa função abaixo
# Arquivo main.py
@app.delete("/deletaAluno", response_class=HTMLResponse)
def deletar_aluno(id: int):
with Session(engine) as session:
query = select(Aluno).where(Aluno.id == id)
aluno = session.exec(query).first()
if (not aluno):
raise HTTPException(404, "Aluno não encontrado")
session.delete(aluno)
session.commit()
return HTMLResponse(content=f"<p>O(a) aluno(a) {aluno.nome} foi deletado(a)!</p>")
O hx-delete em específico passa as variáveis como querry parameters (no caso, o request feito será /deletaAluno?id=1, por exemplo). Logo, não precisamos utilizar o Form(...) como no request anterior, pois a variável será encontrada na URL.
Agora, nossa página tem um botão para deletar um Aluno do banco de dados.
Para atualizar os dados de um aluno já existente, faremos um processo muito semelhante ao de deleção. Veja o código necessário para fazer atualizações:
<!-- Arquivo options.html -->
<form hx-put="/atualizaAluno"
hx-target="#mensagem"
hx-swap="beforeend">
<input type="number" name="id" placeholder="ID do aluno" required>
<input type="text" name="novoNome" placeholder="Novo nome do aluno" required>
<button type="submit">Atualizar aluno</button>
</form>
# Arquivo main.py
@app.put("/atualizaAluno", response_class=HTMLResponse)
def atualizar_aluno(id: int = Form(...), novoNome: str = Form(...)):
with Session(engine) as session:
query = select(Aluno).where(Aluno.id == id)
aluno = session.exec(query).first()
if (not aluno):
raise HTTPException(404, "Aluno não encontrado")
nomeAntigo = aluno.nome
aluno.nome = novoNome
session.commit()
session.refresh(aluno)
return HTMLResponse(content=f"<p>O(a) aluno(a) {nomeAntigo} foi atualizado(a) para {aluno.nome}!</p>")
As únicas diferenças do código de atualização para o de deleção de Aluno são o tipo do request (para a atualização estamos usandoPUT) e que voltamos a utilizar o Form(...), pois o hx-put passa as variáveis pelo corpo do request.
Por fim, temos controle sobre nosso banco de dados e populamos nossa lista de Alunos. Agora, podemos criar o sistema que fará a busca por alunos na nossa lista.
Normalmente, criaríamos um novo endpoint para realizar essa busca, o que significa que teríamos que fragmentar o arquivo lista.html em dois: Uma parte com o input e outra parte com a lista em si. No entanto, o HTMX possuí o atributo hx-select que nos ajudará a resolver esse problema e nos permitirá fazer essa busca apenas com o endpoint /lista já definido.
Veja abaixo o HTML modificado para que a busque funcione:
<!-- Arquivo lista.html -->
<input type="text"
placeholder="Buscar..."
hx-get="/lista"
hx-target="#lista"
hx-trigger="input changed delay:500ms"
hx-select="#lista"
name="busca">
<div id="lista">
<ul>
{% for aluno in alunos %}
<li>{{ aluno.id }} {{ aluno.nome }}</li>
{% else %}
<li>Não há alunos</li>
{% endfor %}
</ul>
</div>
Vamos analisar o que está sendo feito pelo HTMX no input:
hx-get faz um request para /lista.hx-target indica que a resposta desse request deve ser inserida na div com id='lista'.hx-trigger indica o que dispara o request. No caso, o request será feito toda vez que o input mudar (input changed), ou seja, toda vez que for adicionada ou removida uma letra da caixa de texto, sem precisar apertar o 'Enter'.delay:500ms faz com que o request seja apenas disparado 500ms após o término da digitação. Isso previne uma mudança "epilética" da lista e faz com que nossa busca atue como esperado.hx-select muda a forma que o request é respondido. Ao invés de ser retornado todo o HTML que viria no request GET de /lista, o hx-select indica que deve apenas ser retornada a div cujo id é 'lista' no HTML obtido no request.hx-select não está fazendo referência à div com id 'lista' do próprio HTML, mas sim a div com id 'lista' que virá no request.Para terminar, precisamos apenas adapatar nossa função em python para que ela receba o argumento 'busca' e faça a filtragem dos Alunos.
# Arquivo main.py
def buscar_alunos(busca):
with Session(engine) as session:
query = select(Aluno).where(col(Aluno.nome).contains(busca)).order_by(Aluno.nome)
return session.exec(query).all()
@app.get("/lista", response_class=HTMLResponse)
def lista(request: Request, busca: str | None=''):
alunos = buscar_alunos(busca)
return templates.TemplateResponse(request, "lista.html", {"alunos": alunos})
Ainda temos duas observações importantes para fazer.
A primeira é que foi passado um None='' para a variável busca, pois o primeiro request de /lista renderiza a página completa e não terá um parâmetro no input (filtraremos os alunos pela string vazia, ou seja, serão retornados todos os alunos).
A segunda observação é que utilizamos o .order_by(Aluno.nome) na query. Por padrão, o objeto retornado seria ordenado pelos ids dos alunos, mas preferimos que eles fossem ordenados por ordem alfabética.
Finalmente, nossa busca pelos alunos da lista está totalmente funcional!
Para finalizar a aula, falaremos de algumas melhorias visuais possíveis para nossa página.
Existe no HTMX o atributo hx-indicator. Esse atributo serve para indicar ao usuário que um processamento está ocorrendo no lado do servidor. Vamos adicionar esse atributo na nossa caixa de busca:
<!-- Arquivo lista.html -->
<div>
<input type="text"
name="busca"
placeholder="Buscar..."
hx-get="/lista"
hx-trigger="input changed delay:500ms"
hx-select="#lista"
hx-target="#lista"
hx-indicator="#indicator">
<span id="indicator" class="htmx-indicator">Carregando...</span>
</div>
Veja que adicionamos uma tag 'span' com o texto "Carregando..." para servir como nosso indicador. Também adicionamos a classe 'htmx-indicator' e um id para esse span. No hx-indicator, passamos o id do elemento que servirá como nosso indicador de carregamento.
Agora, enquanto o banco de dados processa a informação, o texto "Carregando..." aparecerá ao lado da nossa caixa de busca para indicar ao usuário que seu input está sendo processado.
O hx-indicator nos permite usar qualquer elemento html como indicador, basta passar o id e colocar a classe 'htmx-indicator'. A opção mais popular é utilizar um svg de um círculo rotativo . Para fazer isso, precisamos carregar o svg na nossa aplicação.
No FastAPI, carregar mídias externas (como imagens) é um processo semelhante à criação de templates. Primeiro, precisamos criar um diretório no mesmo endereço de main.py. Por convenção, normalmente nomeamos esse diretório como "Static" ou "Media". Depois, precisamos importar esse diretório em nosso back-end, e podemos fazer isso com o seguinte comando:
# Arquivo main.py
app.mount("/Static", StaticFiles(directory="static"), name="static")
Isso nos permite referenciar o diretório "Static" dentro da nossa aplicação. Como estamos apenas fazendo uso desse diretório dentro de um HTML, podemos simplesmente subistituir a linha do antigo indicador por:
<!-- Arquivo lista.html -->
<img id="indicator" class="htmx-indicator" src="/Static/spinner.svg" alt="Carregando..."/>
Animações servem puramente para apelo visual. Normalmente, elas são criadas utilizando CSS e JavaScript, o que não nos afeta pois o HTMX possui integração com animações e também pode ser utilizado normalamente. Como exemplo, criaremos um botão de apagar o nosso site.
Para desencadear nossa animação, vamos adicionar um novo botão na nav do nosso site:
<!-- Arquivo index.html -->
<button hx-delete="/apagar"
hx-target="body"
hx-confirm="Deseja deletar meu site?"
hx-swap="outerHTML swap:1s"
id="apagar">
Deletar site
</button>
Esse botão subistituirá o body do nosso HTML com a informação retornada pelo request hx-delete de /apagar, que será uma página HTML em branco. O hx-swap indica que será trocado o 'outerHTML' da página e que essa troca demorará 1 segundo para se completar. Por fim, o hx-confirm enviará uma mensagem para confirmação antes de efetuar o request, e já veremos como isso pode ser utilizado.
Agora, precisamos construir a animação de fato usando CSS.
<!-- Arquivo index.html -->
<style>
.fade-out.htmx-swapping {
opacity: 0;
transition: opacity 1s ease-out;
}
</style>
<body class="fade-out">
A animação em questão será um Fade-out, ou seja, colocaremos o HTML recebido pelo servidor "em baixo" do HTML atual e modificaremos a opacidade do HTML atual para zero. O transition é uma das formas mais comuns de se implementar animações em CSS, mas também é comum o uso de animation e os @keyframes. No transition indicamos o que irá mudar, quanto tempo durará a transição e a curva da animação.
Curvas de animação determinam a velocidade da animação. Para mais informações sobre as curvas de animação, a página Josh Comeau tem demostrações visuais de como algumas curvas se comportam.
Mencionamos acima que precisamos de uma resposta vazia do servidor. Podemos facilmente implementar essa função em nosso back-end.
# Arquivo main.py
@app.delete("/apagar", response_class=HTMLResponse)
def apagar():
return ""
A última coisa que ainda precisamos fazer é estilizar a mensagem de confirmação. Por enquanto, o hx-confirm apenas envia um alert para confirmarmos se realmente desejamos deltar o site, mas está utilizando o alert padrão do navegador. Podemos utilizar a bibliteca sweetalert para estilizar nossos alerts. Veja o script de estilização abaixo:
<!-- Arquivo index.html -->
<!-- Import da biblioteca que deve ser colocado na head -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Script de estilização que deve ser colocado no final do body -->
<script>
document.getElementById("apagar").addEventListener("htmx:confirm", (e) => {
e.preventDefault()
Swal.fire({
title: "Prosseguir?",
text: `${e.detail.question}`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
confirmButtonText: "Sim, deletar o site",
cancelButtonText: "Cancelar",
cancelButtonColor: "#d33"
}).then(result => {
if (result.isConfirmed) {
e.detail.issueRequest(true);
}
})
})
</script>
Assim, fizemos a estilização do nosso botão de alert e nossa página está completa!
A partir da página construída nessa aula, faça com que a visualização da lista de alunos seja paginada.
Veja abaixo um vídeo mostrando como deve ser o funcionamento da paginação:
Neste exercício, não podem ser utilizadas bibliotecas como fastapi-pagination, é possível resolver apenas com o conteúdo visto no curso.
Essa semana não vamos passar exercícios para casa. Sua tarefa será finalizar seu projeto.
As especificações do projeto estão listadas aqui.