# Listas y tablas con datos

> Muestra datos de Python en una app de Pyzed: una lista con búsqueda, una tabla ordenable con filas clicables y celdas propias, y un panel con los detalles de la fila elegida.

Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/

Casi todas las apps muestran **datos**: clientes, archivos, pedidos, libros. Este tutorial toma una lista normal de
Python y la muestra de dos formas, como **tabla** ordenable y como **lista** simple, con un buscador y un panel con
los detalles de la fila en la que haces clic.

| Objetivo | Tiempo | Nivel | Necesitas |
| --- | --- | --- | --- |
| Una biblioteca de libros con búsqueda, orden y detalles | 30 minutos | Intermedio · Python | [Una app con varias vistas](https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/) |

## Qué vas a aprender

- Filtrar datos en `view()` a partir de un buscador.
- Dibujar un `ui.table` con columnas, **orden** al hacer clic en un encabezado y filas clicables.
- Convertir una celda en cualquier widget (una insignia de color) con `render=`.
- Mostrar los mismos datos como una lista de filas `ui.list_item`.
- Guardar el elemento **seleccionado** y mostrarlo en un panel de detalles; cambiarlo en el sitio.

## Pasos

1. **Los datos son solo Python.** Una lista de dicts (también sirven objetos o dataclasses):

```python
BOOKS = [
    {"title": "Dune", "author": "Frank Herbert", "year": 1965, "status": "Read"},
    {"title": "Neuromancer", "author": "William Gibson", "year": 1984, "status": "Reading"},
    # ...
]
```

2. **Búsqueda.** Guarda el texto en `self.query` y filtra en cada redibujado. No hace falta "refrescar": escribir
   cambia el estado y `view()` se vuelve a ejecutar.

```python
def visible(self):
    q = self.query.lower()
    return [b for b in BOOKS if q in b["title"].lower() or q in b["author"].lower()]
```

3. **Las columnas.** Un `ui.Column` tiene un título, la clave que lee y un ancho (`("fill", 3)` toma tres partes
   del espacio libre). `render=` convierte el valor en un widget:

```python
columns = [
    ui.Column("Title", "title", width=("fill", 3)),
    ui.Column("Author", "author", width=("fill", 2)),
    ui.Column("Year", "year", width=70),
    ui.Column("Status", "status", width=100, render=lambda s: ui.badge(s, tint=TINTS[s])),
]
```

4. **Orden.** Pasa a la tabla la columna por la que ordena y el sentido; al hacer clic en un encabezado se llama a
   `on_sort` con el título de esa columna, y tú inviertes el sentido si es la misma:

```python
def sort(self, column):
    self.descending = not self.descending if column == self.sort_by else False
    self.sort_by = column

ui.table(columns, books, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort,
         on_row_press=lambda book: setattr(self, "selected", book), selected=self.selected,
         striped=True, height="fill")
```

5. **El panel de detalles** muestra `self.selected`, o un estado vacío. Cambiar el estado del libro escribe en el
   mismo dict, así que la tabla también lo muestra:

```python
ui.segmented(list(TINTS), book["status"], lambda s: book.__setitem__("status", s))
```

6. **Una vista de lista.** Los mismos datos como filas con un icono y el año al final, alternando con un control
   segmentado:

```python
ui.list_item(book["title"], lambda book=book: setattr(self, "selected", book), icon="book",
             selected=book is self.selected, end=ui.muted(str(book["year"])))
```

**Miles de filas:**

`ui.table` solo construye las filas que se ven, así que una tabla de 100.000 filas se desplaza tan suave como una
de diez. Mantén la lista ordenada en tu estado (o en una función `@pz.memo`) en lugar de ordenarla en cada
redibujado. La demo de [contactos con SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/) muestra 20.000 filas
buscadas en SQL.

## Código completo

`examples/tutorial/lists_tables.py`:

