dioxus-field
A form-library-agnostic field convention for Dioxus widget libraries.
dioxus-field lets widget libraries accept reactive values, change callbacks, and interaction
commits without depending on a form library or prescribing rendered controls.
Features
- Two binding levels: use a dependency-free
value/on_change/on_commitprop trio or a reactiveBindingthat preserves the origin of each write. - Field metadata: resolve signal-backed accessibility and interaction state from explicit props, Field Context, or standalone state.
- Control attributes: derive a control's
id,name, state, and ARIA references in one call, spelled the way the rendered element accepts them. - Headless field parts: render unstyled fields, labels, descriptions, and errors with coordinated ARIA attributes and focus requests.
- Conformance testing: verify widget registries through reusable probes without a browser renderer or form library.
Quick Start
Provide a normal Dioxus signal through a Field to a field-shaped widget:
use *;
use Field;
The complete field example adds metadata, labels, descriptions, errors, and derived accessibility attributes.
On Dioxus 0.7.10, forward listeners through an explicit attributes: vec![...] collection or an
explicit Option<EventHandler<_>> prop. Bare listener props passed through extends are not yet a
safe forwarding mechanism, and duplicate listeners on one element silently keep the first. Upstream
tracks this in DioxusLabs/dioxus#4019, resolved
on main by DioxusLabs/dioxus#5554 for the 0.8
release line.
Control Attributes
A field-aware control resolves its metadata once, then asks for the attributes its element should carry:
use ;
let meta = use_field_meta;
let attributes = meta.attributes_for;
Explicit props win over Field Context, which wins over standalone state. Resolution happens before any attribute is built, so an overridden state is never emitted twice and the control never filters the result.
FieldSurface carries one axis per attribute — required, disabled, validity, and name —
because their validity lattices disagree: native disabled is legal on a button where native
required is not, and name is not a valid attribute on the div a radio group roots on. Presets
cover the common elements: NATIVE for input / textarea / select, BUTTON_WIDGET for
button[role=checkbox|switch], and ARIA_WIDGET for div[role=radiogroup].
The returned vector is sorted by attribute name and holds at most one entry per name. The sort is
what dioxus-core requires of any spread — its attribute diff is a sorted merge-join, so an
unsorted spread makes a later render drop attributes that did not change. The single entry per name
guards the neighbouring failure, where a duplicate that drops to one deletes the attribute outright.
To combine that with a widget's own attributes, hand both to merge_attributes:
use merge_attributes;
let merged = merge_attributes;
Groups resolve last-wins, ordered weakest to strongest, and the result keeps the sort and the
one-entry-per-name guarantee. class is the exception: values are concatenated weakest-first, so a
widget's own classes survive a caller's.
Pass ordered groups rather than one pre-concatenated list: merging the metadata into the explicit
props before the call moves the widget's base attributes past both, so base silently outranks an
explicit name or required it was meant to lose to. To replace a value the metadata supplied, set
the matching override on FieldControlOptions instead of adding a second entry. Widgets already
merging through merge_attributes in dioxus-primitives do not need this one — that helper also
sorts, deduplicates, and concatenates class.
Conformance Testing
Widget registries can use the public dioxus_field::testing module from ordinary integration tests;
no browser renderer or form library is required. The convention has two conformance levels, and a
registry states which one each widget meets:
- Trio-conformant: the widget honors the
value/on_change/on_commitprop trio plus attribute spread, with no dependency on this crate. Applicable tests are commit ordering and change origin; trio-only widgets imply the user origin. - Field-aware: the widget additionally resolves the Field Context. Applicable tests are the three
resolution-precedence assertions, the focus round-trip, and part-id registration. Create the probe
outside the
VirtualDom, obtain its Dioxus callbacks while rendering the test component, drive the registry component through its normal interaction path, then call the assertion after rendering.
Keep these six named tests in every registry:
| Test | Kit API | Registry adapter responsibility |
|---|---|---|
commit_is_synchronously_observable_before_submit_handling_runs |
CommitOrderProbe |
Wire on_commit() to the widget commit path and on_submit() to the containing submit handler. |
writes_carry_their_change_origin |
ChangeOriginProbe |
Give the produced binding to the widget and drive user and programmatic writes. |
binding_resolution_precedence_holds_for_values_and_meta_flags |
assert_binding_resolution_precedence, assert_meta_resolution_precedence, assert_meta_flag_precedence |
Exercise explicit, Field Context, and internal sources; report flags from actual rendered state or attributes. |
focus_request_round_trips_to_the_widget_control |
FocusRoundTripProbe |
Register on_focus() through the widget's normal focus registration and request focus through Field Context. |
a_focus_request_does_not_move_focus_while_the_control_is_disabled |
FocusRoundTripProbe::assert_focus_not_moved |
Request focus while the widget is disabled. A disabled control focuses nothing rather than handing focus to a proxy element. |
error_and_description_ids_appear_on_mount_and_vanish_on_drop |
assert_field_part_ids |
Mount and drop the registry's description and error parts around the same FieldMeta. |
The test adapter is intentionally registry-owned. It may dispatch DOM events or expose the same handlers the rendered control uses, but it should not reproduce binding or metadata resolution in test-only code. This keeps the assertions shared while allowing checkbox, select, slider, and other widgets to retain their native interaction semantics.
The testing module documentation links to a runnable interaction-probe adapter, and
tests/conformance.rs is the reference implementation exercising all six tests against a minimal
conforming widget.
Development
Run the local Rust checks with:
cargo fmt --all --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all-targets
cargo test --locked --doc
RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps
Run the same repository gate used by CI with:
dagger check
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.