Skip to main content

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. Widget registries that do not depend on this crate can instead accept separate `value`,
6//! `on_change`, and `on_commit` props matching the lower-level [`BindingPropTrio`] contract.
7//!
8//! # Quick start
9//!
10//! ```rust
11//! use dioxus::prelude::*;
12//! use dioxus_field::Field;
13//!
14//! fn app() -> Element {
15//!     let mut name = use_signal(String::new);
16//!
17//!     rsx! {
18//!         Field { context: name,
19//!             input {
20//!                 value: name,
21//!                 oninput: move |event| name.set(event.value()),
22//!             }
23//!         }
24//!     }
25//! }
26//! ```
27
28use std::{any::Any, cell::RefCell, fmt, rc::Rc};
29
30use dioxus::prelude::{Props, dioxus_elements, rsx};
31use dioxus_core::{
32    Attribute, Callback, Element, has_context, provide_context, try_consume_context, use_hook,
33};
34use dioxus_hooks::{use_effect, use_reactive, use_signal};
35use dioxus_signals::{ReadSignal, ReadableExt, Signal, WritableExt};
36
37pub mod testing;
38
39/// Initial presentation metadata for one field-shaped value.
40///
41/// `invalid: None` derives invalidity from whether `errors` is empty. Setting it to `Some` keeps
42/// invalidity independently controlled by the metadata producer.
43#[allow(
44    clippy::struct_excessive_bools,
45    reason = "these independent presentation flags have no invalid combinations"
46)]
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct FieldMetaValues {
49    /// The rendered control's element id.
50    pub id: Option<Rc<str>>,
51    /// The rendered control's name.
52    pub name: Option<Rc<str>>,
53    /// Whether the field is required according to its producer.
54    pub required: bool,
55    /// Whether the field is disabled according to its producer.
56    pub disabled: bool,
57    /// An explicit invalid state, or `None` to derive it from `errors`.
58    pub invalid: Option<bool>,
59    /// Pre-rendered error text.
60    pub errors: Vec<Rc<str>>,
61    /// Whether the field is touched according to its producer.
62    pub touched: bool,
63    /// Whether the field is dirty according to its producer.
64    pub dirty: bool,
65}
66
67/// Per-use overrides applied while deriving field attributes or rendering a field part.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub struct FieldMetaOverrides {
70    /// Overrides the metadata's invalid state when present.
71    pub invalid: Option<bool>,
72    /// Overrides the metadata's disabled state when present.
73    pub disabled: Option<bool>,
74}
75
76/// Signal-backed presentation metadata for one field-shaped value.
77///
78/// The flag meanings are producer-defined. This type does not track an initial value or classify
79/// validity. Error strings are already formatted for display before they cross this boundary.
80#[derive(Clone, Copy, PartialEq)]
81pub struct FieldMeta {
82    id: Signal<Option<Rc<str>>>,
83    name: Signal<Option<Rc<str>>>,
84    required: Signal<bool>,
85    disabled: Signal<bool>,
86    invalid: Signal<Option<bool>>,
87    errors: Signal<Vec<Rc<str>>>,
88    touched: Signal<bool>,
89    dirty: Signal<bool>,
90    registered_ids: Signal<RegisteredIds>,
91}
92
93impl fmt::Debug for FieldMeta {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.debug_struct("FieldMeta")
96            .field("id", &*self.id.peek())
97            .field("name", &*self.name.peek())
98            .field("required", &*self.required.peek())
99            .field("disabled", &*self.disabled.peek())
100            .field("invalid", &*self.invalid.peek())
101            .field("errors", &*self.errors.peek())
102            .field("touched", &*self.touched.peek())
103            .field("dirty", &*self.dirty.peek())
104            .finish_non_exhaustive()
105    }
106}
107
108impl FieldMeta {
109    /// Returns the rendered control id.
110    pub fn id(&self) -> Option<Rc<str>> {
111        (self.id)()
112    }
113
114    /// Replaces the rendered control id.
115    pub fn set_id(&mut self, id: Option<Rc<str>>) {
116        self.id.set(id);
117    }
118
119    /// Returns the rendered control name.
120    pub fn name(&self) -> Option<Rc<str>> {
121        (self.name)()
122    }
123
124    /// Replaces the rendered control name.
125    pub fn set_name(&mut self, name: Option<Rc<str>>) {
126        self.name.set(name);
127    }
128
129    /// Returns whether the field is required.
130    pub fn required(&self) -> bool {
131        (self.required)()
132    }
133
134    /// Replaces the producer-defined required state.
135    pub fn set_required(&mut self, required: bool) {
136        self.required.set(required);
137    }
138
139    /// Returns whether the field is disabled.
140    pub fn disabled(&self) -> bool {
141        (self.disabled)()
142    }
143
144    /// Replaces the producer-defined disabled state.
145    pub fn set_disabled(&mut self, disabled: bool) {
146        self.disabled.set(disabled);
147    }
148
149    /// Returns whether the field is invalid.
150    ///
151    /// An explicit invalid state wins; otherwise invalidity is derived from whether errors exist.
152    pub fn invalid(&self) -> bool {
153        (self.invalid)().unwrap_or_else(|| !(self.errors)().is_empty())
154    }
155
156    /// Sets an explicit invalid state, or restores error-derived invalidity with `None`.
157    pub fn set_invalid(&mut self, invalid: Option<bool>) {
158        self.invalid.set(invalid);
159    }
160
161    /// Returns the pre-rendered error text.
162    pub fn errors(&self) -> Vec<Rc<str>> {
163        (self.errors)()
164    }
165
166    /// Replaces the pre-rendered error text.
167    pub fn set_errors(&mut self, errors: Vec<Rc<str>>) {
168        self.errors.set(errors);
169    }
170
171    /// Returns whether the field is touched.
172    pub fn touched(&self) -> bool {
173        (self.touched)()
174    }
175
176    /// Replaces the producer-defined touched state.
177    pub fn set_touched(&mut self, touched: bool) {
178        self.touched.set(touched);
179    }
180
181    /// Returns whether the field is dirty.
182    pub fn dirty(&self) -> bool {
183        (self.dirty)()
184    }
185
186    /// Replaces the producer-defined dirty state.
187    pub fn set_dirty(&mut self, dirty: bool) {
188        self.dirty.set(dirty);
189    }
190
191    /// Registers a description element id until the returned registration is dropped.
192    #[must_use]
193    pub fn register_description_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
194        self.register_id(RegisteredIdKind::Description, id)
195    }
196
197    /// Registers an error element id until the returned registration is dropped.
198    #[must_use]
199    pub fn register_error_id(&mut self, id: Rc<str>) -> FieldMetaIdRegistration {
200        self.register_id(RegisteredIdKind::Error, id)
201    }
202
203    /// Returns attributes for a rendered control using the metadata's flag states.
204    pub fn attributes(&self) -> Vec<Attribute> {
205        self.attributes_with(FieldMetaOverrides::default())
206    }
207
208    /// Returns attributes for a rendered control, applying per-flag explicit overrides.
209    pub fn attributes_with(&self, overrides: FieldMetaOverrides) -> Vec<Attribute> {
210        let invalid = overrides.invalid.unwrap_or_else(|| self.invalid());
211        let disabled = overrides.disabled.unwrap_or_else(|| self.disabled());
212        let registered_ids = (self.registered_ids)();
213        let description_ids = registered_ids.joined(RegisteredIdKind::Description);
214        let error_ids = registered_ids.joined(RegisteredIdKind::Error);
215        let mut attributes = Vec::new();
216
217        push_optional_text(&mut attributes, "id", self.id());
218        push_optional_text(&mut attributes, "name", self.name());
219        push_bool(&mut attributes, "required", self.required());
220        push_bool(&mut attributes, "disabled", disabled);
221        attributes.push(Attribute::new(
222            "aria-invalid",
223            invalid.to_string(),
224            None,
225            false,
226        ));
227        push_optional_text(&mut attributes, "aria-describedby", description_ids);
228        if invalid {
229            push_optional_text(&mut attributes, "aria-errormessage", error_ids);
230        }
231        push_data_state(&mut attributes, "data-required", self.required());
232        push_data_state(&mut attributes, "data-disabled", disabled);
233        push_data_state(&mut attributes, "data-invalid", invalid);
234        push_data_state(&mut attributes, "data-touched", self.touched());
235        push_data_state(&mut attributes, "data-dirty", self.dirty());
236
237        attributes
238    }
239
240    fn register_id(&mut self, kind: RegisteredIdKind, id: Rc<str>) -> FieldMetaIdRegistration {
241        let token = self.registered_ids.with_mut(|ids| ids.insert(kind, id));
242
243        FieldMetaIdRegistration {
244            registered_ids: self.registered_ids,
245            token,
246        }
247    }
248
249    /// Replaces producer-owned metadata values without disturbing registered part ids.
250    pub fn set_values(&mut self, values: FieldMetaValues) {
251        if *self.id.peek() != values.id {
252            self.id.set(values.id);
253        }
254        if *self.name.peek() != values.name {
255            self.name.set(values.name);
256        }
257        if *self.required.peek() != values.required {
258            self.required.set(values.required);
259        }
260        if *self.disabled.peek() != values.disabled {
261            self.disabled.set(values.disabled);
262        }
263        if *self.invalid.peek() != values.invalid {
264            self.invalid.set(values.invalid);
265        }
266        if *self.errors.peek() != values.errors {
267            self.errors.set(values.errors);
268        }
269        if *self.touched.peek() != values.touched {
270            self.touched.set(values.touched);
271        }
272        if *self.dirty.peek() != values.dirty {
273            self.dirty.set(values.dirty);
274        }
275    }
276}
277
278/// Creates signal-backed field metadata owned by the current component scope.
279pub fn use_field_meta_state(initial: FieldMetaValues) -> FieldMeta {
280    let FieldMetaValues {
281        id,
282        name,
283        required,
284        disabled,
285        invalid,
286        errors,
287        touched,
288        dirty,
289    } = initial;
290
291    FieldMeta {
292        id: use_signal(|| id),
293        name: use_signal(|| name),
294        required: use_signal(|| required),
295        disabled: use_signal(|| disabled),
296        invalid: use_signal(|| invalid),
297        errors: use_signal(|| errors),
298        touched: use_signal(|| touched),
299        dirty: use_signal(|| dirty),
300        registered_ids: use_signal(RegisteredIds::default),
301    }
302}
303
304fn use_synced_field_meta_state(values: &FieldMetaValues) -> FieldMeta {
305    let meta = use_field_meta_state(values.clone());
306    use_effect(use_reactive(values, move |values| {
307        let mut meta = meta;
308        meta.set_values(values);
309    }));
310
311    meta
312}
313
314/// A lifecycle-bound description or error id registration.
315pub struct FieldMetaIdRegistration {
316    registered_ids: Signal<RegisteredIds>,
317    token: u64,
318}
319
320impl fmt::Debug for FieldMetaIdRegistration {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        f.debug_struct("FieldMetaIdRegistration")
323            .field("token", &self.token)
324            .finish_non_exhaustive()
325    }
326}
327
328impl Drop for FieldMetaIdRegistration {
329    fn drop(&mut self) {
330        self.registered_ids
331            .with_mut(|ids| ids.entries.retain(|entry| entry.token != self.token));
332    }
333}
334
335#[derive(Clone, Copy, PartialEq, Eq)]
336enum RegisteredIdKind {
337    Description,
338    Error,
339}
340
341#[derive(Clone, Default, PartialEq, Eq)]
342struct RegisteredIds {
343    next_token: u64,
344    entries: Vec<RegisteredId>,
345}
346
347impl RegisteredIds {
348    fn insert(&mut self, kind: RegisteredIdKind, id: Rc<str>) -> u64 {
349        let token = self.next_token;
350        self.next_token += 1;
351        self.entries.push(RegisteredId { token, kind, id });
352
353        token
354    }
355
356    fn joined(&self, kind: RegisteredIdKind) -> Option<Rc<str>> {
357        let ids = self
358            .entries
359            .iter()
360            .filter(|entry| entry.kind == kind)
361            .map(|entry| entry.id.as_ref())
362            .collect::<Vec<_>>();
363
364        (!ids.is_empty()).then(|| Rc::from(ids.join(" ")))
365    }
366}
367
368#[derive(Clone, PartialEq, Eq)]
369struct RegisteredId {
370    token: u64,
371    kind: RegisteredIdKind,
372    id: Rc<str>,
373}
374
375fn push_optional_text(attributes: &mut Vec<Attribute>, name: &'static str, value: Option<Rc<str>>) {
376    if let Some(value) = value {
377        attributes.push(Attribute::new(name, value.to_string(), None, false));
378    }
379}
380
381fn push_bool(attributes: &mut Vec<Attribute>, name: &'static str, value: bool) {
382    if value {
383        attributes.push(Attribute::new(name, true, None, false));
384    }
385}
386
387fn push_data_state(attributes: &mut Vec<Attribute>, name: &'static str, value: bool) {
388    if value {
389        attributes.push(Attribute::new(name, "true", None, false));
390    }
391}
392
393/// Describes whether a value write came from user interaction or application code.
394#[derive(Clone, Copy, Debug, PartialEq, Eq)]
395pub enum ChangeOrigin {
396    /// The user changed the value through a widget.
397    User,
398    /// Application code changed the value.
399    Programmatic,
400}
401
402/// A reactive, two-way binding to one field-shaped value.
403///
404/// Equality compares the binding's producer-defined identity. Equal bindings are guaranteed to
405/// represent the same read and write behavior; producers may conservatively return unequal
406/// bindings when they cannot prove that interchangeability.
407pub struct Binding<T: 'static> {
408    /// The binding's reactive value.
409    pub read: ReadSignal<T>,
410    write: Callback<(T, ChangeOrigin)>,
411    commit: Callback<()>,
412    identity: BindingIdentity,
413}
414
415impl<T: 'static> Binding<T> {
416    /// Creates a binding identified by its exact read, write, and commit handles.
417    pub fn new(
418        read: ReadSignal<T>,
419        write: Callback<(T, ChangeOrigin)>,
420        commit: Callback<()>,
421    ) -> Self {
422        Self::new_with_identity(read, write, commit, (read, write, commit))
423    }
424
425    /// Creates a binding with a producer-defined comparable identity.
426    ///
427    /// Equal identities must always represent interchangeable read, write, and commit behavior.
428    /// Producers that cannot prove interchangeability should use [`Binding::new`] instead.
429    pub fn new_with_identity<I>(
430        read: ReadSignal<T>,
431        write: Callback<(T, ChangeOrigin)>,
432        commit: Callback<()>,
433        identity: I,
434    ) -> Self
435    where
436        I: PartialEq + 'static,
437    {
438        Self {
439            read,
440            write,
441            commit,
442            identity: BindingIdentity::new(identity),
443        }
444    }
445
446    /// Writes a value and preserves where the change originated.
447    pub fn write(&self, value: T, origin: ChangeOrigin) {
448        self.write.call((value, origin));
449    }
450
451    /// Reports the widget-defined end of one interaction unit.
452    pub fn commit(&self) {
453        self.commit.call(());
454    }
455
456    /// Decomposes this binding into the dependency-free widget prop contract.
457    ///
458    /// The lower-level `on_change` callback has no origin parameter, so its writes are user writes.
459    pub fn into_trio(self) -> BindingPropTrio<T> {
460        let value = self.read;
461        let on_commit = self.commit;
462        let on_change = Callback::new(move |value| self.write(value, ChangeOrigin::User));
463
464        BindingPropTrio {
465            value,
466            on_change,
467            on_commit,
468        }
469    }
470}
471
472impl<T: fmt::Debug + 'static> fmt::Debug for Binding<T> {
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        f.debug_struct("Binding")
475            .field("read", &*self.read.peek())
476            .finish_non_exhaustive()
477    }
478}
479
480impl<T: 'static> Clone for Binding<T> {
481    fn clone(&self) -> Self {
482        Self {
483            read: self.read,
484            write: self.write,
485            commit: self.commit,
486            identity: self.identity.clone(),
487        }
488    }
489}
490
491impl<T: 'static> PartialEq for Binding<T> {
492    fn eq(&self, other: &Self) -> bool {
493        self.identity == other.identity
494    }
495}
496
497impl<T: 'static> From<Signal<T>> for Binding<T> {
498    fn from(signal: Signal<T>) -> Self {
499        let read = ReadSignal::from(signal);
500        let mut writer = signal;
501        let write = Callback::new(move |(value, _origin)| writer.set(value));
502        let commit = Callback::new(|()| {});
503
504        Self::new_with_identity(read, write, commit, signal)
505    }
506}
507
508impl<T: 'static> From<(ReadSignal<T>, Callback<T>)> for Binding<T> {
509    fn from((read, on_change): (ReadSignal<T>, Callback<T>)) -> Self {
510        let write = Callback::new(move |(value, _origin)| on_change.call(value));
511        let commit = Callback::new(|()| {});
512
513        Self::new_with_identity(read, write, commit, (read, on_change))
514    }
515}
516
517impl<T: 'static> From<T> for Binding<T> {
518    fn from(value: T) -> Self {
519        Signal::new(value).into()
520    }
521}
522
523/// A carrier for the lower-level prop contract implemented by field-shaped widgets.
524///
525/// Decompose this carrier into three separate props to keep a widget independent from this crate.
526/// Since `on_change` does not carry a [`ChangeOrigin`], calling it represents a user change.
527pub struct BindingPropTrio<T: 'static> {
528    /// The reactive value read by the widget.
529    pub value: ReadSignal<T>,
530    /// The callback invoked when user interaction changes the value.
531    pub on_change: Callback<T>,
532    /// The callback invoked at the widget-defined end of an interaction unit.
533    pub on_commit: Callback<()>,
534}
535
536impl<T: fmt::Debug + 'static> fmt::Debug for BindingPropTrio<T> {
537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538        f.debug_struct("BindingPropTrio")
539            .field("value", &*self.value.peek())
540            .field("on_change", &self.on_change)
541            .field("on_commit", &self.on_commit)
542            .finish()
543    }
544}
545
546impl<T: 'static> From<Binding<T>> for BindingPropTrio<T> {
547    fn from(binding: Binding<T>) -> Self {
548        binding.into_trio()
549    }
550}
551
552/// A field-scoped slot through which a widget exposes its focus behavior.
553#[derive(Clone, Default)]
554pub struct FocusRequest(Rc<RefCell<FocusRequestState>>);
555
556impl FocusRequest {
557    /// Registers the callback used by [`FocusRequest::request`].
558    ///
559    /// Dropping the returned registration removes this callback without disturbing a newer
560    /// registration in the same slot.
561    #[must_use]
562    pub fn register(&self, callback: Callback<()>) -> FocusRegistration {
563        let mut state = self.0.borrow_mut();
564        let token = state.next_token;
565        state.next_token += 1;
566        state.current = Some((token, callback));
567
568        FocusRegistration {
569            request: self.clone(),
570            token,
571        }
572    }
573
574    /// Requests focus from the currently registered widget.
575    ///
576    /// Returns whether a widget was registered to receive the request.
577    pub fn request(&self) -> bool {
578        let callback = self.0.borrow().current.map(|(_, callback)| callback);
579
580        if let Some(callback) = callback {
581            callback.call(());
582            true
583        } else {
584            false
585        }
586    }
587}
588
589impl fmt::Debug for FocusRequest {
590    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591        f.debug_struct("FocusRequest")
592            .field("registered", &self.0.borrow().current.is_some())
593            .finish()
594    }
595}
596
597impl PartialEq for FocusRequest {
598    fn eq(&self, other: &Self) -> bool {
599        Rc::ptr_eq(&self.0, &other.0)
600    }
601}
602
603#[derive(Default)]
604struct FocusRequestState {
605    next_token: u64,
606    current: Option<(u64, Callback<()>)>,
607}
608
609/// A lifecycle-bound focus callback registration.
610pub struct FocusRegistration {
611    request: FocusRequest,
612    token: u64,
613}
614
615impl fmt::Debug for FocusRegistration {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        f.debug_struct("FocusRegistration")
618            .field("request", &self.request)
619            .field("token", &self.token)
620            .finish()
621    }
622}
623
624impl Drop for FocusRegistration {
625    fn drop(&mut self) {
626        let mut state = self.request.0.borrow_mut();
627
628        if state.current.is_some_and(|(token, _)| token == self.token) {
629            state.current = None;
630        }
631    }
632}
633
634/// Type-erased context for one field's binding, metadata, and focus request slot.
635///
636/// The context itself is intentionally not generic. This lets [`use_binding`] distinguish an
637/// absent context from a present context containing the wrong value type, and lets [`Field`]
638/// accept any value type without becoming generic itself.
639///
640/// # Equality
641///
642/// Two contexts are equal when their bindings are equal under [`Binding`]'s identity equality and
643/// their metadata is equal, regardless of when either context was constructed. The focus request
644/// slot is intentionally excluded: [`Field`] pins the slot of the first context it receives for
645/// its lifetime, so the slot carried by a context built on a later render is never observed by
646/// descendants, and comparing it would only defeat memoization.
647#[derive(Clone)]
648pub struct FieldContext {
649    binding: Option<ErasedBinding>,
650    meta: Option<FieldMeta>,
651    meta_values: Option<FieldMetaValues>,
652    focus_request: FocusRequest,
653}
654
655impl FieldContext {
656    /// Creates context for a binding.
657    pub fn new<T: 'static>(binding: Binding<T>) -> Self {
658        Self {
659            binding: Some(ErasedBinding::new(binding)),
660            meta: None,
661            meta_values: None,
662            focus_request: FocusRequest::default(),
663        }
664    }
665
666    /// Creates context with no value binding or metadata.
667    pub fn empty() -> Self {
668        Self {
669            binding: None,
670            meta: None,
671            meta_values: None,
672            focus_request: FocusRequest::default(),
673        }
674    }
675
676    /// Replaces the context's value binding.
677    #[must_use]
678    pub fn with_binding<T: 'static>(mut self, binding: Binding<T>) -> Self {
679        self.binding = Some(ErasedBinding::new(binding));
680        self
681    }
682
683    /// Adds signal-backed metadata to the context.
684    #[must_use]
685    pub fn with_meta(mut self, meta: FieldMeta) -> Self {
686        self.meta = Some(meta);
687        self.meta_values = None;
688        self
689    }
690
691    /// Adds producer values that [`Field`] realizes as signal-backed metadata.
692    #[must_use]
693    pub fn with_meta_values(mut self, values: FieldMetaValues) -> Self {
694        self.meta = None;
695        self.meta_values = Some(values);
696        self
697    }
698
699    /// Returns the context's metadata, when present.
700    pub fn meta(&self) -> Option<FieldMeta> {
701        self.meta
702    }
703
704    /// Returns the context's focus request slot.
705    ///
706    /// [`Field`] pins the slot of the first context it receives, so producers that request focus
707    /// through a context must keep that context stable across renders.
708    pub fn focus_request(&self) -> FocusRequest {
709        self.focus_request.clone()
710    }
711
712    /// Resolves the context binding for `T`.
713    ///
714    /// # Panics
715    ///
716    /// Panics when the field context contains no binding or a binding for a different value type.
717    pub fn resolve<T: 'static>(&self) -> Binding<T> {
718        let erased = self
719            .binding
720            .as_ref()
721            .unwrap_or_else(|| panic!("Field Context contains no value binding"));
722
723        erased
724            .binding
725            .downcast_ref::<Binding<T>>()
726            .unwrap_or_else(|| {
727                panic!(
728                    "Field Context contains a binding for {}, but a binding for {} was requested",
729                    erased.value_type_name,
730                    std::any::type_name::<T>()
731                )
732            })
733            .clone()
734    }
735
736    fn try_resolve<T: 'static>(&self) -> Option<Binding<T>> {
737        self.binding.as_ref().map(|_| self.resolve())
738    }
739
740    fn with_focus_request(mut self, focus_request: FocusRequest) -> Self {
741        self.focus_request = focus_request;
742        self
743    }
744}
745
746impl fmt::Debug for FieldContext {
747    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
748        f.debug_struct("FieldContext")
749            .field(
750                "value_type_name",
751                &self.binding.as_ref().map(|binding| binding.value_type_name),
752            )
753            .field("meta", &self.meta)
754            .field("meta_values", &self.meta_values)
755            .field("focus_request", &self.focus_request)
756            .finish_non_exhaustive()
757    }
758}
759
760impl PartialEq for FieldContext {
761    fn eq(&self, other: &Self) -> bool {
762        self.binding == other.binding
763            && self.meta == other.meta
764            && self.meta_values == other.meta_values
765    }
766}
767
768impl<T: 'static> From<Binding<T>> for FieldContext {
769    fn from(binding: Binding<T>) -> Self {
770        Self::new(binding)
771    }
772}
773
774impl<T: 'static> From<Signal<T>> for FieldContext {
775    fn from(signal: Signal<T>) -> Self {
776        Self::new(Binding::<T>::from(signal))
777    }
778}
779
780/// A value binding erased to `dyn Any` together with the comparator for its concrete type.
781///
782/// The comparator is captured at erasure time so [`FieldContext`] equality can delegate to
783/// [`Binding`]'s identity equality instead of comparing wrapper allocations.
784#[derive(Clone)]
785struct ErasedBinding {
786    binding: Rc<dyn Any>,
787    value_type_name: &'static str,
788    eq: fn(&dyn Any, &dyn Any) -> bool,
789}
790
791impl ErasedBinding {
792    fn new<T: 'static>(binding: Binding<T>) -> Self {
793        Self {
794            binding: Rc::new(binding),
795            value_type_name: std::any::type_name::<T>(),
796            eq: |left, right| match (
797                left.downcast_ref::<Binding<T>>(),
798                right.downcast_ref::<Binding<T>>(),
799            ) {
800                (Some(left), Some(right)) => left == right,
801                _ => false,
802            },
803        }
804    }
805}
806
807impl PartialEq for ErasedBinding {
808    fn eq(&self, other: &Self) -> bool {
809        (self.eq)(&*self.binding, &*other.binding)
810    }
811}
812
813/// Provides a binding as the current scope's [`FieldContext`].
814///
815/// The provided context keeps the focus request slot of the context this scope provided on an
816/// earlier render, so widgets that memoized on that render stay registered with the slot producers
817/// observe.
818pub fn provide_field_context<T: 'static>(binding: Binding<T>) -> FieldContext {
819    let mut context = FieldContext::new(binding);
820
821    if let Some(existing) = has_context::<FieldContext>() {
822        context = context.with_focus_request(existing.focus_request());
823    }
824
825    provide_context(context)
826}
827
828/// Resolves a binding using explicit prop, [`FieldContext`], then uncontrolled-state precedence.
829///
830/// The internal signal hook is called regardless of which source wins so the resolution order can
831/// change between renders without violating Dioxus's hook ordering rules.
832pub fn use_binding<T: 'static>(explicit: Option<Binding<T>>, default: T) -> Binding<T> {
833    let internal = use_signal(|| default);
834
835    if let Some(binding) = explicit {
836        return binding;
837    }
838
839    if let Some(binding) =
840        try_consume_context::<FieldContext>().and_then(|context| context.try_resolve())
841    {
842        return binding;
843    }
844
845    internal.into()
846}
847
848/// Resolves metadata using explicit prop, [`FieldContext`], then standalone-state precedence.
849///
850/// The standalone state hook is always called so the source can change between renders without
851/// violating Dioxus's hook ordering rules.
852pub fn use_field_meta(explicit: Option<FieldMeta>) -> FieldMeta {
853    let internal = use_field_meta_state(FieldMetaValues::default());
854
855    explicit
856        .or_else(|| try_consume_context::<FieldContext>().and_then(|context| context.meta()))
857        .unwrap_or(internal)
858}
859
860/// Resolves the current [`FocusRequest`], or creates a standalone slot when no context exists.
861pub fn use_focus_request() -> FocusRequest {
862    let internal = use_hook(FocusRequest::default);
863
864    try_consume_context::<FieldContext>().map_or(internal, |context| context.focus_request())
865}
866
867/// Registers a widget focus callback with the resolved [`FocusRequest`] for this component's
868/// lifetime.
869pub fn use_focus_registration(callback: Callback<()>) -> FocusRequest {
870    let request = use_focus_request();
871    let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFocusRegistration>)));
872    let should_replace = active
873        .borrow()
874        .as_ref()
875        .is_none_or(|active| active.request != request || active.callback != callback);
876
877    if should_replace {
878        let registration = request.register(callback);
879        active.borrow_mut().replace(ActiveFocusRegistration {
880            request: request.clone(),
881            callback,
882            _registration: registration,
883        });
884    }
885
886    request
887}
888
889struct ActiveFocusRegistration {
890    request: FocusRequest,
891    callback: Callback<()>,
892    _registration: FocusRegistration,
893}
894
895/// Props for the headless [`Field`] context provider.
896#[derive(Clone, Debug, Props, PartialEq)]
897pub struct FieldProps {
898    /// The [`FieldContext`] provided to descendants.
899    ///
900    /// Accepts a [`FieldContext`], a [`Binding`], or a [`Signal`]. The prop is named after its
901    /// payload rather than the binding it may carry, since a context can also hold only metadata.
902    #[props(into)]
903    pub context: FieldContext,
904    /// Attributes forwarded to the rendered `div`.
905    #[props(extends = GlobalAttributes)]
906    pub attributes: Vec<Attribute>,
907    /// Field content.
908    pub children: Element,
909}
910
911/// Provides one [`FieldContext`] and renders an unstyled `div` around its children.
912///
913/// On Dioxus 0.7.10, pass listeners through an explicit `attributes: vec![...]` prop so listener
914/// ordering remains visible at the call site.
915///
916/// # Memoization
917///
918/// Children authored inline in `rsx!` and inline listener attributes compare unequal on every
919/// parent render, so a `Field` receiving either re-renders with its parent regardless of
920/// [`FieldContext`] equality. Forwarding a received element through the `children` prop keeps it
921/// comparable; context equality then decides whether `Field` re-renders.
922#[allow(non_snake_case)]
923#[allow(
924    clippy::missing_errors_doc,
925    reason = "Dioxus Element uses Result as its renderer protocol"
926)]
927pub fn Field(props: FieldProps) -> Element {
928    let has_meta_values = props.context.meta_values.is_some();
929    let meta_values = props.context.meta_values.clone().unwrap_or_default();
930    let synced_meta = use_synced_field_meta_state(&meta_values);
931    let mut context = props.context;
932
933    if has_meta_values {
934        context = context.with_meta(synced_meta);
935    }
936
937    let focus_request = use_hook(|| context.focus_request());
938    provide_context(context.with_focus_request(focus_request));
939
940    rsx! {
941        div { ..props.attributes, {props.children} }
942    }
943}
944
945/// Props for the headless [`Label`] part.
946#[derive(Clone, Debug, Props, PartialEq)]
947pub struct LabelProps {
948    /// Explicit metadata, which wins over Field Context metadata.
949    #[props(default)]
950    pub meta: Option<FieldMeta>,
951    /// Explicit invalid state, which wins over the metadata state.
952    #[props(default)]
953    pub invalid: Option<bool>,
954    /// Explicit disabled state, which wins over the metadata state.
955    #[props(default)]
956    pub disabled: Option<bool>,
957    /// Attributes forwarded to the rendered `label`.
958    #[props(extends = GlobalAttributes)]
959    pub attributes: Vec<Attribute>,
960    /// Label content.
961    pub children: Element,
962}
963
964/// Renders an unstyled `label` associated with the resolved metadata's control id.
965///
966/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
967#[allow(non_snake_case)]
968#[allow(
969    clippy::missing_errors_doc,
970    reason = "Dioxus Element uses Result as its renderer protocol"
971)]
972pub fn Label(props: LabelProps) -> Element {
973    let meta = use_field_meta(props.meta);
974    let control_id = meta.id().map(|id| id.to_string());
975    let mut attributes = part_state_attributes(
976        &meta,
977        FieldMetaOverrides {
978            invalid: props.invalid,
979            disabled: props.disabled,
980        },
981    );
982    attributes.extend(props.attributes);
983
984    rsx! {
985        label { r#for: control_id, ..attributes, {props.children} }
986    }
987}
988
989/// Props for the headless [`FieldDescription`] part.
990#[derive(Clone, Debug, Props, PartialEq)]
991pub struct FieldDescriptionProps {
992    /// Stable id registered with the resolved field metadata for this part's lifetime.
993    #[props(into)]
994    pub id: Rc<str>,
995    /// Explicit metadata, which wins over Field Context metadata.
996    #[props(default)]
997    pub meta: Option<FieldMeta>,
998    /// Explicit invalid state, which wins over the metadata state.
999    #[props(default)]
1000    pub invalid: Option<bool>,
1001    /// Explicit disabled state, which wins over the metadata state.
1002    #[props(default)]
1003    pub disabled: Option<bool>,
1004    /// Attributes forwarded to the rendered description `div`.
1005    #[props(extends = GlobalAttributes)]
1006    pub attributes: Vec<Attribute>,
1007    /// Description content.
1008    pub children: Element,
1009}
1010
1011/// Renders an unstyled description and registers its id for `aria-describedby` chaining.
1012///
1013/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1014#[allow(non_snake_case)]
1015#[allow(
1016    clippy::missing_errors_doc,
1017    reason = "Dioxus Element uses Result as its renderer protocol"
1018)]
1019pub fn FieldDescription(props: FieldDescriptionProps) -> Element {
1020    let meta = use_field_meta(props.meta);
1021    use_field_meta_id_registration(&meta, RegisteredIdKind::Description, props.id.clone());
1022    let id = props.id.to_string();
1023    let mut attributes = part_state_attributes(
1024        &meta,
1025        FieldMetaOverrides {
1026            invalid: props.invalid,
1027            disabled: props.disabled,
1028        },
1029    );
1030    attributes.extend(props.attributes);
1031
1032    rsx! {
1033        div { id: id, ..attributes, {props.children} }
1034    }
1035}
1036
1037/// Props for the headless [`FieldError`] part.
1038#[derive(Clone, Debug, Props, PartialEq)]
1039pub struct FieldErrorProps {
1040    /// Stable id registered with the resolved field metadata for this part's lifetime.
1041    #[props(into)]
1042    pub id: Rc<str>,
1043    /// Explicit metadata, which wins over Field Context metadata.
1044    #[props(default)]
1045    pub meta: Option<FieldMeta>,
1046    /// Explicit invalid state, which wins over the metadata state.
1047    #[props(default)]
1048    pub invalid: Option<bool>,
1049    /// Explicit disabled state used by data-state attributes.
1050    #[props(default)]
1051    pub disabled: Option<bool>,
1052    /// Attributes forwarded to the rendered error `div`.
1053    #[props(extends = GlobalAttributes)]
1054    pub attributes: Vec<Attribute>,
1055}
1056
1057/// Renders pre-formatted field errors in an unstyled polite live region while invalid.
1058///
1059/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1060#[allow(non_snake_case)]
1061#[allow(
1062    clippy::missing_errors_doc,
1063    reason = "Dioxus Element uses Result as its renderer protocol"
1064)]
1065pub fn FieldError(props: FieldErrorProps) -> Element {
1066    let meta = use_field_meta(props.meta);
1067    use_field_meta_id_registration(&meta, RegisteredIdKind::Error, props.id.clone());
1068    let invalid = props.invalid.unwrap_or_else(|| meta.invalid());
1069
1070    if !invalid {
1071        return dioxus_core::VNode::empty();
1072    }
1073
1074    let id = props.id.to_string();
1075    let errors = meta
1076        .errors()
1077        .iter()
1078        .map(AsRef::as_ref)
1079        .collect::<Vec<_>>()
1080        .join("\n");
1081    let mut attributes = part_state_attributes(
1082        &meta,
1083        FieldMetaOverrides {
1084            invalid: props.invalid,
1085            disabled: props.disabled,
1086        },
1087    );
1088    attributes.extend(props.attributes);
1089
1090    rsx! {
1091        div {
1092            id: id,
1093            aria_live: "polite",
1094            ..attributes,
1095            {errors}
1096        }
1097    }
1098}
1099
1100fn part_state_attributes(meta: &FieldMeta, overrides: FieldMetaOverrides) -> Vec<Attribute> {
1101    let invalid = overrides.invalid.unwrap_or_else(|| meta.invalid());
1102    let disabled = overrides.disabled.unwrap_or_else(|| meta.disabled());
1103    let mut attributes = Vec::new();
1104
1105    push_data_state(&mut attributes, "data-required", meta.required());
1106    push_data_state(&mut attributes, "data-disabled", disabled);
1107    push_data_state(&mut attributes, "data-invalid", invalid);
1108    push_data_state(&mut attributes, "data-touched", meta.touched());
1109    push_data_state(&mut attributes, "data-dirty", meta.dirty());
1110
1111    attributes
1112}
1113
1114fn use_field_meta_id_registration(meta: &FieldMeta, kind: RegisteredIdKind, id: Rc<str>) {
1115    let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFieldMetaIdRegistration>)));
1116    let should_replace = active
1117        .borrow()
1118        .as_ref()
1119        .is_none_or(|active| active.meta != *meta || active.kind != kind || active.id != id);
1120
1121    if should_replace {
1122        let mut writable_meta = *meta;
1123        let registration = writable_meta.register_id(kind, id.clone());
1124        active.borrow_mut().replace(ActiveFieldMetaIdRegistration {
1125            meta: *meta,
1126            kind,
1127            id,
1128            _registration: registration,
1129        });
1130    }
1131}
1132
1133struct ActiveFieldMetaIdRegistration {
1134    meta: FieldMeta,
1135    kind: RegisteredIdKind,
1136    id: Rc<str>,
1137    _registration: FieldMetaIdRegistration,
1138}
1139
1140#[derive(Clone)]
1141struct BindingIdentity(Rc<dyn ComparableIdentity>);
1142
1143impl BindingIdentity {
1144    fn new<I: PartialEq + 'static>(identity: I) -> Self {
1145        Self(Rc::new(identity))
1146    }
1147}
1148
1149impl PartialEq for BindingIdentity {
1150    fn eq(&self, other: &Self) -> bool {
1151        self.0.equals(other.0.as_ref())
1152    }
1153}
1154
1155trait ComparableIdentity: Any {
1156    fn equals(&self, other: &dyn ComparableIdentity) -> bool;
1157}
1158
1159impl<I: PartialEq + 'static> ComparableIdentity for I {
1160    fn equals(&self, other: &dyn ComparableIdentity) -> bool {
1161        let other = other as &dyn Any;
1162        other.downcast_ref::<I>().is_some_and(|other| self == other)
1163    }
1164}