# Padrões de Interação HTMX — PharmData Design System

> Convenções para interatividade inline com HTMX no ecossistema PharmData.
> Toda interação que não exija navegação de página deve seguir estes padrões.

---

## Princípios

1. **Inline primeiro** — Edição, confirmação e feedback acontecem na própria página, sem navegação.
2. **Atualização imediata** — Após qualquer ação (salvar, excluir, alterar), os dados visíveis atualizam automaticamente, sem reload.
3. **Carregamento sob demanda** — Dados pesados (evidências, histórico, relações) carregam apenas quando o usuário solicita.
4. **Feedback explícito** — O usuário sempre sabe o que aconteceu: sucesso, erro, carregando.

---

## 1. Modal de Edição (List → Edit → List)

O padrão mais comum: editar um registro sem sair da listagem.

### Fluxo

```
Lista → clique no item → modal abre (hx-get)
Modal → preencher form → salvar (hx-post)
POST responde com HX-Trigger → fecha modal + atualiza tabela
```

### Na lista: link que abre o modal

```html
<a href="#"
   hx-get="/vmps/{{ id }}/modal"
   hx-target="#modal-container"
   hx-swap="innerHTML"
   onclick="event.preventDefault()">
    {{ nome }}
</a>
```

### Container do modal (no final da página de lista)

```html
<div id="modal-container"></div>
```

### Endpoint do modal (retorna HTML parcial, sem `base.html`)

```python
@router.get("/{id}/modal")
def modal_item(request: Request, id: int):
    item = repo.get(id)
    return templates.TemplateResponse("_modal.html", {
        "request": request,
        "item": item,
    })
```

### Form dentro do modal

```html
<form hx-post="/vmps/{{ id }}" hx-swap="none">
    <!-- campos -->
    <button type="submit" class="btn btn-primary">Salvar</button>
</form>
```

### POST que fecha modal e atualiza tabela

```python
import json

@router.post("/{id}")
def save_item(request: Request, id: int, ...):
    repo.update(id, ...)
    if request.headers.get("HX-Request") == "true":
        return HTMLResponse("", headers={
            "HX-Trigger": json.dumps({
                "closeModal": None,
                "refreshTable": None,
            }),
        })
    return RedirectResponse(f"/items/{id}", status_code=303)
```

> **Importante**: múltiplos triggers HTMX devem ser JSON (`{"a": null, "b": null}`), não string separada por vírgula.

> **Armadilha de timing**: eventos `HX-Trigger` são disparados no elemento que fez o request. Se `closeModal` remove esse elemento do DOM antes de `refreshTable` propagar, o segundo evento nunca chega ao `document.body`. Solução: usar `requestAnimationFrame` no `closeModal` para adiar a remoção do DOM.

### Listeners JS na página de lista

```html
<script>
// Fechar modal (requestAnimationFrame adia a remoção para após todos os HX-Trigger propagarem)
document.body.addEventListener('closeModal', function() {
    requestAnimationFrame(function() {
        document.getElementById('modal-container').innerHTML = '';
    });
});

// Atualizar tabela re-disparando a busca
document.body.addEventListener('refreshTable', function() {
    var form = document.querySelector('.search-bar');
    if (form) form.requestSubmit();
});

// Fechar ao clicar no overlay
document.body.addEventListener('click', function(e) {
    if (e.target.classList.contains('modal-overlay')) {
        document.getElementById('modal-container').innerHTML = '';
    }
});

// Fechar com Escape
document.body.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        document.getElementById('modal-container').innerHTML = '';
    }
});
</script>
```

---

## 2. Carregamento Sob Demanda (Lazy Load)

Dados secundários (evidências, histórico, relações, detalhes expandidos) não carregam com a página ou modal. Carregam quando o usuário clica.

### Botão que carrega conteúdo

```html
<div id="evidencias-content">
    <button type="button" class="btn btn-secondary" style="width: 100%;"
            hx-get="/vmps/{{ id }}/evidencias"
            hx-target="#evidencias-content"
            hx-swap="innerHTML">
        <i class="fa-solid fa-table-list"></i> Ver enquadramento nos AMPPs
    </button>
</div>
```

### Endpoint parcial

```python
@router.get("/{id}/evidencias")
def evidencias(request: Request, id: int):
    data = repo.get_evidencias(id)
    return templates.TemplateResponse("_evidencias.html", {
        "request": request,
        "evidencias": data,
    })
```

### Quando usar lazy load

