# Custom components

> Build reusable Pyzed components with their own props, state, logic and events, like React components, and use them in the visual designer.

Source: https://albakore.github.io/pyzed/guides/components/

A component is a class that groups widgets with their logic, like a React component:

- **Props** are annotated class attributes with defaults. The parent passes them on every render.
- **State** is any other attribute you set. It stays between renders.
- `render()` returns the component's widgets.
- `events` declares the custom events it raises with `self.emit(name, value)`.

```python
import pyzed as pz

class Rating(pz.Component):
    """Stars the user can click."""

    value: int = 0                 # props
    max: int = 5
    events = {"change": "A star was picked (event.value is the rating)."}
    icon = "star"                  # how it appears in the designer's toolbox

    def setup(self):               # once, before the first render
        self.hover = None          # state

    def render(self):
        shown = self.hover or self.value
        return pz.row([
            pz.text("★" if i <= shown else "☆", size=20).on(
                click=lambda i=i: self.emit("change", i),
                mouse_enter=lambda i=i: setattr(self, "hover", i),
                mouse_leave=lambda: setattr(self, "hover", None),
            )
            for i in range(1, self.max + 1)
        ], spacing=2)
```

Calling a component returns a widget, so it goes anywhere a widget does:

```python
class Review(pz.App):
    stars = 3

    def view(self):
        return pz.column([
            Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value)),
            pz.text(f"{self.stars} of 5"),
        ])
```

(Image: Events and components)