```python
"""Lists and tables with data: search, sort, select a row and show its details.

    python examples/tutorial/lists_tables.py

- The data is a plain Python list of dicts; ``view()`` filters it with the search text.
- ``ui.table`` draws it with sortable columns (``sort_by``/``descending``/``on_sort``),
  clickable rows (``on_row_press``) and a highlighted ``selected`` row.
- ``ui.Column(..., render=...)`` turns a cell into any widget (here, a colored badge).
- A segmented control switches between the table and a simple list of ``ui.list_item`` rows.

Walkthrough: https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/
"""

import pyzed as pz
from pyzed import ui

BOOKS = [
    {"title": "Dune", "author": "Frank Herbert", "year": 1965, "status": "Read"},
    {"title": "Neuromancer", "author": "William Gibson", "year": 1984, "status": "Reading"},
    {"title": "The Left Hand of Darkness", "author": "Ursula K. Le Guin", "year": 1969, "status": "Read"},
    {"title": "Foundation", "author": "Isaac Asimov", "year": 1951, "status": "Wishlist"},
    {"title": "Hyperion", "author": "Dan Simmons", "year": 1989, "status": "Reading"},
    {"title": "Snow Crash", "author": "Neal Stephenson", "year": 1992, "status": "Wishlist"},
    {"title": "Solaris", "author": "Stanisław Lem", "year": 1961, "status": "Read"},
]
TINTS = {"Read": "success", "Reading": "accent", "Wishlist": "warning"}

class Library(pz.App):
    title = "My books"
    size = (860, 540)

    def __init__(self):
        self.query = ""
        self.sort_by, self.descending = "Year", False
        self.selected = None  # the book (a dict) shown in the details panel
        self.mode = "Table"

    def visible(self):
        q = self.query.lower()
        return [b for b in BOOKS if q in b["title"].lower() or q in b["author"].lower()]

    def sort(self, column):
        # Clicking the same header again flips the order.
        self.descending = not self.descending if column == self.sort_by else False
        self.sort_by = column

    def table(self, books):
        columns = [
            ui.Column("Title", "title", width=("fill", 3)),
            ui.Column("Author", "author", width=("fill", 2)),
            ui.Column("Year", "year", width=70),
            ui.Column("Status", "status", width=100, render=lambda s: ui.badge(s, tint=TINTS[s])),
        ]
        return ui.table(columns, books, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort,
                        on_row_press=lambda book: setattr(self, "selected", book), selected=self.selected,
                        striped=True, row_height=34, height="fill")

    def simple_list(self, books):
        return pz.scrollable(pz.column([
            ui.list_item(book["title"], lambda book=book: setattr(self, "selected", book), icon="book",
                         selected=book is self.selected, end=ui.muted(str(book["year"])), height=32)
            for book in books
        ], spacing=2), height="fill")

    def details(self):
        book = self.selected
        if book is None:
            return ui.empty_state("Pick a book", "Click a row to see its details.", icon="book-open")
        return pz.column([
            ui.heading(book["title"], level=3),
            ui.label(f"by {book['author']}, {book['year']}", color="text_muted"),
            ui.segmented(list(TINTS), book["status"], lambda s: book.__setitem__("status", s)),
        ], spacing=10)

    def view(self):
        t = self.resolved_theme()
        books = self.visible()
        left = pz.column([
            pz.row([
                pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v),
                                             placeholder="Search title or author…"), width="fill"),
                ui.segmented(["Table", "List"], self.mode, lambda m: setattr(self, "mode", m)),
            ], spacing=8, align="center"),
            ui.muted(f"{len(books)} of {len(BOOKS)} books", size="sm"),
            self.table(books) if self.mode == "Table" else self.simple_list(books),
        ], spacing=10, width="fill", height="fill")
        right = pz.container(self.details(), width=260, height="fill", padding=16, bg=t.surface, radius=t.radius)
        return pz.container(pz.row([left, right], spacing=16), padding=20, width="fill", height="fill",
                            bg=t.background)

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

## Eventos de este tutorial

| Widget | Evento | El handler recibe |
| --- | --- | --- |
| `ui.table` | `on_sort` | El título de la columna pulsada. |
| `ui.table` | `on_row_press` | La fila: el dict del libro. |
| `ui.list_item` | `on_press` | Nada (el libro se ata con `book=book`). |
| `ui.search_input`, `ui.segmented` | `on_input`, `on_select` | La búsqueda; el modo. |

### Prueba más eventos

La tabla tiene dos eventos de fila más: el doble clic y el clic derecho.

```python
def table(self, books):
    return ui.table(
        columns, books, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort,
        selected=self.selected,
        on_row_press=lambda book: setattr(self, "selected", book),
        on_row_double_press=lambda book: setattr(self, "editing", book),   # abrir un editor
        on_row_context=lambda book: setattr(self, "menu_for", book),       # un menú contextual
        striped=True, row_height=34, height="fill",
    )

def on_key(self, combo):                    # las flechas mueven la selección
    books = self.visible()
    if combo in ("up", "down") and books:
        i = books.index(self.selected) if self.selected in books else -1
        self.selected = books[max(0, min(len(books) - 1, i + (1 if combo == "down" else -1)))]
```

## Siguiente

- [Diálogos, notificaciones y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/dialogs-feedback/).
- La [tabla de datos](https://albakore.github.io/pyzed/es/showcase/dashboards/#tabla-de-datos) del Showcase: filtros, selección, páginas y
  exportación.
- [Table](https://albakore.github.io/pyzed/es/library/table/) y [List item](https://albakore.github.io/pyzed/es/library/list-item/) en la biblioteca de componentes.
