# BoundField Architecture

## Overview

Fields in textual-wtf are split into two classes with distinct responsibilities:

- **Field** (unbound) — Immutable configuration, defined at class level, shared across all form instances. Holds label, validators, widget class, default value, and so on. Never holds runtime state.
- **BoundField** (bound) — Mutable runtime state, created per form instance. Holds the current value, widget instance, and validation errors. References the Field for configuration.

This separation means Field instances are never copied or duplicated. Each Form instantiation creates lightweight BoundField wrappers that reference the shared Field configuration.

## Value Synchronisation

BoundField maintains `_value` as a canonical store of the current Python value at all times. This is critical for correctness — it means the value is never lost, even if the widget is destroyed.

The synchronisation strategy is:

**Getter** — If a widget exists, reads from it and syncs back to `_value` before returning. This picks up any changes the user has made by typing. If no widget exists, returns `_value` directly.

**Setter** — Always writes to `_value` first. If a widget exists, syncs the value to it as well.

**Widget setter** — Before replacing or clearing an existing widget, syncs its current value back to `_value`. This preserves user input if the widget is being swapped out or removed programmatically.

Together, these three ensure that `_value` is kept current regardless of whether changes arrive from user input, programmatic assignment, or widget lifecycle events.

## Widget Lifecycle Caveat

The synchronisation described above covers all cases where widgets are created or destroyed *through the BoundField* — that is, where code explicitly sets `bound_field.widget = ...`. In that case, the widget setter syncs the departing widget's value before it is released.

However, if a widget is destroyed implicitly by Textual's DOM management (e.g. a parent container is unmounted, or the screen is replaced) without the BoundField's widget setter being called, the sync will not fire. In that scenario, `_value` will contain whatever was last synced — either by a previous read of `.value`, or by the last programmatic set.

In practice this is unlikely to cause data loss, since form values are typically read (via `get_data()`) before screens are torn down. But if you are building something where widgets may be implicitly destroyed while their values are still needed, be aware of this and ensure `.value` or `get_data()` is called beforehand.

## The Three States

A field moves through three states during its lifetime:

1. **Unbound** — The Field instance exists on the Form class. No runtime state. This is configuration only.
2. **Bound, no widget** — A BoundField has been created (at Form instantiation). `_value` holds the current value. No widget exists yet. This is the state during testing without rendering, or between form creation and `render()`.
3. **Bound, with widget** — `render()` has been called. Both `_value` and the widget hold the value, kept in sync by the getter, setter, and widget setter.

## Delegation

BoundField delegates configuration properties (label, help_text, required, disabled, validators) to the underlying Field via properties. Field-specific attributes (e.g. IntegerField.min_value, ChoiceField.choices) are delegated via `__getattr__`. This means user code does not need to know about the Field/BoundField split — `form.name.label` works regardless.
