dataclasses enhancement
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
# Binding Dataclasses Deep Dive
|
||||
# Binding Dataclasses
|
||||
|
||||
Use this reference to model NiceGUI state with bindable dataclasses and avoid common propagation and performance pitfalls.
|
||||
Use this reference to understand how NiceGUI creates binding links, detects changes, propagates values, and applies `forward` and `backward` transforms.
|
||||
|
||||
The implementation details and signatures below are verified against NiceGUI `3.16.0`. Check the target project's pinned version before copying version-sensitive behavior.
|
||||
|
||||
## Primary Sources
|
||||
|
||||
- NiceGUI binding docs: [binding properties](https://www.nicegui.io/documentation/section_binding_properties)
|
||||
- Python dataclass docs: [dataclasses module](https://docs.python.org/3/library/dataclasses.html)
|
||||
- Data class design rationale: [PEP 557](https://peps.python.org/pep-0557/)
|
||||
- [NiceGUI binding documentation](https://www.nicegui.io/documentation/section_binding_properties): public binding behavior and examples
|
||||
- [NiceGUI `binding.py` at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/binding.py): binding graph, propagation, active links, strict checks, and `bindable_dataclass`
|
||||
- [NiceGUI `ValueElement` at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/value_element.py): `bind_value*` signatures and transform direction
|
||||
- [Python dataclasses](https://docs.python.org/3/library/dataclasses.html): generated methods, fields, defaults, and mutable-value rules
|
||||
- [PEP 557](https://peps.python.org/pep-0557/): dataclass design rationale
|
||||
|
||||
## Bindable Dataclass Behavior
|
||||
## What `bindable_dataclass` Changes
|
||||
|
||||
`@binding.bindable_dataclass` extends standard dataclasses by turning fields into bindable properties, allowing UI bindings to propagate when a field is assigned.
|
||||
`@binding.bindable_dataclass` first applies Python's `@dataclass`, then replaces each selected field on the resulting class with a NiceGUI `BindableProperty` descriptor. The descriptor stores the field value privately and intercepts later assignment.
|
||||
|
||||
```python
|
||||
from nicegui import binding, ui
|
||||
@@ -26,24 +30,152 @@ profile = Profile()
|
||||
|
||||
ui.input("Name").bind_value(profile, "name")
|
||||
ui.number("Age", min=0).bind_value(profile, "age")
|
||||
ui.label().bind_text_from(profile, "name", backward=lambda name: f"User: {name}")
|
||||
ui.label().bind_text_from(
|
||||
profile,
|
||||
"name",
|
||||
backward=lambda name: f"User: {name}",
|
||||
)
|
||||
```
|
||||
|
||||
## Propagation And Performance
|
||||
Assigning a different value to `profile.name` invokes the descriptor immediately. It records the new value, propagates it through the binding graph, and then runs any descriptor change handler. Assigning an equal value returns without propagation.
|
||||
|
||||
NiceGUI distinguishes between two link types:
|
||||
By default every dataclass field is bindable. Pass `bindable_fields` to limit descriptor conversion:
|
||||
|
||||
- Bindable properties propagate efficiently when values are assigned.
|
||||
- Active links are checked in a refresh loop.
|
||||
```python
|
||||
@binding.bindable_dataclass(bindable_fields={"query", "page_size"})
|
||||
class SearchState:
|
||||
query: str = ""
|
||||
page_size: int = 25
|
||||
request_count: int = 0
|
||||
```
|
||||
|
||||
Prefer bindable dataclasses for frequently updated form state. Keep binding transforms pure and inexpensive. If an application has many active links, tune `binding_refresh_interval` in `ui.run(...)` only after measuring the impact.
|
||||
A bound field omitted from `bindable_fields` still works, but NiceGUI must treat it as an active link and poll it for changes.
|
||||
|
||||
## Binding Graph And Propagation
|
||||
|
||||
NiceGUI stores bindings as directed edges from one object attribute to another. A two-way binding is two one-way edges with transforms in opposite directions.
|
||||
|
||||
When an edge is registered, NiceGUI propagates its source immediately. For a two-way binding, it registers and runs the `backward` edge first, then registers the `forward` edge. The model value therefore wins initial synchronization and seeds the control.
|
||||
|
||||
After registration, propagation follows these rules:
|
||||
|
||||
1. A `BindableProperty` assignment starts propagation immediately when `old_value != new_value`.
|
||||
2. NiceGUI walks outgoing edges depth first.
|
||||
3. Each object-and-attribute node is visited at most once during that propagation pass, preventing a two-way cycle from running forever.
|
||||
4. Each edge transforms the source value, compares it with the target, and only assigns and continues when the values differ.
|
||||
|
||||
Since NiceGUI `2.16.0`, this depth-first walk updates each affected node once per pass. Transform functions must not depend on call count or traversal order.
|
||||
|
||||
### Bindable Properties Versus Active Links
|
||||
|
||||
| Source | Change detection | Update timing |
|
||||
| --- | --- | --- |
|
||||
| NiceGUI element property or `BindableProperty` field | descriptor intercepts assignment | immediate |
|
||||
| ordinary object attribute or mapping entry | refresh loop compares source and target | next refresh step |
|
||||
| tuple path such as `("address", "city")` | the full path is not a single bindable descriptor key | refresh loop unless the owning leaf object is bound directly |
|
||||
|
||||
The active-link refresh interval defaults to `0.1` seconds and is configured with `binding_refresh_interval` in `ui.run(...)`. Every refresh applies the transform and compares the result, so polling large collections or running expensive transforms can block the event loop. Tune the interval only after measuring; first reduce active links and transform cost.
|
||||
|
||||
## Transform Direction
|
||||
|
||||
The names `forward` and `backward` are relative to the element on which `bind_value*` is called:
|
||||
|
||||
| API | Source to target | Transform |
|
||||
| --- | --- | --- |
|
||||
| `element.bind_value_to(model, "field")` | element to model | `forward` |
|
||||
| `element.bind_value_from(model, "field")` | model to element | `backward` |
|
||||
| `element.bind_value(model, "field")` | both directions | both; `backward` runs first initially |
|
||||
|
||||
Each transform adapts the source value before NiceGUI assigns it to the target. The examples below convert between control values and native Python types only to make the two directions easy to observe; they do not prescribe a state-modeling approach.
|
||||
|
||||
Keep both functions pure, fast, and valid for every value the source can emit. NiceGUI does not turn transform exceptions into validation messages.
|
||||
|
||||
## Example: Observe Both Directions
|
||||
|
||||
This example uses [`datetime.date`](https://docs.python.org/3/library/datetime.html#date-objects) and `int` conversions to expose the mechanics. Their different representations make it clear which transform runs as a value crosses each binding edge.
|
||||
|
||||
```python
|
||||
from dataclasses import field
|
||||
from datetime import date
|
||||
|
||||
from nicegui import binding, ui
|
||||
|
||||
|
||||
@binding.bindable_dataclass
|
||||
class ReportFilters:
|
||||
start_on: date = field(default_factory=date.today)
|
||||
page_size: int = 25
|
||||
|
||||
|
||||
filters = ReportFilters()
|
||||
|
||||
ui.date().bind_value(
|
||||
filters,
|
||||
"start_on",
|
||||
forward=date.fromisoformat, # control str -> model date
|
||||
backward=date.isoformat, # model date -> control str
|
||||
)
|
||||
|
||||
ui.select(
|
||||
options={"10": "10 rows", "25": "25 rows", "50": "50 rows"},
|
||||
label="Page size",
|
||||
).bind_value(
|
||||
filters,
|
||||
"page_size",
|
||||
forward=int, # control str -> model int
|
||||
backward=str, # model int -> control str
|
||||
)
|
||||
|
||||
ui.label().bind_text_from(
|
||||
filters,
|
||||
"start_on",
|
||||
backward=lambda value: f"Starting {value:%d %B %Y}",
|
||||
)
|
||||
```
|
||||
|
||||
At binding time, NiceGUI runs `backward` from the model to each control. Later control changes run `forward` toward the model. Assigning a new model value runs `backward` again.
|
||||
|
||||
## Example: Follow A Constrained Value
|
||||
|
||||
A select and an [`Enum`](https://docs.python.org/3/library/enum.html) provide a second visible representation change. Because the select only emits known values, this example keeps attention on propagation rather than parse failures.
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
from nicegui import binding, ui
|
||||
|
||||
|
||||
class SortOrder(Enum):
|
||||
NEWEST = "newest"
|
||||
OLDEST = "oldest"
|
||||
|
||||
|
||||
@binding.bindable_dataclass
|
||||
class ResultsState:
|
||||
sort_order: SortOrder = SortOrder.NEWEST
|
||||
|
||||
|
||||
state = ResultsState()
|
||||
|
||||
ui.select(
|
||||
options={"newest": "Newest first", "oldest": "Oldest first"},
|
||||
label="Sort order",
|
||||
).bind_value(
|
||||
state,
|
||||
"sort_order",
|
||||
forward=SortOrder, # control str -> model SortOrder
|
||||
backward=lambda value: value.value, # model SortOrder -> control str
|
||||
)
|
||||
```
|
||||
|
||||
The concrete types are incidental. The same graph mechanics apply whenever `forward` and `backward` map two representations.
|
||||
|
||||
## Dataclass Modeling Rules
|
||||
|
||||
- Use `field(default_factory=...)` for mutable defaults.
|
||||
- Avoid `frozen=True` for models edited by UI controls.
|
||||
- Use `slots=True` only after confirming compatibility with inheritance and extension needs.
|
||||
- Keep UI-editable fields explicit and typed.
|
||||
- Use `field(default_factory=...)` for mutable defaults and time-dependent defaults.
|
||||
- NiceGUI `3.16.0` rejects `frozen=True` and `slots=True` in `bindable_dataclass`; both conflict with its descriptor storage model.
|
||||
- Keep UI-editable fields explicit and typed. Dataclass annotations describe intent but do not enforce runtime types; the control or transform must produce the right type.
|
||||
- Replace collections instead of mutating them in place.
|
||||
|
||||
```python
|
||||
from dataclasses import field
|
||||
@@ -55,28 +187,36 @@ from nicegui import binding
|
||||
class Filters:
|
||||
query: str = ""
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
filters = Filters()
|
||||
filters.tags = [*filters.tags, "python"] # unequal assignment propagates
|
||||
```
|
||||
|
||||
Calling `filters.tags.append("python")` bypasses the descriptor. Mutating first and then assigning an equal copy also does not propagate because `BindableProperty` compares with `!=` and returns when values are equal.
|
||||
|
||||
## Nested Structures
|
||||
|
||||
NiceGUI supports tuple paths for nested data structures.
|
||||
Tuple paths support nested mappings and object attributes:
|
||||
|
||||
```python
|
||||
from nicegui import ui
|
||||
|
||||
data = {"user": {"name": "Ada"}}
|
||||
|
||||
ui.input("Name").bind_value(data, ("user", "name"))
|
||||
ui.label().bind_text_from(data, ("user", "name"))
|
||||
```
|
||||
|
||||
Keep nested dataclass updates explicit and predictable at the field level.
|
||||
A tuple path is checked as an active link. When a nested object is itself a bindable dataclass, bind its owning object directly to preserve immediate descriptor-driven propagation:
|
||||
|
||||
## Strictness And Refactor Safety
|
||||
```python
|
||||
ui.input("City").bind_value(profile.address, "city")
|
||||
```
|
||||
|
||||
- Object attributes are checked by default.
|
||||
- Dictionary keys are not checked by default.
|
||||
- Use `strict=True` when missing dictionary keys should produce warnings.
|
||||
If `profile.address` is replaced later, rebuild that direct binding or bind through the root tuple path and accept active-link polling.
|
||||
|
||||
## Strictness And Missing Paths
|
||||
|
||||
NiceGUI `3.16.0` checks object attributes by default and does not check mapping keys by default. A failed strict check raises `AttributeError` or `KeyError` while the binding is being created.
|
||||
|
||||
```python
|
||||
from nicegui import app, ui
|
||||
@@ -84,17 +224,23 @@ from nicegui import app, ui
|
||||
ui.input().bind_value(app.storage.user, "display_name", strict=True)
|
||||
```
|
||||
|
||||
Use `strict=False` for an intentionally lazy object attribute and `strict=True` when a mapping key must already exist. On assignment, NiceGUI can create missing intermediate dictionaries, but it cannot create missing intermediate object attributes.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- In-place mutation may not produce immediate UI synchronization. Assign the updated value back to the bound field.
|
||||
- Heavy binding transforms can degrade refresh performance. Move expensive work to event handlers or services.
|
||||
- State shared across unrelated pages or users can leak data. Scope models to the appropriate page, client, or user context.
|
||||
- Do not put logging, I/O, model mutation, notifications, or other side effects in transforms. Propagation order and call count are implementation details.
|
||||
- Do not use a transform as the validation boundary for free-form text. A raised parser exception interrupts propagation.
|
||||
- Do not mutate a bound collection in place. Construct and assign a different value.
|
||||
- Do not assume a nested tuple path gets the same immediate behavior as binding directly to a bindable leaf object.
|
||||
- Scope bindable models to the appropriate page, client, or user. A module-global model shares state across users.
|
||||
- Remove bindings with NiceGUI's public element lifecycle rather than retaining discarded elements or models indefinitely.
|
||||
|
||||
## Version Checks
|
||||
|
||||
- `bindable_dataclass` was added in NiceGUI 2.11.0.
|
||||
- Depth-first binding propagation was documented in NiceGUI 2.16.0.
|
||||
- Binding `strict` behavior was documented in NiceGUI 3.0.0.
|
||||
- Tuple paths for nested properties were documented in NiceGUI 3.10.0.
|
||||
- `bindable_dataclass` was added in NiceGUI `2.11.0`.
|
||||
- Depth-first binding propagation changed in NiceGUI `2.16.0`.
|
||||
- Binding strictness controls were added in NiceGUI `3.0.0`.
|
||||
- Tuple paths for nested properties were added in NiceGUI `3.10.0`.
|
||||
- NiceGUI `3.16.0` supports `bindable_fields` and rejects `slots=True` and `frozen=True`.
|
||||
|
||||
Verify these behaviors against the NiceGUI version pinned by the target project.
|
||||
Verify the installed NiceGUI source and documentation when any of these mechanics affect application correctness.
|
||||
Reference in New Issue
Block a user