See [`examples/components.py`](https://github.com/albakore/pyzed/blob/main/examples/components.py)
for a star rating, a tag editor and a draggable note.

## Events

- **Custom events** are declared in `events`, as a tuple of names or a `{name: description}` dict.
  The parent handles them with `on_<name>=`.
- `self.emit(name, value)` calls the parent's handler with a `pz.Event` whose `value` and
  `source` are set. Return its result from your callback, so a `Command` or a message reaches
  the runtime.
- **Common events**, such as `on_click`, `on_drag` and `on_key_down`, work on every component
  without any code. See [Events](https://albakore.github.io/pyzed/guides/events/).
- **Lifecycle events**: `on_mount` and `on_unmount` tell the parent when the component appears
  and disappears.
- **`style=`** styles the component like any widget (`Rating(value=3, style="card")`), unless it
  declares a `style` prop of its own. See [Styles](https://albakore.github.io/pyzed/theming/styles/).

## State and keys

State can also be declared on the class with `pz.state(default)`. Unlike a prop, the parent cannot
pass it, every instance starts with its own copy of the default (so lists and dicts are safe), and the
designer lists it next to the props:

```python
class Counter(pz.Component):
    step: int = 1             # prop
    count: int = pz.state(0)  # state

    def render(self):
        return ui.button(f"{self.count}", lambda: setattr(self, "count", self.count + self.step))
```

State is kept per position in the parent's `view()`: the second `Rating` in a view keeps its own
state as long as the order stays the same. When components come from a list whose items move,
give each one a stable `key`:

```python
pz.column([TodoItem(key=todo.id, todo=todo) for todo in self.todos])
```

A component that is no longer rendered is unmounted, and its state is dropped.

```mermaid
flowchart LR
    View["App.view()"] -->|"Rating(value=3)"| Slot{"Instance at this<br/>position or key?"}
    Slot -->|no| New["new instance<br/>setup()"]
    Slot -->|yes| Same["same instance<br/>(state kept)"]
    New --> Props["props set"]
    Same --> Props
    Props --> Changed{"props, state or<br/>context changed?"}
    Changed -->|yes| Render["render() → widgets"]
    Changed -->|no| Last["last drawing, reused"]
    Render -->|first time| Mount["on_mount()"]
    View -.->|not rendered again| Unmount["on_unmount()"]
```

`render()` only runs when something it depends on changed: its props, its state, the context it
reads, the theme or a component inside it. Otherwise the last drawing is reused, so a view with
hundreds of components stays fast. If `render()` reads something else (the time, a global), call
`self.refresh()` when it changes or set `cache = False` on the class. See
[components drawn only when they change](https://albakore.github.io/pyzed/guides/performance/#components-drawn-only-when-they-change).

## Children, context and styles

Components nest like HTML elements and React components:

```python
class Card(pz.Component):
    title: str = ""
    children: list = ()            # the positional arguments, like JSX children
    stylesheet = {                 # CSS-like rules for this component (& is the component)
        "&": {"background": "panel", "padding": 16, "radius": 8, "border": (1, "border")},
        "&:hover": {"border_color": "accent"},
    }

    def render(self):
        return pz.box(pz.text(self.title).style(weight="bold"), *self.children, gap=8)

Card(pz.text("Three new messages"), ui.button("Open", self.open), title="Inbox",
     class_="wide", animate="fade_in_up")
```

- A prop called `children` takes the positional arguments. Lists are flattened, and `None`
  and `False` are skipped.
- `stylesheet` adds rules to [`pz.stylesheet`](https://albakore.github.io/pyzed/theming/styles/#stylesheets) when the class
  is defined. `&` stands for the component: `"&"`, `"&:hover"`, `"&.wide"`.
- Every component takes `style=`, `class_=`, `id=` and `animate=` (presets for
  [`widget.animate`](https://albakore.github.io/pyzed/animation/)), unless it declares props with those names.
- Context shares values with every component rendered inside, without passing props down
  each level:

```python
with pz.provide(user=self.user):
    page = Dashboard()                      # any component inside reads it

class Avatar(pz.Component):
    def render(self):
        return ui.avatar(pz.use_context("user").name)   # or self.context("user")
```

A component can also share values with what it renders by returning them from a
`provide()` method. Children passed in as arguments are rendered before the component, so
they see the context around it, not its own. See
[`examples/web_layout.py`](https://github.com/albakore/pyzed/blob/main/examples/web_layout.py) for a page built this way.

## Lifecycle

| Hook | When |
| --- | --- |
| `setup()` | Once, before the first render. Props are already set. Initialize state here. |
| `render()` | On every render of the parent. |
| `on_mount()` | After the first render. It may return a `Command` or a message. |
| `on_unmount()` | When the component is no longer rendered. |

Hooks run after the view is drawn, never in the middle of it.

## In the designer

Every `pz.Component` subclass appears in the [designer](https://albakore.github.io/pyzed/editor/)'s toolbox:

- **Props** become properties. `bool` gets a switch, `int` and `float` a number box, `Literal[...]`
  a drop-down, `list` one item per line, and a prop named `color` or `icon` gets its own picker.
  Unions, optionals, dicts, sets and tuples (`int | None`, `dict[str, int]`...) get an editor
  built from the type, with a row per item.
  The type hints of `pyzed` pick a richer editor, like export hints in Godot:

  | Annotation | Editor |
  | --- | --- |
  | `pz.Color` | A color picker (theme token or `#rrggbb`). |
  | `pz.Icon` | An icon picker. |
  | `pz.Multiline` | A text area. |
  | `Annotated[int, pz.Range(0, 10)]` | A number box limited to 0 to 10 (`step` is optional). |

  They are plain `str` and `int` at run time: `tint: pz.Color = "accent"`.
- **Custom events** become events you can write code for.

Class attributes control how it appears:

| Attribute | Default |
| --- | --- |
| `label` | The class name, split into words. |
| `icon` | `"component"` (any Lucide icon). |
| `category` | `"Custom"`. |
| `size` | `(160, 40)`, the size when placed on a form. |
| `designer` | `True`; `False` keeps it out of the toolbox. |
| `type_name` | The class name in snake case; the id saved in forms. |

The docstring's first paragraph is shown as the description.

To build a component by drawing it instead of writing `render()`, use the designer's
[component mode](https://albakore.github.io/pyzed/editor/component-designer/).
