dioxus_field/lib.rs
1//! A form-library-agnostic field convention for Dioxus.
2//!
3//! Use this crate to connect form-library-owned values and metadata to field-shaped widgets without
4//! coupling the widget library to a form implementation. [`Binding`] is the upper-level reactive
5//! contract, including independent Commit and Focus Exit reports. Widget registries that do not
6//! depend on this crate can instead accept separate `value`, `on_change`, and `on_commit` props
7//! matching the lower-level [`BindingPropTrio`] contract.
8//!
9//! # Quick start
10//!
11//! ```rust
12//! use dioxus::prelude::*;
13//! use dioxus_field::Field;
14//!
15//! fn app() -> Element {
16//! let mut name = use_signal(String::new);
17//!
18//! rsx! {
19//! Field { context: name,
20//! input {
21//! value: name,
22//! oninput: move |event| name.set(event.value()),
23//! }
24//! }
25//! }
26//! }
27//! ```
28
29use std::{
30 any::Any,
31 cell::{Cell, RefCell},
32 fmt,
33 rc::Rc,
34};
35
36use dioxus::prelude::{Props, dioxus_elements, rsx};
37use dioxus_core::{
38 Attribute, AttributeValue, Callback, Element, current_scope_id, has_context, provide_context,
39 try_consume_context, use_hook,
40};
41use dioxus_hooks::{use_effect, use_reactive, use_signal};
42use dioxus_signals::{ReadSignal, ReadableExt, Signal, WritableExt};
43
44pub mod testing;
45
46/// Initial presentation metadata for one field-shaped value.
47///
48/// `invalid: None` derives invalidity from whether `errors` is empty. Setting it to `Some` keeps
49/// invalidity independently controlled by the metadata producer.
50#[allow(
51 clippy::struct_excessive_bools,
52 reason = "these independent presentation flags have no invalid combinations"
53)]
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
55pub struct FieldMetaValues {
56 /// The rendered control's element id.
57 pub id: Option<Rc<str>>,
58 /// The rendered control's name.
59 pub name: Option<Rc<str>>,
60 /// Whether the field is required according to its producer.
61 pub required: bool,
62 /// Whether the field is disabled according to its producer.
63 pub disabled: bool,
64 /// An explicit invalid state, or `None` to derive it from `errors`.
65 pub invalid: Option<bool>,
66 /// Pre-rendered error text.
67 pub errors: Vec<Rc<str>>,
68 /// Whether the field is touched according to its producer.
69 pub touched: bool,
70 /// Whether the field is dirty according to its producer.
71 pub dirty: bool,
72}
73
74/// Explicit state that wins over the resolved metadata's own state.
75///
76/// This is the whole override set for a field part, and the state subset of the control path
77/// carried by [`FieldControlOptions`]. A `None` field defers to the metadata.
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub struct FieldStateOverrides {
80 /// Overrides the metadata's invalid state when present.
81 pub invalid: Option<bool>,
82 /// Overrides the metadata's disabled state when present.
83 pub disabled: Option<bool>,
84 /// Overrides the metadata's required state when present.
85 pub required: Option<bool>,
86}
87
88/// How one rendered element spells a field attribute that has both a native and an ARIA form.
89///
90/// The question this answers is *attribute applicability* — does this element accept this
91/// attribute — not which accessibility spelling reads better. Only the widget knows the element it
92/// renders and the role that element carries, so the widget supplies it.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum AttributeSurface {
96 /// Emit the native HTML attribute, such as `required` or `disabled`.
97 #[default]
98 Native,
99 /// Emit the ARIA attribute, such as `aria-required` or `aria-disabled`.
100 Aria,
101 /// Emit neither spelling.
102 ///
103 /// Use this where the attribute is invalid on the rendered element and no ARIA spelling
104 /// applies to its role either.
105 Omit,
106}
107
108/// How one rendered element exposes validity.
109///
110/// Validity has no native spelling, so this axis has no `Native` variant. It gates `aria-invalid`
111/// and `aria-errormessage` together, since a validity reference without a validity state is not
112/// meaningful.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114#[non_exhaustive]
115pub enum ValiditySurface {
116 /// Emit `aria-invalid`, and `aria-errormessage` while invalid.
117 #[default]
118 Aria,
119 /// Emit neither, for roles where `aria-invalid` is unsupported or deprecated.
120 Omit,
121}
122
123/// Whether one rendered element accepts the `name` attribute.
124///
125/// `name` has no ARIA spelling, so this axis has no `Aria` variant.
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
127#[non_exhaustive]
128pub enum NameSurface {
129 /// Emit `name`.
130 #[default]
131 Native,
132 /// Emit nothing.
133 ///
134 /// Controls rooted on a `div` need this: `name` is not a valid attribute there, and such
135 /// controls do not participate in native form submission.
136 Omit,
137}
138
139/// How one rendered element spells its field state, one axis per attribute.
140///
141/// The axes are independent because their validity lattices disagree pairwise: native `disabled`
142/// is legal on a `button` where native `required` is not, and `aria-invalid` is unsupported on
143/// some roles where `aria-disabled` is fine. A single index over all of them cannot describe any
144/// element correctly.
145///
146/// The `data-*` state attributes are outside this type. They are valid on every element, so
147/// [`FieldMeta::attributes_for`] always emits them regardless of the surface — an `Omit` axis
148/// suppresses only the attribute the element cannot carry, never the styling hook.
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
150pub struct FieldSurface {
151 /// How the element spells its required state.
152 pub required: AttributeSurface,
153 /// How the element spells its disabled state.
154 pub disabled: AttributeSurface,
155 /// How the element spells its validity.
156 pub validity: ValiditySurface,
157 /// Whether the element carries a `name`.
158 pub name: NameSurface,
159}
160
161impl FieldSurface {
162 /// `input`, `textarea`, and `select` — every axis that *has* a native spelling uses it, and
163 /// this is the default. Validity stays ARIA, since no element spells validity natively.
164 pub const NATIVE: Self = Self {
165 required: AttributeSurface::Native,
166 disabled: AttributeSurface::Native,
167 validity: ValiditySurface::Aria,
168 name: NameSurface::Native,
169 };
170
171 /// `button[role=checkbox|switch]` — native `disabled` and `name` are legal on a `button`,
172 /// native `required` is not.
173 pub const BUTTON_WIDGET: Self = Self {
174 required: AttributeSurface::Aria,
175 disabled: AttributeSurface::Native,
176 validity: ValiditySurface::Aria,
177 name: NameSurface::Native,
178 };
179
180 /// `div[role=radiogroup]` — no native attribute applies, and a `div` carries no `name`.
181 ///
182 /// A control rooted on `role=group` should start here and set `validity` to
183 /// [`ValiditySurface::Omit`], since `aria-invalid` is deprecated on that role.
184 pub const ARIA_WIDGET: Self = Self {
185 required: AttributeSurface::Aria,
186 disabled: AttributeSurface::Aria,
187 validity: ValiditySurface::Aria,
188 name: NameSurface::Omit,
189 };
190}
191
192/// Everything a field-aware control tells [`FieldMeta::attributes_for`] about itself.
193///
194/// Overrides are resolved before any attribute is built, so an overridden state is never emitted
195/// twice and never has to be filtered back out of the result.
196///
197/// ```rust
198/// # use std::rc::Rc;
199/// # use dioxus_field::{FieldControlOptions, FieldSurface};
200/// let options = FieldControlOptions::new()
201/// .required(Some(true))
202/// .name(Some(Rc::from("terms")))
203/// .surface(FieldSurface::BUTTON_WIDGET);
204/// ```
205#[derive(Clone, Debug, Default, PartialEq)]
206pub struct FieldControlOptions {
207 state: FieldStateOverrides,
208 id: Option<Rc<str>>,
209 name: Option<Rc<str>>,
210 surface: FieldSurface,
211}
212
213impl FieldControlOptions {
214 /// Creates options that override nothing and render onto [`FieldSurface::NATIVE`].
215 #[must_use]
216 pub fn new() -> Self {
217 Self::default()
218 }
219
220 /// Replaces the whole state override set.
221 #[must_use]
222 pub fn state(mut self, state: FieldStateOverrides) -> Self {
223 self.state = state;
224 self
225 }
226
227 /// Overrides the metadata's invalid state.
228 #[must_use]
229 pub fn invalid(mut self, invalid: Option<bool>) -> Self {
230 self.state.invalid = invalid;
231 self
232 }
233
234 /// Overrides the metadata's disabled state.
235 #[must_use]
236 pub fn disabled(mut self, disabled: Option<bool>) -> Self {
237 self.state.disabled = disabled;
238 self
239 }
240
241 /// Overrides the metadata's required state.
242 #[must_use]
243 pub fn required(mut self, required: Option<bool>) -> Self {
244 self.state.required = required;
245 self
246 }
247
248 /// Overrides the metadata's control id.
249 ///
250 /// This replaces the emitted value; it does not suppress the attribute. `id` is a global
251 /// attribute, so there is no element that must not carry one.
252 #[must_use]
253 pub fn id(mut self, id: Option<Rc<str>>) -> Self {
254 self.id = id;
255 self
256 }
257
258 /// Overrides the metadata's control name.
259 ///
260 /// This replaces the emitted value. To suppress the attribute on an element that cannot carry
261 /// it, set [`FieldSurface::name`] to [`NameSurface::Omit`] instead.
262 #[must_use]
263 pub fn name(mut self, name: Option<Rc<str>>) -> Self {
264 self.name = name;
265 self
266 }
267
268 /// Declares how the rendered element spells each field attribute.
269 #[must_use]
270 pub fn surface(mut self, surface: FieldSurface) -> Self {
271 self.surface = surface;
272 self
273 }
274}
275
276/// Signal-backed presentation metadata for one field-shaped value.
277///
278/// The flag meanings are producer-defined. This type does not track an initial value or classify
279/// validity. Error strings are already formatted for display before they cross this boundary.
280#[derive(Clone, Copy, PartialEq)]
281pub struct FieldMeta {
282 id: Signal<Option<Rc<str>>>,
283 fallback_id: Signal<Rc<str>>,
284 name: Signal<Option<Rc<str>>>,
285 required: Signal<bool>,
286 disabled: Signal<bool>,
287 invalid: Signal<Option<bool>>,
288 errors: Signal<Vec<Rc<str>>>,
289 touched: Signal<bool>,
290 dirty: Signal<bool>,
291 registered_ids: Signal<RegisteredIds>,
292}
293
294impl fmt::Debug for FieldMeta {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 let id = self
297 .id
298 .peek()
299 .clone()
300 .unwrap_or_else(|| self.fallback_id.peek().clone());
301
302 f.debug_struct("FieldMeta")
303 .field("id", &id)
304 .field("name", &*self.name.peek())
305 .field("required", &*self.required.peek())
306 .field("disabled", &*self.disabled.peek())
307 .field("invalid", &*self.invalid.peek())
308 .field("errors", &*self.errors.peek())
309 .field("touched", &*self.touched.peek())
310 .field("dirty", &*self.dirty.peek())
311 .finish_non_exhaustive()
312 }
313}
314
315impl FieldMeta {
316 /// Returns the rendered control id.
317 ///
318 /// Metadata always carries an id. When its producer supplies none,
319 /// [`use_field_meta_state`] generates one that is stable for the owning scope's lifetime, so
320 /// [`Label`]'s `for`, `aria-labelledby`, `aria-describedby`, and `aria-errormessage` all
321 /// resolve instead of silently leaving the control unnamed.
322 pub fn id(&self) -> Rc<str> {
323 (self.id)().unwrap_or_else(|| (self.fallback_id)())
324 }
325
326 /// Replaces the rendered control id, or restores the generated fallback with `None`.
327 pub fn set_id(&mut self, id: Option<Rc<str>>) {
328 self.id.set(id);
329 }
330
331 /// Returns the rendered control name.
332 pub fn name(&self) -> Option<Rc<str>> {
333 (self.name)()
334 }
335
336 /// Replaces the rendered control name.
337 pub fn set_name(&mut self, name: Option<Rc<str>>) {
338 self.name.set(name);
339 }
340
341 /// Returns whether the field is required.
342 pub fn required(&self) -> bool {
343 (self.required)()
344 }
345
346 /// Replaces the producer-defined required state.
347 pub fn set_required(&mut self, required: bool) {
348 self.required.set(required);
349 }
350
351 /// Returns whether the field is disabled.
352 pub fn disabled(&self) -> bool {
353 (self.disabled)()
354 }
355
356 /// Replaces the producer-defined disabled state.
357 pub fn set_disabled(&mut self, disabled: bool) {
358 self.disabled.set(disabled);
359 }
360
361 /// Returns whether the field is invalid.
362 ///
363 /// An explicit invalid state wins; otherwise invalidity is derived from whether errors exist.
364 pub fn invalid(&self) -> bool {
365 (self.invalid)().unwrap_or_else(|| !(self.errors)().is_empty())
366 }
367
368 /// Sets an explicit invalid state, or restores error-derived invalidity with `None`.
369 pub fn set_invalid(&mut self, invalid: Option<bool>) {
370 self.invalid.set(invalid);
371 }
372
373 /// Returns the pre-rendered error text.
374 pub fn errors(&self) -> Vec<Rc<str>> {
375 (self.errors)()
376 }
377
378 /// Replaces the pre-rendered error text.
379 pub fn set_errors(&mut self, errors: Vec<Rc<str>>) {
380 self.errors.set(errors);
381 }
382
383 /// Returns whether the field is touched.
384 pub fn touched(&self) -> bool {
385 (self.touched)()
386 }
387
388 /// Replaces the producer-defined touched state.
389 pub fn set_touched(&mut self, touched: bool) {
390 self.touched.set(touched);
391 }
392
393 /// Returns whether the field is dirty.
394 pub fn dirty(&self) -> bool {
395 (self.dirty)()
396 }
397
398 /// Replaces the producer-defined dirty state.
399 pub fn set_dirty(&mut self, dirty: bool) {
400 self.dirty.set(dirty);
401 }
402
403 /// Registers a label element id until the returned registration is dropped.
404 ///
405 /// Registered label ids reach the control through `aria-labelledby`, which is the only naming
406 /// path available to a control rooted on an element `<label for>` cannot address.
407 #[must_use]
408 pub fn register_label_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
409 self.register_id(RegisteredIdKind::Label, id)
410 }
411
412 /// Registers a description element id until the returned registration is dropped.
413 #[must_use]
414 pub fn register_description_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
415 self.register_id(RegisteredIdKind::Description, id)
416 }
417
418 /// Registers an error element id until the returned registration is dropped.
419 #[must_use]
420 pub fn register_error_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
421 self.register_id(RegisteredIdKind::Error, id)
422 }
423
424 /// Returns attributes for a rendered control using the metadata's own state, on a
425 /// [`FieldSurface::NATIVE`] element.
426 pub fn attributes(&self) -> Vec<Attribute> {
427 self.attributes_for(&FieldControlOptions::default())
428 }
429
430 /// Returns attributes for a rendered control, resolving explicit overrides and the element's
431 /// attribute surface.
432 ///
433 /// Overrides are resolved first and only the resolved state is emitted, so the caller never
434 /// filters the result and no attribute appears twice.
435 ///
436 /// # Guarantees
437 ///
438 /// The returned vector is **sorted by attribute name** and carries at most one entry per
439 /// attribute name and namespace.
440 ///
441 /// The sort is what `dioxus-core` requires of any spread list: its attribute diff is a sorted
442 /// merge-join, so an unsorted list makes a later render drop attributes that did not change.
443 /// The single entry per name guards the neighbouring failure, where a spread carrying one name
444 /// twice and dropping to once emits a removal, deleting an attribute the new render still has.
445 ///
446 /// To combine this with a widget's own attributes, pass both to [`merge_attributes`], which
447 /// preserves the guarantee and resolves each name last-wins. To *replace* a value the metadata
448 /// supplied, set the matching override on [`FieldControlOptions`] rather than adding a second
449 /// entry — that is what the overrides are for.
450 ///
451 /// # Emitted attributes
452 ///
453 /// - `id`, always, from the override or the metadata.
454 /// - `name`, when [`FieldSurface::name`] is [`NameSurface::Native`] and a name resolves.
455 /// - `required` or `aria-required="true"`, when required, per [`FieldSurface::required`].
456 /// - `disabled` or `aria-disabled="true"`, when disabled, per [`FieldSurface::disabled`].
457 /// - `aria-invalid`, and `aria-errormessage` while invalid, per [`FieldSurface::validity`].
458 /// `aria-errormessage` takes a single IDREF in ARIA 1.2, so it references only the first
459 /// mounted error part; every error id also reaches `aria-describedby` while invalid.
460 /// - `aria-labelledby` and `aria-describedby`, from the currently mounted parts. Both are
461 /// legal on every role in play, so neither has a surface axis.
462 /// - `data-required`, `data-disabled`, `data-invalid`, `data-touched`, and `data-dirty`, from
463 /// the resolved state, absent when false and independent of the surface.
464 pub fn attributes_for(&self, options: &FieldControlOptions) -> Vec<Attribute> {
465 let required = options.state.required.unwrap_or_else(|| self.required());
466 let disabled = options.state.disabled.unwrap_or_else(|| self.disabled());
467 let invalid = options.state.invalid.unwrap_or_else(|| self.invalid());
468 let id = options.id.clone().unwrap_or_else(|| self.id());
469 let name = options.name.clone().or_else(|| self.name());
470 let registered_ids = (self.registered_ids)();
471 let error_ids = registered_ids.ids(RegisteredIdKind::Error);
472 let mut described_by = registered_ids.ids(RegisteredIdKind::Description);
473 if invalid {
474 described_by.extend(error_ids.iter().cloned());
475 }
476 let mut attributes = Vec::new();
477
478 attributes.push(Attribute::new("id", id.to_string(), None, false));
479
480 if options.surface.name == NameSurface::Native {
481 push_optional_text(&mut attributes, "name", name);
482 }
483
484 push_surface_state(
485 &mut attributes,
486 options.surface.required,
487 ("required", "aria-required"),
488 required,
489 );
490 push_surface_state(
491 &mut attributes,
492 options.surface.disabled,
493 ("disabled", "aria-disabled"),
494 disabled,
495 );
496
497 if options.surface.validity == ValiditySurface::Aria {
498 attributes.push(Attribute::new(
499 "aria-invalid",
500 invalid.to_string(),
501 None,
502 false,
503 ));
504 if invalid {
505 push_optional_text(
506 &mut attributes,
507 "aria-errormessage",
508 error_ids.first().cloned(),
509 );
510 }
511 }
512
513 push_optional_text(
514 &mut attributes,
515 "aria-labelledby",
516 join_ids(®istered_ids.ids(RegisteredIdKind::Label)),
517 );
518 push_optional_text(&mut attributes, "aria-describedby", join_ids(&described_by));
519
520 push_state(&mut attributes, "data-required", required);
521 push_state(&mut attributes, "data-disabled", disabled);
522 push_state(&mut attributes, "data-invalid", invalid);
523 push_state(&mut attributes, "data-touched", self.touched());
524 push_state(&mut attributes, "data-dirty", self.dirty());
525
526 normalize_attributes(&mut attributes);
527
528 attributes
529 }
530
531 fn register_id(&mut self, kind: RegisteredIdKind, id: Rc<str>) -> FieldMetaIdRegistration {
532 let token = self.registered_ids.with_mut(|ids| ids.insert(kind, id));
533
534 FieldMetaIdRegistration {
535 registered_ids: self.registered_ids,
536 token,
537 }
538 }
539
540 /// Replaces producer-owned metadata values without disturbing registered part ids.
541 pub fn set_values(&mut self, values: FieldMetaValues) {
542 if *self.id.peek() != values.id {
543 self.id.set(values.id);
544 }
545 if *self.name.peek() != values.name {
546 self.name.set(values.name);
547 }
548 if *self.required.peek() != values.required {
549 self.required.set(values.required);
550 }
551 if *self.disabled.peek() != values.disabled {
552 self.disabled.set(values.disabled);
553 }
554 if *self.invalid.peek() != values.invalid {
555 self.invalid.set(values.invalid);
556 }
557 if *self.errors.peek() != values.errors {
558 self.errors.set(values.errors);
559 }
560 if *self.touched.peek() != values.touched {
561 self.touched.set(values.touched);
562 }
563 if *self.dirty.peek() != values.dirty {
564 self.dirty.set(values.dirty);
565 }
566 }
567}
568
569/// Creates signal-backed field metadata owned by the current component scope.
570///
571/// When `initial.id` is `None`, the metadata falls back to an id generated for this hook, stable
572/// for the owning scope's lifetime. Setting the id back to `None` later restores that fallback, so
573/// a control resolved through this metadata always has an id to be labelled and described by.
574pub fn use_field_meta_state(initial: FieldMetaValues) -> FieldMeta {
575 let FieldMetaValues {
576 id,
577 name,
578 required,
579 disabled,
580 invalid,
581 errors,
582 touched,
583 dirty,
584 } = initial;
585
586 FieldMeta {
587 id: use_signal(|| id),
588 fallback_id: use_signal(|| generated_id("field")),
589 name: use_signal(|| name),
590 required: use_signal(|| required),
591 disabled: use_signal(|| disabled),
592 invalid: use_signal(|| invalid),
593 errors: use_signal(|| errors),
594 touched: use_signal(|| touched),
595 dirty: use_signal(|| dirty),
596 registered_ids: use_signal(RegisteredIds::default),
597 }
598}
599
600fn use_synced_field_meta_state(values: &FieldMetaValues) -> FieldMeta {
601 let meta = use_field_meta_state(values.clone());
602 use_effect(use_reactive(values, move |values| {
603 let mut meta = meta;
604 meta.set_values(values);
605 }));
606
607 meta
608}
609
610/// A lifecycle-bound description or error id registration.
611pub struct FieldMetaIdRegistration {
612 registered_ids: Signal<RegisteredIds>,
613 token: u64,
614}
615
616impl fmt::Debug for FieldMetaIdRegistration {
617 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
618 f.debug_struct("FieldMetaIdRegistration")
619 .field("token", &self.token)
620 .finish_non_exhaustive()
621 }
622}
623
624impl Drop for FieldMetaIdRegistration {
625 fn drop(&mut self) {
626 self.registered_ids
627 .with_mut(|ids| ids.entries.retain(|entry| entry.token != self.token));
628 }
629}
630
631#[derive(Clone, Copy, PartialEq, Eq)]
632enum RegisteredIdKind {
633 Label,
634 Description,
635 Error,
636}
637
638#[derive(Clone, Default, PartialEq, Eq)]
639struct RegisteredIds {
640 next_token: u64,
641 entries: Vec<RegisteredId>,
642}
643
644impl RegisteredIds {
645 fn insert(&mut self, kind: RegisteredIdKind, id: Rc<str>) -> u64 {
646 let token = self.next_token;
647 self.next_token += 1;
648 self.entries.push(RegisteredId { token, kind, id });
649
650 token
651 }
652
653 /// Returns the ids of one kind in registration order, which is the order ARIA id references
654 /// are rendered in.
655 fn ids(&self, kind: RegisteredIdKind) -> Vec<Rc<str>> {
656 self.entries
657 .iter()
658 .filter(|entry| entry.kind == kind)
659 .map(|entry| Rc::clone(&entry.id))
660 .collect()
661 }
662}
663
664fn join_ids(ids: &[Rc<str>]) -> Option<Rc<str>> {
665 (!ids.is_empty()).then(|| {
666 Rc::from(
667 ids.iter()
668 .map(AsRef::as_ref)
669 .collect::<Vec<_>>()
670 .join(" ")
671 .as_str(),
672 )
673 })
674}
675
676/// A per-scope counter that keeps generated ids unique within one component.
677#[derive(Clone)]
678struct GeneratedIdCounter(Rc<Cell<u64>>);
679
680/// Generates an id unique to the calling scope and to this call's position within it.
681///
682/// Call this only from a hook initializer so the value is computed once and stays stable for the
683/// scope's lifetime. The counter lives in the scope's own context, which keeps generated ids
684/// deterministic for a given [`dioxus_core::VirtualDom`] rather than dependent on global state.
685fn generated_id(prefix: &str) -> Rc<str> {
686 let counter = has_context::<GeneratedIdCounter>()
687 .unwrap_or_else(|| provide_context(GeneratedIdCounter(Rc::new(Cell::new(0)))));
688 let index = counter.0.get();
689 counter.0.set(index + 1);
690
691 Rc::from(format!("dxf-{prefix}-{}-{index}", current_scope_id().0).as_str())
692}
693
694/// Resolves a field part's own element id, generating a stable one when the caller supplies none.
695fn use_part_id(explicit: Option<Rc<str>>, prefix: &'static str) -> Rc<str> {
696 let generated = use_hook(|| generated_id(prefix));
697
698 explicit.unwrap_or(generated)
699}
700
701// Dioxus cannot compose an `Into<Rc<str>>` conversion with an optional prop setter.
702#[doc(hidden)]
703pub struct OptionalRcStrPropMarker;
704
705impl<'a> dioxus_core::SuperFrom<&'a str, OptionalRcStrPropMarker> for Option<Rc<str>> {
706 fn super_from(value: &'a str) -> Self {
707 Some(Rc::from(value))
708 }
709}
710
711impl dioxus_core::SuperFrom<String, OptionalRcStrPropMarker> for Option<Rc<str>> {
712 fn super_from(value: String) -> Self {
713 Some(Rc::from(value))
714 }
715}
716
717impl dioxus_core::SuperFrom<Box<str>, OptionalRcStrPropMarker> for Option<Rc<str>> {
718 fn super_from(value: Box<str>) -> Self {
719 Some(Rc::from(value))
720 }
721}
722
723impl<'a> dioxus_core::SuperFrom<std::borrow::Cow<'a, str>, OptionalRcStrPropMarker>
724 for Option<Rc<str>>
725{
726 fn super_from(value: std::borrow::Cow<'a, str>) -> Self {
727 Some(Rc::from(value))
728 }
729}
730
731#[derive(Clone, PartialEq, Eq)]
732struct RegisteredId {
733 token: u64,
734 kind: RegisteredIdKind,
735 id: Rc<str>,
736}
737
738fn push_optional_text(attributes: &mut Vec<Attribute>, name: &'static str, value: Option<Rc<str>>) {
739 if let Some(value) = value {
740 attributes.push(Attribute::new(name, value.to_string(), None, false));
741 }
742}
743
744fn push_bool(attributes: &mut Vec<Attribute>, name: &'static str, value: bool) {
745 if value {
746 attributes.push(Attribute::new(name, true, None, false));
747 }
748}
749
750/// Pushes one state in the spelling its surface calls for, and nothing when `value` is false.
751///
752/// The native spelling is a boolean attribute; the ARIA one is `="true"`. Both are absent when
753/// false, so a selector never has to distinguish `false` from unset.
754fn push_surface_state(
755 attributes: &mut Vec<Attribute>,
756 surface: AttributeSurface,
757 (native, aria): (&'static str, &'static str),
758 value: bool,
759) {
760 match surface {
761 AttributeSurface::Native => push_bool(attributes, native, value),
762 AttributeSurface::Aria => push_state(attributes, aria, value),
763 AttributeSurface::Omit => {}
764 }
765}
766
767/// Pushes `name="true"` when `value`, and nothing otherwise.
768///
769/// Both the `data-*` state attributes and the ARIA states this crate emits use the same
770/// absent-when-false convention, so a selector never has to distinguish `false` from unset.
771fn push_state(attributes: &mut Vec<Attribute>, name: &'static str, value: bool) {
772 if value {
773 attributes.push(Attribute::new(name, "true", None, false));
774 }
775}
776
777/// Merges ordered attribute groups into one list a widget can spread.
778///
779/// Groups are resolved **last-wins**: where two groups set the same attribute name and namespace,
780/// the later group's value survives. Order the groups from weakest to strongest — for a
781/// field-aware control that is typically the metadata attributes, then the widget's own base
782/// attributes, then its explicit props, then the caller's forwarded attributes.
783///
784/// Passing ordered groups rather than one pre-concatenated list is the point. Concatenating the
785/// metadata and explicit groups before the call moves the widget's base attributes past both, so
786/// base silently outranks an explicit `name` or `required` it was meant to lose to.
787///
788/// `class` is the exception to last-wins: values are **concatenated**, weakest first, so a widget's
789/// own classes survive a caller's. Replacing there would silently unstyle the widget. Every other
790/// name, `style` included, resolves last-wins.
791///
792/// The result carries the same guarantee as [`FieldMeta::attributes_for`]: sorted by attribute
793/// name, at most one entry per name and namespace. `dioxus-core` requires the sort of any spread,
794/// and the deduplication keeps a name that appears twice and later drops to once from deleting the
795/// attribute outright.
796///
797/// Widgets already merging through `merge_attributes` in `dioxus-primitives` do not need this one.
798/// That helper also sorts, deduplicates, and concatenates `class`, so either satisfies the
799/// guarantee — but they are separate implementations, so do not assume they agree on every detail.
800///
801/// ```rust
802/// # use dioxus_core::Attribute;
803/// # use dioxus_field::merge_attributes;
804/// let merged = merge_attributes(vec![
805/// vec![Attribute::new("name", "from-meta", None, false)],
806/// vec![Attribute::new("name", "from-explicit", None, false)],
807/// ]);
808///
809/// assert_eq!(merged.len(), 1);
810/// ```
811pub fn merge_attributes(groups: Vec<Vec<Attribute>>) -> Vec<Attribute> {
812 let mut attributes = groups.into_iter().flatten().collect::<Vec<_>>();
813 normalize_attributes(&mut attributes);
814
815 attributes
816}
817
818/// Sorts by attribute name and keeps the last entry for each name and namespace.
819///
820/// `dioxus-core` diffs a spread attribute list with a sorted merge-join keyed on the attribute
821/// name, so an unsorted or duplicated list makes the next render emit removals for attributes that
822/// are still present. Every list this crate hands to `rsx!` passes through here, including after
823/// caller attributes are appended — appending last is what makes a caller's attribute win its
824/// name.
825fn normalize_attributes(attributes: &mut Vec<Attribute>) {
826 attributes.sort_by(|left, right| {
827 left.name
828 .cmp(right.name)
829 .then_with(|| left.namespace.cmp(&right.namespace))
830 });
831 attributes.dedup_by(|later, earlier| {
832 let duplicate = later.name == earlier.name && later.namespace == earlier.namespace;
833
834 if duplicate {
835 if let ("class", AttributeValue::Text(kept), AttributeValue::Text(dropped)) =
836 (later.name, &later.value, &earlier.value)
837 {
838 // Classes compose rather than replace: a widget's own classes and a caller's are
839 // both meant to apply, and last-wins here would silently unstyle the widget.
840 let combined = format!("{dropped} {kept}");
841 later.value = AttributeValue::Text(combined);
842 }
843
844 std::mem::swap(later, earlier);
845 }
846
847 duplicate
848 });
849}
850
851/// Describes whether a value write came from user interaction or application code.
852#[derive(Clone, Copy, Debug, PartialEq, Eq)]
853pub enum ChangeOrigin {
854 /// The user changed the value through a widget.
855 User,
856 /// Application code changed the value.
857 Programmatic,
858}
859
860/// A reactive, two-way binding to one field-shaped value.
861///
862/// # Interaction boundaries
863///
864/// [`Binding::commit`] and [`Binding::focus_exit`] report independent facts:
865///
866/// - **Commit** is the widget-defined end of one interaction unit. A switch click or slider release
867/// can commit while the control remains focused.
868/// - **Focus Exit** means focus left the widget's complete logical focus scope. An unchanged native
869/// text input can report Focus Exit without committing a value change.
870///
871/// ## Native control example
872///
873/// A switch click can call `write`, then `commit`, while focus remains on the switch. A later
874/// departure from the switch calls `focus_exit`. An unchanged text input that loses focus can call
875/// only `focus_exit` because there was no value interaction to commit.
876///
877/// ## Compound-widget example
878///
879/// For a compound widget, the logical focus scope includes its owned controls and popup or portal
880/// content. Moving focus from a combobox trigger into its popup does not report Focus Exit. When a
881/// selection writes and commits before focus leaves that complete scope, report the write, then
882/// Commit, then Focus Exit.
883///
884/// Equality compares the binding's producer-defined identity. Equal bindings are guaranteed to
885/// represent interchangeable read, write, Commit, and Focus Exit behavior; producers may
886/// conservatively return unequal bindings when they cannot prove that interchangeability.
887pub struct Binding<T: 'static> {
888 /// The binding's reactive value.
889 pub read: ReadSignal<T>,
890 write: Callback<(T, ChangeOrigin)>,
891 commit: Callback<()>,
892 focus_exit: Callback<()>,
893 identity: BindingIdentity,
894}
895
896impl<T: 'static> Binding<T> {
897 /// Creates a binding identified by its exact read, write, and commit handles.
898 ///
899 /// Focus Exit is a no-op unless replaced with [`Binding::with_focus_exit`] or
900 /// [`Binding::with_focus_exit_using_identity`].
901 pub fn new(
902 read: ReadSignal<T>,
903 write: Callback<(T, ChangeOrigin)>,
904 commit: Callback<()>,
905 ) -> Self {
906 Self::new_with_identity(read, write, commit, (read, write, commit))
907 }
908
909 /// Creates a binding with a producer-defined comparable identity.
910 ///
911 /// Equal identities must always represent interchangeable read, write, and commit behavior.
912 /// This constructor installs no-op Focus Exit behavior, so that behavior is interchangeable as
913 /// well. Producers that cannot prove interchangeability should use [`Binding::new`] instead.
914 pub fn new_with_identity<I>(
915 read: ReadSignal<T>,
916 write: Callback<(T, ChangeOrigin)>,
917 commit: Callback<()>,
918 identity: I,
919 ) -> Self
920 where
921 I: PartialEq + 'static,
922 {
923 Self {
924 read,
925 write,
926 commit,
927 focus_exit: Callback::new(|()| {}),
928 identity: BindingIdentity::new(identity),
929 }
930 }
931
932 /// Adds the callback invoked when focus leaves the widget's complete logical focus scope.
933 ///
934 /// This builder also incorporates the callback into binding identity, preserving the guarantee
935 /// that equal bindings have interchangeable Focus Exit behavior. It does not alter Commit or
936 /// imply any form-library blur, touched, or validation semantics.
937 #[must_use]
938 pub fn with_focus_exit(mut self, focus_exit: Callback<()>) -> Self {
939 self.identity = BindingIdentity::new((self.identity, focus_exit));
940 self.focus_exit = focus_exit;
941 self
942 }
943
944 /// Adds Focus Exit behavior covered by the binding's existing comparable identity.
945 ///
946 /// Unlike [`Binding::with_focus_exit`], this builder does not incorporate the callback's
947 /// allocation identity. Calling it asserts that bindings with equal existing identities also
948 /// have interchangeable Focus Exit behavior, in addition to interchangeable read, write, and
949 /// Commit behavior. Producers that cannot prove this must use [`Binding::with_focus_exit`]
950 /// instead.
951 ///
952 /// This builder does not alter Commit or imply any form-library blur, touched, or validation
953 /// semantics.
954 #[must_use]
955 pub fn with_focus_exit_using_identity(mut self, focus_exit: Callback<()>) -> Self {
956 self.focus_exit = focus_exit;
957 self
958 }
959
960 /// Writes a value and preserves where the change originated.
961 pub fn write(&self, value: T, origin: ChangeOrigin) {
962 self.write.call((value, origin));
963 }
964
965 /// Reports the widget-defined end of one interaction unit.
966 pub fn commit(&self) {
967 self.commit.call(());
968 }
969
970 /// Reports that focus left the widget's complete logical focus scope.
971 ///
972 /// This is independent from [`Binding::commit`]. Widgets are responsible for defining their
973 /// complete scope, including owned child controls and popup or portal content, and for
974 /// suppressing reports while focus moves within it.
975 pub fn focus_exit(&self) {
976 self.focus_exit.call(());
977 }
978
979 /// Decomposes this binding into the dependency-free widget prop contract.
980 ///
981 /// The lower-level `on_change` callback has no origin parameter, so its writes are user writes.
982 pub fn into_trio(self) -> BindingPropTrio<T> {
983 let value = self.read;
984 let on_commit = self.commit;
985 let on_change = Callback::new(move |value| self.write(value, ChangeOrigin::User));
986
987 BindingPropTrio {
988 value,
989 on_change,
990 on_commit,
991 }
992 }
993}
994
995impl<T: fmt::Debug + 'static> fmt::Debug for Binding<T> {
996 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
997 f.debug_struct("Binding")
998 .field("read", &*self.read.peek())
999 .finish_non_exhaustive()
1000 }
1001}
1002
1003impl<T: 'static> Clone for Binding<T> {
1004 fn clone(&self) -> Self {
1005 Self {
1006 read: self.read,
1007 write: self.write,
1008 commit: self.commit,
1009 focus_exit: self.focus_exit,
1010 identity: self.identity.clone(),
1011 }
1012 }
1013}
1014
1015impl<T: 'static> PartialEq for Binding<T> {
1016 fn eq(&self, other: &Self) -> bool {
1017 self.identity == other.identity
1018 }
1019}
1020
1021impl<T: 'static> From<Signal<T>> for Binding<T> {
1022 fn from(signal: Signal<T>) -> Self {
1023 let read = ReadSignal::from(signal);
1024 let mut writer = signal;
1025 let write = Callback::new(move |(value, _origin)| writer.set(value));
1026 let commit = Callback::new(|()| {});
1027
1028 Self::new_with_identity(read, write, commit, signal)
1029 }
1030}
1031
1032impl<T: 'static> From<(ReadSignal<T>, Callback<T>)> for Binding<T> {
1033 fn from((read, on_change): (ReadSignal<T>, Callback<T>)) -> Self {
1034 let write = Callback::new(move |(value, _origin)| on_change.call(value));
1035 let commit = Callback::new(|()| {});
1036
1037 Self::new_with_identity(read, write, commit, (read, on_change))
1038 }
1039}
1040
1041impl<T: 'static> From<T> for Binding<T> {
1042 fn from(value: T) -> Self {
1043 Signal::new(value).into()
1044 }
1045}
1046
1047/// A carrier for the lower-level prop contract implemented by field-shaped widgets.
1048///
1049/// Decompose this carrier into three separate props to keep a widget independent from this crate.
1050/// Since `on_change` does not carry a [`ChangeOrigin`], calling it represents a user change.
1051pub struct BindingPropTrio<T: 'static> {
1052 /// The reactive value read by the widget.
1053 pub value: ReadSignal<T>,
1054 /// The callback invoked when user interaction changes the value.
1055 pub on_change: Callback<T>,
1056 /// The callback invoked at the widget-defined end of an interaction unit.
1057 pub on_commit: Callback<()>,
1058}
1059
1060impl<T: fmt::Debug + 'static> fmt::Debug for BindingPropTrio<T> {
1061 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1062 f.debug_struct("BindingPropTrio")
1063 .field("value", &*self.value.peek())
1064 .field("on_change", &self.on_change)
1065 .field("on_commit", &self.on_commit)
1066 .finish()
1067 }
1068}
1069
1070impl<T: 'static> From<Binding<T>> for BindingPropTrio<T> {
1071 fn from(binding: Binding<T>) -> Self {
1072 binding.into_trio()
1073 }
1074}
1075
1076/// A field-scoped slot through which a widget exposes its focus behavior.
1077#[derive(Clone, Default)]
1078pub struct FocusRequest(Rc<RefCell<FocusRequestState>>);
1079
1080impl FocusRequest {
1081 /// Registers the callback used by [`FocusRequest::request`].
1082 ///
1083 /// Dropping the returned registration removes this callback without disturbing a newer
1084 /// registration in the same slot.
1085 #[must_use]
1086 pub fn register(&self, callback: Callback<()>) -> FocusRegistration {
1087 let mut state = self.0.borrow_mut();
1088 let token = state.next_token;
1089 state.next_token += 1;
1090 state.current = Some((token, callback));
1091
1092 FocusRegistration {
1093 request: self.clone(),
1094 token,
1095 }
1096 }
1097
1098 /// Requests focus from the currently registered widget.
1099 ///
1100 /// Returns whether a widget was registered to receive the request.
1101 pub fn request(&self) -> bool {
1102 let callback = self.0.borrow().current.map(|(_, callback)| callback);
1103
1104 if let Some(callback) = callback {
1105 callback.call(());
1106 true
1107 } else {
1108 false
1109 }
1110 }
1111}
1112
1113impl fmt::Debug for FocusRequest {
1114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115 f.debug_struct("FocusRequest")
1116 .field("registered", &self.0.borrow().current.is_some())
1117 .finish()
1118 }
1119}
1120
1121impl PartialEq for FocusRequest {
1122 fn eq(&self, other: &Self) -> bool {
1123 Rc::ptr_eq(&self.0, &other.0)
1124 }
1125}
1126
1127#[derive(Default)]
1128struct FocusRequestState {
1129 next_token: u64,
1130 current: Option<(u64, Callback<()>)>,
1131}
1132
1133/// A lifecycle-bound focus callback registration.
1134pub struct FocusRegistration {
1135 request: FocusRequest,
1136 token: u64,
1137}
1138
1139impl fmt::Debug for FocusRegistration {
1140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1141 f.debug_struct("FocusRegistration")
1142 .field("request", &self.request)
1143 .field("token", &self.token)
1144 .finish()
1145 }
1146}
1147
1148impl Drop for FocusRegistration {
1149 fn drop(&mut self) {
1150 let mut state = self.request.0.borrow_mut();
1151
1152 if state.current.is_some_and(|(token, _)| token == self.token) {
1153 state.current = None;
1154 }
1155 }
1156}
1157
1158/// A requested binding type did not match the binding stored in a [`FieldContext`].
1159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1160pub struct BindingTypeMismatch {
1161 actual_type_name: &'static str,
1162 requested_type_name: &'static str,
1163}
1164
1165impl BindingTypeMismatch {
1166 /// Returns the value type of the binding stored in the context.
1167 pub const fn actual_type_name(&self) -> &'static str {
1168 self.actual_type_name
1169 }
1170
1171 /// Returns the value type requested by the control.
1172 pub const fn requested_type_name(&self) -> &'static str {
1173 self.requested_type_name
1174 }
1175}
1176
1177impl fmt::Display for BindingTypeMismatch {
1178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1179 write!(
1180 f,
1181 "Field Context contains a binding for {}, but a binding for {} was requested",
1182 self.actual_type_name, self.requested_type_name
1183 )
1184 }
1185}
1186
1187impl std::error::Error for BindingTypeMismatch {}
1188
1189/// Type-erased context for one field's binding, metadata, and focus request slot.
1190///
1191/// The context itself is intentionally not generic. This lets [`use_binding`] distinguish an
1192/// absent context from a present context containing the wrong value type, and lets [`Field`]
1193/// accept any value type without becoming generic itself.
1194///
1195/// # Equality
1196///
1197/// Two contexts are equal when their bindings are equal under [`Binding`]'s identity equality and
1198/// their metadata is equal, regardless of when either context was constructed. The focus request
1199/// slot is intentionally excluded: [`Field`] pins the slot of the first context it receives for
1200/// its lifetime, so the slot carried by a context built on a later render is never observed by
1201/// descendants, and comparing it would only defeat memoization.
1202#[derive(Clone)]
1203pub struct FieldContext {
1204 binding: Option<ErasedBinding>,
1205 meta: Option<FieldMeta>,
1206 meta_values: Option<FieldMetaValues>,
1207 focus_request: FocusRequest,
1208}
1209
1210impl FieldContext {
1211 /// Creates context for a binding.
1212 pub fn new<T: 'static>(binding: Binding<T>) -> Self {
1213 Self {
1214 binding: Some(ErasedBinding::new(binding)),
1215 meta: None,
1216 meta_values: None,
1217 focus_request: FocusRequest::default(),
1218 }
1219 }
1220
1221 /// Creates context with no value binding or metadata.
1222 pub fn empty() -> Self {
1223 Self {
1224 binding: None,
1225 meta: None,
1226 meta_values: None,
1227 focus_request: FocusRequest::default(),
1228 }
1229 }
1230
1231 /// Replaces the context's value binding.
1232 #[must_use]
1233 pub fn with_binding<T: 'static>(mut self, binding: Binding<T>) -> Self {
1234 self.binding = Some(ErasedBinding::new(binding));
1235 self
1236 }
1237
1238 /// Adds signal-backed metadata to the context.
1239 #[must_use]
1240 pub fn with_meta(mut self, meta: FieldMeta) -> Self {
1241 self.meta = Some(meta);
1242 self.meta_values = None;
1243 self
1244 }
1245
1246 /// Adds producer values that [`Field`] realizes as signal-backed metadata.
1247 #[must_use]
1248 pub fn with_meta_values(mut self, values: FieldMetaValues) -> Self {
1249 self.meta = None;
1250 self.meta_values = Some(values);
1251 self
1252 }
1253
1254 /// Returns the context's metadata, when present.
1255 pub fn meta(&self) -> Option<FieldMeta> {
1256 self.meta
1257 }
1258
1259 /// Returns the context's focus request slot.
1260 ///
1261 /// [`Field`] pins the slot of the first context it receives, so producers that request focus
1262 /// through a context must keep that context stable across renders.
1263 pub fn focus_request(&self) -> FocusRequest {
1264 self.focus_request.clone()
1265 }
1266
1267 /// Resolves the context binding for `T`.
1268 ///
1269 /// # Panics
1270 ///
1271 /// Panics when the field context contains no binding or a binding for a different value type.
1272 pub fn resolve<T: 'static>(&self) -> Binding<T> {
1273 match self.try_resolve() {
1274 Ok(Some(binding)) => binding,
1275 Ok(None) => panic!("Field Context contains no value binding"),
1276 Err(mismatch) => panic!("{mismatch}"),
1277 }
1278 }
1279
1280 /// Tries to resolve the context binding for `T` without panicking.
1281 ///
1282 /// `Ok(Some(_))` contains a matching binding, `Ok(None)` means the context has no value
1283 /// binding, and `Err(_)` reports the actual and requested value types when they differ. This
1284 /// lets a control support more than one binding type while preserving absence as the signal to
1285 /// use standalone state.
1286 ///
1287 /// # Errors
1288 ///
1289 /// Returns [`BindingTypeMismatch`] when the context contains a binding whose value type is not
1290 /// `T`.
1291 ///
1292 /// ```rust
1293 /// use dioxus_field::{Binding, BindingTypeMismatch, FieldContext};
1294 ///
1295 /// #[derive(Clone, Copy)]
1296 /// enum CheckboxState {
1297 /// Checked,
1298 /// Indeterminate,
1299 /// Unchecked,
1300 /// }
1301 ///
1302 /// enum CheckboxBinding {
1303 /// State(Binding<CheckboxState>),
1304 /// Boolean(Binding<bool>),
1305 /// }
1306 ///
1307 /// fn resolve_checkbox_binding(
1308 /// context: &FieldContext,
1309 /// ) -> Result<Option<CheckboxBinding>, BindingTypeMismatch> {
1310 /// match context.try_resolve::<CheckboxState>() {
1311 /// Ok(Some(binding)) => Ok(Some(CheckboxBinding::State(binding))),
1312 /// Ok(None) => Ok(None),
1313 /// Err(_) => context
1314 /// .try_resolve::<bool>()
1315 /// .map(|binding| binding.map(CheckboxBinding::Boolean)),
1316 /// }
1317 /// }
1318 /// ```
1319 pub fn try_resolve<T: 'static>(&self) -> Result<Option<Binding<T>>, BindingTypeMismatch> {
1320 let Some(erased) = &self.binding else {
1321 return Ok(None);
1322 };
1323
1324 let binding = erased
1325 .binding
1326 .downcast_ref::<Binding<T>>()
1327 .ok_or(BindingTypeMismatch {
1328 actual_type_name: erased.value_type_name,
1329 requested_type_name: std::any::type_name::<T>(),
1330 })?;
1331
1332 Ok(Some(binding.clone()))
1333 }
1334
1335 fn with_focus_request(mut self, focus_request: FocusRequest) -> Self {
1336 self.focus_request = focus_request;
1337 self
1338 }
1339}
1340
1341impl fmt::Debug for FieldContext {
1342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1343 f.debug_struct("FieldContext")
1344 .field(
1345 "value_type_name",
1346 &self.binding.as_ref().map(|binding| binding.value_type_name),
1347 )
1348 .field("meta", &self.meta)
1349 .field("meta_values", &self.meta_values)
1350 .field("focus_request", &self.focus_request)
1351 .finish_non_exhaustive()
1352 }
1353}
1354
1355impl PartialEq for FieldContext {
1356 fn eq(&self, other: &Self) -> bool {
1357 self.binding == other.binding
1358 && self.meta == other.meta
1359 && self.meta_values == other.meta_values
1360 }
1361}
1362
1363impl<T: 'static> From<Binding<T>> for FieldContext {
1364 fn from(binding: Binding<T>) -> Self {
1365 Self::new(binding)
1366 }
1367}
1368
1369impl<T: 'static> From<Signal<T>> for FieldContext {
1370 fn from(signal: Signal<T>) -> Self {
1371 Self::new(Binding::<T>::from(signal))
1372 }
1373}
1374
1375/// A value binding erased to `dyn Any` together with the comparator for its concrete type.
1376///
1377/// The comparator is captured at erasure time so [`FieldContext`] equality can delegate to
1378/// [`Binding`]'s identity equality instead of comparing wrapper allocations.
1379#[derive(Clone)]
1380struct ErasedBinding {
1381 binding: Rc<dyn Any>,
1382 value_type_name: &'static str,
1383 eq: fn(&dyn Any, &dyn Any) -> bool,
1384}
1385
1386impl ErasedBinding {
1387 fn new<T: 'static>(binding: Binding<T>) -> Self {
1388 Self {
1389 binding: Rc::new(binding),
1390 value_type_name: std::any::type_name::<T>(),
1391 eq: |left, right| match (
1392 left.downcast_ref::<Binding<T>>(),
1393 right.downcast_ref::<Binding<T>>(),
1394 ) {
1395 (Some(left), Some(right)) => left == right,
1396 _ => false,
1397 },
1398 }
1399 }
1400}
1401
1402impl PartialEq for ErasedBinding {
1403 fn eq(&self, other: &Self) -> bool {
1404 (self.eq)(&*self.binding, &*other.binding)
1405 }
1406}
1407
1408/// Provides a binding as the current scope's [`FieldContext`].
1409///
1410/// The provided context keeps the focus request slot of the context this scope provided on an
1411/// earlier render, so widgets that memoized on that render stay registered with the slot producers
1412/// observe.
1413pub fn provide_field_context<T: 'static>(binding: Binding<T>) -> FieldContext {
1414 let mut context = FieldContext::new(binding);
1415
1416 if let Some(existing) = has_context::<FieldContext>() {
1417 context = context.with_focus_request(existing.focus_request());
1418 }
1419
1420 provide_context(context)
1421}
1422
1423/// Resolves a binding using explicit prop, [`FieldContext`], then uncontrolled-state precedence.
1424///
1425/// The internal signal hook is called regardless of which source wins so the resolution order can
1426/// change between renders without violating Dioxus's hook ordering rules.
1427///
1428/// # Panics
1429///
1430/// Panics when there is no explicit binding and the Field Context contains a binding for a value
1431/// type other than `T`.
1432pub fn use_binding<T: 'static>(explicit: Option<Binding<T>>, default: T) -> Binding<T> {
1433 let internal = use_signal(|| default);
1434
1435 if let Some(binding) = explicit {
1436 return binding;
1437 }
1438
1439 if let Some(context) = try_consume_context::<FieldContext>() {
1440 match context.try_resolve() {
1441 Ok(Some(binding)) => return binding,
1442 Ok(None) => {}
1443 Err(mismatch) => panic!("{mismatch}"),
1444 }
1445 }
1446
1447 internal.into()
1448}
1449
1450/// Resolves metadata using explicit prop, [`FieldContext`], then standalone-state precedence.
1451///
1452/// The standalone state hook is always called so the source can change between renders without
1453/// violating Dioxus's hook ordering rules.
1454pub fn use_field_meta(explicit: Option<FieldMeta>) -> FieldMeta {
1455 use_resolved_field_meta(explicit.as_ref()).0
1456}
1457
1458/// Where a part's resolved metadata came from.
1459///
1460/// A part that renders a reference *to a control* needs this: an id resolved from metadata nobody
1461/// else holds addresses no rendered element, so the reference would dangle.
1462#[derive(Clone, Copy, PartialEq, Eq)]
1463enum FieldMetaSource {
1464 /// An explicit prop or the Field Context. A control may hold the same metadata, and if the
1465 /// caller passed it deliberately, one is meant to.
1466 Shared,
1467 /// This part's own standalone state, which by construction no control is reading.
1468 Standalone,
1469}
1470
1471/// Resolves metadata as [`use_field_meta`] does, and reports which source won.
1472fn use_resolved_field_meta(explicit: Option<&FieldMeta>) -> (FieldMeta, FieldMetaSource) {
1473 let internal = use_field_meta_state(FieldMetaValues::default());
1474
1475 explicit
1476 .copied()
1477 .or_else(|| try_consume_context::<FieldContext>().and_then(|context| context.meta()))
1478 .map_or((internal, FieldMetaSource::Standalone), |meta| {
1479 (meta, FieldMetaSource::Shared)
1480 })
1481}
1482
1483/// Resolves the current [`FocusRequest`], or creates a standalone slot when no context exists.
1484pub fn use_focus_request() -> FocusRequest {
1485 let internal = use_hook(FocusRequest::default);
1486
1487 try_consume_context::<FieldContext>().map_or(internal, |context| context.focus_request())
1488}
1489
1490/// Registers a widget focus callback with the resolved [`FocusRequest`] for this component's
1491/// lifetime.
1492///
1493/// # The callback must be render-stable
1494///
1495/// Pass a callback whose identity survives a re-render — [`dioxus_hooks::use_callback`] produces
1496/// one. [`Callback::new`] does not: it allocates a fresh generational box per call and compares by
1497/// pointer identity, so calling it in a component body re-registers on **every** render. One slot
1498/// is shared by the whole field, so a widget that re-registers steals the slot from a sibling
1499/// widget that legitimately owns it, and focus ownership ends up decided by render recency rather
1500/// than by structure. Each re-registration also leaks a generational box until the component
1501/// unmounts.
1502///
1503/// # Which element to register
1504///
1505/// Register the element that actually receives focus, and let the callback do nothing when that
1506/// element cannot take focus — a disabled control should not move focus at all. Never focus a
1507/// proxy element and never blur: both hand the user's focus to something they did not ask for,
1508/// and `HTMLElement.focus()` reports success either way, so nothing downstream can detect it.
1509pub fn use_focus_registration(callback: Callback<()>) -> FocusRequest {
1510 let request = use_focus_request();
1511 let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFocusRegistration>)));
1512 let should_replace = active
1513 .borrow()
1514 .as_ref()
1515 .is_none_or(|active| active.request != request || active.callback != callback);
1516
1517 if should_replace {
1518 let registration = request.register(callback);
1519 active.borrow_mut().replace(ActiveFocusRegistration {
1520 request: request.clone(),
1521 callback,
1522 _registration: registration,
1523 });
1524 }
1525
1526 request
1527}
1528
1529struct ActiveFocusRegistration {
1530 request: FocusRequest,
1531 callback: Callback<()>,
1532 _registration: FocusRegistration,
1533}
1534
1535/// Props for the headless [`Field`] context provider.
1536#[derive(Clone, Debug, Props, PartialEq)]
1537pub struct FieldProps {
1538 /// The [`FieldContext`] provided to descendants.
1539 ///
1540 /// Accepts a [`FieldContext`], a [`Binding`], or a [`Signal`]. The prop is named after its
1541 /// payload rather than the binding it may carry, since a context can also hold only metadata.
1542 #[props(into)]
1543 pub context: FieldContext,
1544 /// Attributes forwarded to the rendered `div`.
1545 ///
1546 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1547 /// of any spread list. A forwarded attribute wins its name.
1548 #[props(extends = GlobalAttributes)]
1549 pub attributes: Vec<Attribute>,
1550 /// Field content.
1551 pub children: Element,
1552}
1553
1554/// Provides one [`FieldContext`] and renders an unstyled `div` around its children.
1555///
1556/// On Dioxus 0.7.10, pass listeners through an explicit `attributes: vec![...]` prop so listener
1557/// ordering remains visible at the call site.
1558///
1559/// # Memoization
1560///
1561/// Children authored inline in `rsx!` and inline listener attributes compare unequal on every
1562/// parent render, so a `Field` receiving either re-renders with its parent regardless of
1563/// [`FieldContext`] equality. Forwarding a received element through the `children` prop keeps it
1564/// comparable; context equality then decides whether `Field` re-renders.
1565#[allow(non_snake_case)]
1566#[allow(
1567 clippy::missing_errors_doc,
1568 reason = "Dioxus Element uses Result as its renderer protocol"
1569)]
1570pub fn Field(props: FieldProps) -> Element {
1571 let has_meta_values = props.context.meta_values.is_some();
1572 let meta_values = props.context.meta_values.clone().unwrap_or_default();
1573 let synced_meta = use_synced_field_meta_state(&meta_values);
1574 let mut context = props.context;
1575
1576 if has_meta_values {
1577 context = context.with_meta(synced_meta);
1578 }
1579
1580 let focus_request = use_hook(|| context.focus_request());
1581 provide_context(context.with_focus_request(focus_request));
1582 let mut attributes = props.attributes;
1583 normalize_attributes(&mut attributes);
1584
1585 rsx! {
1586 div { ..attributes, {props.children} }
1587 }
1588}
1589
1590/// Props for the headless [`Label`] part.
1591#[derive(Clone, Debug, Props, PartialEq)]
1592pub struct LabelProps {
1593 /// The rendered `label`'s own id, registered with the resolved metadata for this part's
1594 /// lifetime. Defaults to a generated id.
1595 ///
1596 /// The control reaches this id through `aria-labelledby`, which is the only naming path
1597 /// available to a control rooted on an element `<label for>` cannot address — `for` requires a
1598 /// labelable element, and widgets rooted on a `div` are not one.
1599 #[props(default)]
1600 pub id: Option<Rc<str>>,
1601 /// Explicit metadata, which wins over Field Context metadata.
1602 #[props(default)]
1603 pub meta: Option<FieldMeta>,
1604 /// Explicit invalid state, which wins over the metadata state.
1605 #[props(default)]
1606 pub invalid: Option<bool>,
1607 /// Explicit disabled state, which wins over the metadata state.
1608 #[props(default)]
1609 pub disabled: Option<bool>,
1610 /// Explicit required state, which wins over the metadata state.
1611 #[props(default)]
1612 pub required: Option<bool>,
1613 /// Attributes forwarded to the rendered `label`.
1614 ///
1615 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1616 /// of any spread list. A forwarded attribute wins its name.
1617 #[props(extends = GlobalAttributes)]
1618 pub attributes: Vec<Attribute>,
1619 /// Label content.
1620 pub children: Element,
1621}
1622
1623/// Renders an unstyled `label` associated with the resolved metadata's control id.
1624///
1625/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1626/// Running standalone means no control shares the metadata, so no `for` is emitted — the label
1627/// still carries its own id, which is the reference a control would reach through
1628/// `aria-labelledby`.
1629#[allow(non_snake_case)]
1630#[allow(
1631 clippy::missing_errors_doc,
1632 reason = "Dioxus Element uses Result as its renderer protocol"
1633)]
1634pub fn Label(props: LabelProps) -> Element {
1635 let (meta, source) = use_resolved_field_meta(props.meta.as_ref());
1636 let id = use_part_id(props.id, "label");
1637 use_field_meta_id_registration(&meta, RegisteredIdKind::Label, Rc::clone(&id));
1638 // Metadata the label resolved for itself alone addresses no rendered control, so pointing
1639 // `for` at its generated id would dangle. Emitting nothing is what 0.1.0 did, and is honest.
1640 let control_id = (source == FieldMetaSource::Shared).then(|| meta.id().to_string());
1641 let attributes = part_attributes(
1642 &meta,
1643 FieldStateOverrides {
1644 invalid: props.invalid,
1645 disabled: props.disabled,
1646 required: props.required,
1647 },
1648 props.attributes,
1649 );
1650
1651 rsx! {
1652 label { id: id.to_string(), r#for: control_id, ..attributes, {props.children} }
1653 }
1654}
1655
1656/// Props for the headless [`FieldDescription`] part.
1657#[derive(Clone, Debug, Props, PartialEq)]
1658pub struct FieldDescriptionProps {
1659 /// The rendered description's own id, registered with the resolved field metadata for this
1660 /// part's lifetime. Defaults to a generated id.
1661 #[props(default)]
1662 pub id: Option<Rc<str>>,
1663 /// Explicit metadata, which wins over Field Context metadata.
1664 #[props(default)]
1665 pub meta: Option<FieldMeta>,
1666 /// Explicit invalid state, which wins over the metadata state.
1667 #[props(default)]
1668 pub invalid: Option<bool>,
1669 /// Explicit disabled state, which wins over the metadata state.
1670 #[props(default)]
1671 pub disabled: Option<bool>,
1672 /// Explicit required state, which wins over the metadata state.
1673 #[props(default)]
1674 pub required: Option<bool>,
1675 /// Attributes forwarded to the rendered description `div`.
1676 ///
1677 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1678 /// of any spread list. A forwarded attribute wins its name.
1679 #[props(extends = GlobalAttributes)]
1680 pub attributes: Vec<Attribute>,
1681 /// Description content.
1682 pub children: Element,
1683}
1684
1685/// Renders an unstyled description and registers its id for `aria-describedby` chaining.
1686///
1687/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1688///
1689/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1690#[allow(non_snake_case)]
1691#[allow(
1692 clippy::missing_errors_doc,
1693 reason = "Dioxus Element uses Result as its renderer protocol"
1694)]
1695pub fn FieldDescription(props: FieldDescriptionProps) -> Element {
1696 let meta = use_field_meta(props.meta);
1697 let id = use_part_id(props.id, "description");
1698 use_field_meta_id_registration(&meta, RegisteredIdKind::Description, Rc::clone(&id));
1699 let attributes = part_attributes(
1700 &meta,
1701 FieldStateOverrides {
1702 invalid: props.invalid,
1703 disabled: props.disabled,
1704 required: props.required,
1705 },
1706 props.attributes,
1707 );
1708
1709 rsx! {
1710 div { id: id.to_string(), ..attributes, {props.children} }
1711 }
1712}
1713
1714/// Props for the headless [`FieldError`] part.
1715#[derive(Clone, Debug, Props, PartialEq)]
1716pub struct FieldErrorProps {
1717 /// The rendered error region's own id, registered with the resolved field metadata for this
1718 /// part's lifetime. Defaults to a generated id.
1719 #[props(default)]
1720 pub id: Option<Rc<str>>,
1721 /// Explicit metadata, which wins over Field Context metadata.
1722 #[props(default)]
1723 pub meta: Option<FieldMeta>,
1724 /// Explicit invalid state, which wins over the metadata state.
1725 #[props(default)]
1726 pub invalid: Option<bool>,
1727 /// Explicit disabled state used by data-state attributes.
1728 #[props(default)]
1729 pub disabled: Option<bool>,
1730 /// Explicit required state used by data-state attributes.
1731 #[props(default)]
1732 pub required: Option<bool>,
1733 /// Attributes forwarded to the rendered error `div`.
1734 ///
1735 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1736 /// of any spread list. A forwarded attribute wins its name.
1737 #[props(extends = GlobalAttributes)]
1738 pub attributes: Vec<Attribute>,
1739}
1740
1741/// Renders pre-formatted field errors in an unstyled polite live region, one element per error.
1742///
1743/// The live region stays mounted while the field is valid, holding no children. A live region that
1744/// enters the accessibility tree in the same update as its content is not announced reliably, so
1745/// the region has to exist before the first error arrives.
1746///
1747/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1748///
1749/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1750#[allow(non_snake_case)]
1751#[allow(
1752 clippy::missing_errors_doc,
1753 reason = "Dioxus Element uses Result as its renderer protocol"
1754)]
1755pub fn FieldError(props: FieldErrorProps) -> Element {
1756 let meta = use_field_meta(props.meta);
1757 let id = use_part_id(props.id, "error");
1758 use_field_meta_id_registration(&meta, RegisteredIdKind::Error, Rc::clone(&id));
1759 let invalid = props.invalid.unwrap_or_else(|| meta.invalid());
1760 let errors = if invalid { meta.errors() } else { Vec::new() };
1761 let attributes = part_attributes(
1762 &meta,
1763 FieldStateOverrides {
1764 invalid: props.invalid,
1765 disabled: props.disabled,
1766 required: props.required,
1767 },
1768 props.attributes,
1769 );
1770
1771 rsx! {
1772 div {
1773 id: id.to_string(),
1774 aria_live: "polite",
1775 ..attributes,
1776 for error in errors {
1777 div { "{error}" }
1778 }
1779 }
1780 }
1781}
1782
1783/// Builds a field part's `data-*` state attributes and appends the caller's forwarded ones.
1784///
1785/// Forwarded attributes go last, so a caller's attribute wins its name once
1786/// [`normalize_attributes`] resolves the result.
1787fn part_attributes(
1788 meta: &FieldMeta,
1789 overrides: FieldStateOverrides,
1790 forwarded: Vec<Attribute>,
1791) -> Vec<Attribute> {
1792 let required = overrides.required.unwrap_or_else(|| meta.required());
1793 let disabled = overrides.disabled.unwrap_or_else(|| meta.disabled());
1794 let invalid = overrides.invalid.unwrap_or_else(|| meta.invalid());
1795 let mut attributes = Vec::new();
1796
1797 push_state(&mut attributes, "data-required", required);
1798 push_state(&mut attributes, "data-disabled", disabled);
1799 push_state(&mut attributes, "data-invalid", invalid);
1800 push_state(&mut attributes, "data-touched", meta.touched());
1801 push_state(&mut attributes, "data-dirty", meta.dirty());
1802
1803 attributes.extend(forwarded);
1804 normalize_attributes(&mut attributes);
1805
1806 attributes
1807}
1808
1809fn use_field_meta_id_registration(meta: &FieldMeta, kind: RegisteredIdKind, id: Rc<str>) {
1810 let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFieldMetaIdRegistration>)));
1811 let should_replace = active
1812 .borrow()
1813 .as_ref()
1814 .is_none_or(|active| active.meta != *meta || active.kind != kind || active.id != id);
1815
1816 if should_replace {
1817 let mut writable_meta = *meta;
1818 let registration = writable_meta.register_id(kind, id.clone());
1819 active.borrow_mut().replace(ActiveFieldMetaIdRegistration {
1820 meta: *meta,
1821 kind,
1822 id,
1823 _registration: registration,
1824 });
1825 }
1826}
1827
1828struct ActiveFieldMetaIdRegistration {
1829 meta: FieldMeta,
1830 kind: RegisteredIdKind,
1831 id: Rc<str>,
1832 _registration: FieldMetaIdRegistration,
1833}
1834
1835#[derive(Clone)]
1836struct BindingIdentity(Rc<dyn ComparableIdentity>);
1837
1838impl BindingIdentity {
1839 fn new<I: PartialEq + 'static>(identity: I) -> Self {
1840 Self(Rc::new(identity))
1841 }
1842}
1843
1844impl PartialEq for BindingIdentity {
1845 fn eq(&self, other: &Self) -> bool {
1846 self.0.equals(other.0.as_ref())
1847 }
1848}
1849
1850trait ComparableIdentity: Any {
1851 fn equals(&self, other: &dyn ComparableIdentity) -> bool;
1852}
1853
1854impl<I: PartialEq + 'static> ComparableIdentity for I {
1855 fn equals(&self, other: &dyn ComparableIdentity) -> bool {
1856 let other = other as &dyn Any;
1857 other.downcast_ref::<I>().is_some_and(|other| self == other)
1858 }
1859}