| Carregar imediatamente | Carregar sob demanda |
|---|---|
| Dados do formulário principal | Evidências / justificativas |
| Opções de select (refs estáticas) | Histórico de alterações |
| Status e badges | Relações com outros registros |
| Dados que cabem em 1 query leve | Dados vindos de views ou joins pesados |

---

## 3. Busca ao Vivo (Live Search)

Tabelas com muitos registros usam busca HTMX sem reload.

### Form de busca

```html
<form class="search-bar"
      hx-get="/items/search"
      hx-target="#table-body"
      hx-trigger="submit,
                   keyup[target.value.length >= 3 || target.value.length == 0]
                   changed delay:300ms from:.search-input">
    <input type="text" name="q" class="form-input search-input"
           placeholder="Digite 3+ caracteres para buscar...">
    <select name="filtro" class="form-select"
            onchange="this.form.requestSubmit()">
        <option value="">Todos</option>
        <!-- opções -->
    </select>
</form>
```

### Regras

- **3+ caracteres** para disparar busca (evita queries sem filtro).
- **delay:300ms** para não disparar a cada tecla.
- **Selects** disparam imediatamente via `requestSubmit()`.
- O endpoint retorna **apenas as `<tr>`**, não a tabela inteira.

---

## 4. Cache de Dados de Referência

Tabelas de referência (tarjas, regimes, listas, tipos) raramente mudam. Devem ser cacheadas em memória no servidor.

### Padrão

```python
_cache: dict | None = None

def get_all_refs() -> dict:
    global _cache
    if _cache is None:
        _cache = _load_from_db()  # 1 conexão, N queries
    return _cache

def invalidate_cache():
    global _cache
    _cache = None
```

### Regras

- **Uma única conexão** para carregar todas as refs (não uma por tabela).
- Cache vive enquanto o processo vive.
- Expor `invalidate_cache()` para quando os dados de referência forem atualizados.
- Refs cacheadas são usadas em todos os templates (selects, maps de lookup).

---

## 5. Feedback Pós-Ação

### Sucesso silencioso

Ações simples (salvar classificação) não precisam de toast/alert — o modal fecha e a tabela atualiza. O feedback é a própria atualização visual dos dados.

### Erro explícito

Se o POST falhar, retornar o HTML do form com mensagem de erro inline:

```python
if erro:
    return templates.TemplateResponse("_modal.html", {
        "request": request,
        "item": item,
        "erro": "Não foi possível salvar. Tente novamente.",
    })
```

```html
{% if erro %}
<div class="alert alert-warning" style="margin-bottom: var(--space-sm);">
    <i class="fa-solid fa-circle-exclamation"></i> {{ erro }}
</div>
{% endif %}
```

### Indicador de carregamento

HTMX adiciona a classe `htmx-request` ao elemento que disparou a requisição. Usar para feedback visual:

```css
.btn.htmx-request {
    opacity: 0.6;
    pointer-events: none;
}
```

---

## 6. Compatibilidade com Navegação Direta

Endpoints POST devem funcionar tanto via HTMX quanto via navegação tradicional:

```python
@router.post("/{id}")
def save(request: Request, id: int, ...):
    repo.update(id, ...)

    # HTMX: fecha modal + atualiza tabela
    if request.headers.get("HX-Request") == "true":
        return HTMLResponse("", headers={
            "HX-Trigger": json.dumps({"closeModal": None, "refreshTable": None}),
        })

    # Navegação direta: redirect padrão
    return RedirectResponse(f"/items/{id}", status_code=303)
```

---

## Checklist de Implementação

- [ ] Modal usa classes do DS (`modal-overlay`, `modal-panel`, `modal-close`)
- [ ] Form usa `hx-post` com `hx-swap="none"`
- [ ] POST retorna `HX-Trigger` como JSON para múltiplos eventos
- [ ] Listeners para `closeModal`, `refreshTable`, overlay click, Escape
- [ ] Dados pesados carregam sob demanda (botão + `hx-get`)
- [ ] Refs de select cacheadas em memória (não re-buscam a cada request)
- [ ] POST funciona tanto via HTMX quanto navegação direta
- [ ] Indicador visual durante carregamento (`htmx-request`)

---

*Ver também*:
- [view-patterns.md](view-patterns.md) — Padrões de visualização
- [compositional-forms.md](compositional-forms.md) — Formulários composicionais
- [enrichment-workflow.md](enrichment-workflow.md) — Fluxo de enriquecimento
- `tokens/modals.css` — Classes CSS de modal do DS
