# Diálogos, notificaciones y validación

> Comunícate con el usuario en una app de Pyzed: errores de campo calculados a partir del estado, un diálogo de confirmación sobre la página y notificaciones que desaparecen solas.

Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/dialogs-feedback/

Una buena app le cuenta al usuario qué pasa: qué está mal en el formulario, qué está a punto de pasar, qué acaba de
pasar. Pyzed tiene una pieza lista para cada cosa: **errores de campo**, **diálogos** y **toasts** (pequeñas
notificaciones en una esquina).

| Objetivo | Tiempo | Nivel | Necesitas |
| --- | --- | --- | --- |
| Un formulario de registro con errores, una confirmación y notificaciones | 30 minutos | Intermedio · Python | [Listas y tablas](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/) |

## Qué vas a aprender

- Calcular los errores de validación a partir de tu estado y mostrarlos con `ui.field(..., error=...)`.
- Mostrarlos solo después del primer intento, no mientras el usuario todavía escribe.
- Abrir un **diálogo de confirmación** sobre la página con `ui.dialog_layer` y `ui.confirm`.
- Mostrar **toasts** con `ui.toasts` y quitarlos a los pocos segundos sin congelar la ventana.

## Pasos

1. **Los errores son una función del estado.** Un método devuelve `{campo: mensaje}` con lo que esté mal ahora
   mismo:

```python
def errors(self):
    found = {}
    if not self.name.strip():
        found["name"] = "Tell us your name."
    if "@" not in self.email:
        found["email"] = "Enter an email such as ada@example.com."
    if len(self.password) < 8:
        found["password"] = "At least 8 characters."
    return found
```

2. **Muéstralos bajo cada campo**, pero solo cuando el usuario ya intentó enviar (`self.tried`). A nadie le gusta
   ver texto rojo antes de tener la oportunidad:

```python
errors = self.errors() if self.tried else {}
ui.field("Email", ui.input(self.email, lambda v: setattr(self, "email", v)), error=errors.get("email"))
```

3. **Un diálogo de confirmación.** `ui.dialog_layer(page, dialog)` dibuja `dialog` centrado sobre la página
   oscurecida; con `dialog=None` se ve solo la página. Así que "¿está abierto el diálogo?" es un atributo:

```python
dialog = None
if self.confirming:
    dialog = ui.confirm(f"Create an account for {self.email}?", self.create,
                        lambda: setattr(self, "confirming", False), title="Almost there",
                        confirm_label="Create")
page = ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "confirming", False))
```

4. **Toasts.** Guarda una lista de avisos, dibújalos con `ui.toasts` y quita cada uno a los 4 segundos.
   `Command.perform` espera en un hilo en segundo plano, así que la ventana sigue respondiendo:

```python
def notify(self, kind, title, text=""):
    note_id = next(_ids)
    self.notes.append((note_id, kind, title, text))
    return pz.Command.perform(lambda: time.sleep(4), lambda _: self.dismiss(note_id))

# en view():
return ui.toasts(page, [ui.toast(title, text, kind=kind, on_dismiss=lambda i=i: self.dismiss(i))
                        for i, kind, title, text in self.notes])
```

5. **Júntalo todo.** El botón marca `tried`, muestra un toast de error si algo está mal o abre el diálogo;
   confirmar vacía el formulario y muestra un toast de éxito.

**Otros diálogos listos:**

`ui.alert` (un botón), `ui.prompt` (pide una línea de texto), `ui.choice` (tus propios botones) y
`ui.unsaved_changes` ("¿Guardar antes de cerrar?") funcionan igual. Míralos todos en `examples/dialogs.py` y en las
páginas [Alert](https://albakore.github.io/pyzed/es/library/alert/) y [Confirm](https://albakore.github.io/pyzed/es/library/confirm/) de la biblioteca.

**Validar con Pydantic:**

Si tus datos ya tienen un modelo de Pydantic (o SQLModel, Django, FastAPI), deja que valide él y convierte sus
errores en `{campo: mensaje}` con `pyzed.forms.validation_errors(error)`. Mira
[Pydantic](https://albakore.github.io/pyzed/es/integrations/pydantic/).

## Código completo

`examples/tutorial/feedback.py`:

```python
"""Talking to the user: validation messages, a confirmation dialog and toast notifications.

    python examples/tutorial/feedback.py

- ``ui.field(..., error=...)`` shows a message under a field; the errors are computed from the state.
- ``ui.dialog_layer(page, dialog)`` puts a dialog (``ui.confirm``, ``ui.alert``...) over the page;
  ``dialog`` is ``None`` when nothing is open.
- ``ui.toasts(page, [...])`` stacks notifications in the corner; ``Command.perform`` waits a few
  seconds in the background and then removes them.

Walkthrough: https://albakore.github.io/pyzed/tutorials/intermediate/dialogs-feedback/
"""

import itertools
import time

import pyzed as pz
from pyzed import ui

_ids = itertools.count()

class Signup(pz.App):
    title = "Create an account"
    size = (640, 560)

    def __init__(self):
        self.name = ""
        self.email = ""
        self.password = ""
        self.tried = False   # show errors only after the first try
        self.confirming = False
        self.notes = []      # (id, kind, title, text) of the visible toasts

    # -- validation -----------------------------------------------------------------------
    def errors(self):
        found = {}
        if not self.name.strip():
            found["name"] = "Tell us your name."
        if "@" not in self.email or "." not in self.email.split("@")[-1]:
            found["email"] = "Enter an email such as ada@example.com."
        if len(self.password) < 8:
            found["password"] = "At least 8 characters."
        return found

    # -- toasts ---------------------------------------------------------------------------
    def notify(self, kind, title, text=""):
        note_id = next(_ids)
        self.notes.append((note_id, kind, title, text))
        # wait 4 seconds in a background thread, then remove it (the window never freezes)
        return pz.Command.perform(lambda: time.sleep(4), lambda _: self.dismiss(note_id))

    def dismiss(self, note_id):
        self.notes = [n for n in self.notes if n[0] != note_id]

    # -- actions --------------------------------------------------------------------------
    def submit(self):
        self.tried = True
        if self.errors():
            return self.notify("error", "Check the form", "Some fields need your attention.")
        self.confirming = True  # open the dialog

    def create(self):
        self.confirming = False
        name = self.name
        self.name = self.email = self.password = ""
        self.tried = False
        return self.notify("success", "Account created", f"Welcome, {name}!")

    # -- view -----------------------------------------------------------------------------
    def view(self):
        t = self.resolved_theme()
        errors = self.errors() if self.tried else {}
        form = pz.column([
            ui.heading("Create an account", level=2),
            ui.muted("Leave a field empty and press the button to see the messages."),
            ui.field("Name", ui.input(self.name, lambda v: setattr(self, "name", v), placeholder="Ada Lovelace"),
                     error=errors.get("name")),
            ui.field("Email", ui.input(self.email, lambda v: setattr(self, "email", v), icon="mail",
                                       placeholder="ada@example.com"), error=errors.get("email")),
            ui.field("Password", ui.input(self.password, lambda v: setattr(self, "password", v), secure=True,
                                          on_submit=self.submit), error=errors.get("password"),
                     description="8 characters or more"),
            pz.row([pz.hspace(), ui.button("Create account", self.submit, variant="solid", icon="user-plus")]),
        ], spacing=14, padding=28, width="fill")
        page = pz.container(form, width="fill", height="fill", bg=t.background)

        dialog = None
        if self.confirming:
            dialog = ui.confirm(f"Create an account for {self.email}?", self.create,
                                lambda: setattr(self, "confirming", False), title="Almost there",
                                confirm_label="Create")
        page = ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "confirming", False))
        return ui.toasts(page, [ui.toast(title, text, kind=kind, on_dismiss=lambda i=i: self.dismiss(i))
                                for i, kind, title, text in self.notes])

if __name__ == "__main__":
    pz.run(Signup)
```

## Eventos de este tutorial

| Widget | Evento | Handler |
| --- | --- | --- |
| `ui.input` (tres campos) | `on_input` | `lambda v: setattr(self, "name", v)`… |
| `ui.input` (contraseña) | `on_submit` | `self.submit` |
| **Create account** | `on_press` | `self.submit` |
| `ui.confirm` | `on_confirm`, `on_cancel` | `self.create`, cerrar el diálogo |
| `ui.dialog_layer` | `on_dismiss` | cerrar el diálogo (un clic en el fondo) |
| `ui.toast` | `on_dismiss` | `lambda i=i: self.dismiss(i)` |

### Prueba más eventos

```python
def on_key(self, combo):
    if combo == "escape" and self.confirming:   # Escape cierra el diálogo
        self.confirming = False
        return []                               # atendida: la tecla no sigue

def view(self):
    ...
    toasts = [ui.toast(title, text, kind=kind,
                       on_dismiss=lambda i=i: self.dismiss(i),
                       on_press=lambda i=i: self.dismiss(i))   # un clic en la tarjeta también lo cierra
              for i, kind, title, text in self.notes]
```

## Siguiente

- [Un componente propio](https://albakore.github.io/pyzed/es/tutorials/intermediate/custom-component/) en la vista de componente del
  diseñador.
- [Tareas en segundo plano](https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/): barras de progreso y toasts desde
  hilos.
