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`].
900 pub fn new(
901 read: ReadSignal<T>,
902 write: Callback<(T, ChangeOrigin)>,
903 commit: Callback<()>,
904 ) -> Self {
905 Self::new_with_identity(read, write, commit, (read, write, commit))
906 }
907
908 /// Creates a binding with a producer-defined comparable identity.
909 ///
910 /// Equal identities must always represent interchangeable read, write, and commit behavior.
911 /// This constructor installs no-op Focus Exit behavior, so that behavior is interchangeable as
912 /// well. Producers that cannot prove interchangeability should use [`Binding::new`] instead.
913 pub fn new_with_identity<I>(
914 read: ReadSignal<T>,
915 write: Callback<(T, ChangeOrigin)>,
916 commit: Callback<()>,
917 identity: I,
918 ) -> Self
919 where
920 I: PartialEq + 'static,
921 {
922 Self {
923 read,
924 write,
925 commit,
926 focus_exit: Callback::new(|()| {}),
927 identity: BindingIdentity::new(identity),
928 }
929 }
930
931 /// Adds the callback invoked when focus leaves the widget's complete logical focus scope.
932 ///
933 /// This builder also incorporates the callback into binding identity, preserving the guarantee
934 /// that equal bindings have interchangeable Focus Exit behavior. It does not alter Commit or
935 /// imply any form-library blur, touched, or validation semantics.
936 #[must_use]
937 pub fn with_focus_exit(mut self, focus_exit: Callback<()>) -> Self {
938 self.identity = BindingIdentity::new((self.identity, focus_exit));
939 self.focus_exit = focus_exit;
940 self
941 }
942
943 /// Writes a value and preserves where the change originated.
944 pub fn write(&self, value: T, origin: ChangeOrigin) {
945 self.write.call((value, origin));
946 }
947
948 /// Reports the widget-defined end of one interaction unit.
949 pub fn commit(&self) {
950 self.commit.call(());
951 }
952
953 /// Reports that focus left the widget's complete logical focus scope.
954 ///
955 /// This is independent from [`Binding::commit`]. Widgets are responsible for defining their
956 /// complete scope, including owned child controls and popup or portal content, and for
957 /// suppressing reports while focus moves within it.
958 pub fn focus_exit(&self) {
959 self.focus_exit.call(());
960 }
961
962 /// Decomposes this binding into the dependency-free widget prop contract.
963 ///
964 /// The lower-level `on_change` callback has no origin parameter, so its writes are user writes.
965 pub fn into_trio(self) -> BindingPropTrio<T> {
966 let value = self.read;
967 let on_commit = self.commit;
968 let on_change = Callback::new(move |value| self.write(value, ChangeOrigin::User));
969
970 BindingPropTrio {
971 value,
972 on_change,
973 on_commit,
974 }
975 }
976}
977
978impl<T: fmt::Debug + 'static> fmt::Debug for Binding<T> {
979 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
980 f.debug_struct("Binding")
981 .field("read", &*self.read.peek())
982 .finish_non_exhaustive()
983 }
984}
985
986impl<T: 'static> Clone for Binding<T> {
987 fn clone(&self) -> Self {
988 Self {
989 read: self.read,
990 write: self.write,
991 commit: self.commit,
992 focus_exit: self.focus_exit,
993 identity: self.identity.clone(),
994 }
995 }
996}
997
998impl<T: 'static> PartialEq for Binding<T> {
999 fn eq(&self, other: &Self) -> bool {
1000 self.identity == other.identity
1001 }
1002}
1003
1004impl<T: 'static> From<Signal<T>> for Binding<T> {
1005 fn from(signal: Signal<T>) -> Self {
1006 let read = ReadSignal::from(signal);
1007 let mut writer = signal;
1008 let write = Callback::new(move |(value, _origin)| writer.set(value));
1009 let commit = Callback::new(|()| {});
1010
1011 Self::new_with_identity(read, write, commit, signal)
1012 }
1013}
1014
1015impl<T: 'static> From<(ReadSignal<T>, Callback<T>)> for Binding<T> {
1016 fn from((read, on_change): (ReadSignal<T>, Callback<T>)) -> Self {
1017 let write = Callback::new(move |(value, _origin)| on_change.call(value));
1018 let commit = Callback::new(|()| {});
1019
1020 Self::new_with_identity(read, write, commit, (read, on_change))
1021 }
1022}
1023
1024impl<T: 'static> From<T> for Binding<T> {
1025 fn from(value: T) -> Self {
1026 Signal::new(value).into()
1027 }
1028}
1029
1030/// A carrier for the lower-level prop contract implemented by field-shaped widgets.
1031///
1032/// Decompose this carrier into three separate props to keep a widget independent from this crate.
1033/// Since `on_change` does not carry a [`ChangeOrigin`], calling it represents a user change.
1034pub struct BindingPropTrio<T: 'static> {
1035 /// The reactive value read by the widget.
1036 pub value: ReadSignal<T>,
1037 /// The callback invoked when user interaction changes the value.
1038 pub on_change: Callback<T>,
1039 /// The callback invoked at the widget-defined end of an interaction unit.
1040 pub on_commit: Callback<()>,
1041}
1042
1043impl<T: fmt::Debug + 'static> fmt::Debug for BindingPropTrio<T> {
1044 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1045 f.debug_struct("BindingPropTrio")
1046 .field("value", &*self.value.peek())
1047 .field("on_change", &self.on_change)
1048 .field("on_commit", &self.on_commit)
1049 .finish()
1050 }
1051}
1052
1053impl<T: 'static> From<Binding<T>> for BindingPropTrio<T> {
1054 fn from(binding: Binding<T>) -> Self {
1055 binding.into_trio()
1056 }
1057}
1058
1059/// A field-scoped slot through which a widget exposes its focus behavior.
1060#[derive(Clone, Default)]
1061pub struct FocusRequest(Rc<RefCell<FocusRequestState>>);
1062
1063impl FocusRequest {
1064 /// Registers the callback used by [`FocusRequest::request`].
1065 ///
1066 /// Dropping the returned registration removes this callback without disturbing a newer
1067 /// registration in the same slot.
1068 #[must_use]
1069 pub fn register(&self, callback: Callback<()>) -> FocusRegistration {
1070 let mut state = self.0.borrow_mut();
1071 let token = state.next_token;
1072 state.next_token += 1;
1073 state.current = Some((token, callback));
1074
1075 FocusRegistration {
1076 request: self.clone(),
1077 token,
1078 }
1079 }
1080
1081 /// Requests focus from the currently registered widget.
1082 ///
1083 /// Returns whether a widget was registered to receive the request.
1084 pub fn request(&self) -> bool {
1085 let callback = self.0.borrow().current.map(|(_, callback)| callback);
1086
1087 if let Some(callback) = callback {
1088 callback.call(());
1089 true
1090 } else {
1091 false
1092 }
1093 }
1094}
1095
1096impl fmt::Debug for FocusRequest {
1097 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1098 f.debug_struct("FocusRequest")
1099 .field("registered", &self.0.borrow().current.is_some())
1100 .finish()
1101 }
1102}
1103
1104impl PartialEq for FocusRequest {
1105 fn eq(&self, other: &Self) -> bool {
1106 Rc::ptr_eq(&self.0, &other.0)
1107 }
1108}
1109
1110#[derive(Default)]
1111struct FocusRequestState {
1112 next_token: u64,
1113 current: Option<(u64, Callback<()>)>,
1114}
1115
1116/// A lifecycle-bound focus callback registration.
1117pub struct FocusRegistration {
1118 request: FocusRequest,
1119 token: u64,
1120}
1121
1122impl fmt::Debug for FocusRegistration {
1123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1124 f.debug_struct("FocusRegistration")
1125 .field("request", &self.request)
1126 .field("token", &self.token)
1127 .finish()
1128 }
1129}
1130
1131impl Drop for FocusRegistration {
1132 fn drop(&mut self) {
1133 let mut state = self.request.0.borrow_mut();
1134
1135 if state.current.is_some_and(|(token, _)| token == self.token) {
1136 state.current = None;
1137 }
1138 }
1139}
1140
1141/// Type-erased context for one field's binding, metadata, and focus request slot.
1142///
1143/// The context itself is intentionally not generic. This lets [`use_binding`] distinguish an
1144/// absent context from a present context containing the wrong value type, and lets [`Field`]
1145/// accept any value type without becoming generic itself.
1146///
1147/// # Equality
1148///
1149/// Two contexts are equal when their bindings are equal under [`Binding`]'s identity equality and
1150/// their metadata is equal, regardless of when either context was constructed. The focus request
1151/// slot is intentionally excluded: [`Field`] pins the slot of the first context it receives for
1152/// its lifetime, so the slot carried by a context built on a later render is never observed by
1153/// descendants, and comparing it would only defeat memoization.
1154#[derive(Clone)]
1155pub struct FieldContext {
1156 binding: Option<ErasedBinding>,
1157 meta: Option<FieldMeta>,
1158 meta_values: Option<FieldMetaValues>,
1159 focus_request: FocusRequest,
1160}
1161
1162impl FieldContext {
1163 /// Creates context for a binding.
1164 pub fn new<T: 'static>(binding: Binding<T>) -> Self {
1165 Self {
1166 binding: Some(ErasedBinding::new(binding)),
1167 meta: None,
1168 meta_values: None,
1169 focus_request: FocusRequest::default(),
1170 }
1171 }
1172
1173 /// Creates context with no value binding or metadata.
1174 pub fn empty() -> Self {
1175 Self {
1176 binding: None,
1177 meta: None,
1178 meta_values: None,
1179 focus_request: FocusRequest::default(),
1180 }
1181 }
1182
1183 /// Replaces the context's value binding.
1184 #[must_use]
1185 pub fn with_binding<T: 'static>(mut self, binding: Binding<T>) -> Self {
1186 self.binding = Some(ErasedBinding::new(binding));
1187 self
1188 }
1189
1190 /// Adds signal-backed metadata to the context.
1191 #[must_use]
1192 pub fn with_meta(mut self, meta: FieldMeta) -> Self {
1193 self.meta = Some(meta);
1194 self.meta_values = None;
1195 self
1196 }
1197
1198 /// Adds producer values that [`Field`] realizes as signal-backed metadata.
1199 #[must_use]
1200 pub fn with_meta_values(mut self, values: FieldMetaValues) -> Self {
1201 self.meta = None;
1202 self.meta_values = Some(values);
1203 self
1204 }
1205
1206 /// Returns the context's metadata, when present.
1207 pub fn meta(&self) -> Option<FieldMeta> {
1208 self.meta
1209 }
1210
1211 /// Returns the context's focus request slot.
1212 ///
1213 /// [`Field`] pins the slot of the first context it receives, so producers that request focus
1214 /// through a context must keep that context stable across renders.
1215 pub fn focus_request(&self) -> FocusRequest {
1216 self.focus_request.clone()
1217 }
1218
1219 /// Resolves the context binding for `T`.
1220 ///
1221 /// # Panics
1222 ///
1223 /// Panics when the field context contains no binding or a binding for a different value type.
1224 pub fn resolve<T: 'static>(&self) -> Binding<T> {
1225 let erased = self
1226 .binding
1227 .as_ref()
1228 .unwrap_or_else(|| panic!("Field Context contains no value binding"));
1229
1230 erased
1231 .binding
1232 .downcast_ref::<Binding<T>>()
1233 .unwrap_or_else(|| {
1234 panic!(
1235 "Field Context contains a binding for {}, but a binding for {} was requested",
1236 erased.value_type_name,
1237 std::any::type_name::<T>()
1238 )
1239 })
1240 .clone()
1241 }
1242
1243 fn try_resolve<T: 'static>(&self) -> Option<Binding<T>> {
1244 self.binding.as_ref().map(|_| self.resolve())
1245 }
1246
1247 fn with_focus_request(mut self, focus_request: FocusRequest) -> Self {
1248 self.focus_request = focus_request;
1249 self
1250 }
1251}
1252
1253impl fmt::Debug for FieldContext {
1254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1255 f.debug_struct("FieldContext")
1256 .field(
1257 "value_type_name",
1258 &self.binding.as_ref().map(|binding| binding.value_type_name),
1259 )
1260 .field("meta", &self.meta)
1261 .field("meta_values", &self.meta_values)
1262 .field("focus_request", &self.focus_request)
1263 .finish_non_exhaustive()
1264 }
1265}
1266
1267impl PartialEq for FieldContext {
1268 fn eq(&self, other: &Self) -> bool {
1269 self.binding == other.binding
1270 && self.meta == other.meta
1271 && self.meta_values == other.meta_values
1272 }
1273}
1274
1275impl<T: 'static> From<Binding<T>> for FieldContext {
1276 fn from(binding: Binding<T>) -> Self {
1277 Self::new(binding)
1278 }
1279}
1280
1281impl<T: 'static> From<Signal<T>> for FieldContext {
1282 fn from(signal: Signal<T>) -> Self {
1283 Self::new(Binding::<T>::from(signal))
1284 }
1285}
1286
1287/// A value binding erased to `dyn Any` together with the comparator for its concrete type.
1288///
1289/// The comparator is captured at erasure time so [`FieldContext`] equality can delegate to
1290/// [`Binding`]'s identity equality instead of comparing wrapper allocations.
1291#[derive(Clone)]
1292struct ErasedBinding {
1293 binding: Rc<dyn Any>,
1294 value_type_name: &'static str,
1295 eq: fn(&dyn Any, &dyn Any) -> bool,
1296}
1297
1298impl ErasedBinding {
1299 fn new<T: 'static>(binding: Binding<T>) -> Self {
1300 Self {
1301 binding: Rc::new(binding),
1302 value_type_name: std::any::type_name::<T>(),
1303 eq: |left, right| match (
1304 left.downcast_ref::<Binding<T>>(),
1305 right.downcast_ref::<Binding<T>>(),
1306 ) {
1307 (Some(left), Some(right)) => left == right,
1308 _ => false,
1309 },
1310 }
1311 }
1312}
1313
1314impl PartialEq for ErasedBinding {
1315 fn eq(&self, other: &Self) -> bool {
1316 (self.eq)(&*self.binding, &*other.binding)
1317 }
1318}
1319
1320/// Provides a binding as the current scope's [`FieldContext`].
1321///
1322/// The provided context keeps the focus request slot of the context this scope provided on an
1323/// earlier render, so widgets that memoized on that render stay registered with the slot producers
1324/// observe.
1325pub fn provide_field_context<T: 'static>(binding: Binding<T>) -> FieldContext {
1326 let mut context = FieldContext::new(binding);
1327
1328 if let Some(existing) = has_context::<FieldContext>() {
1329 context = context.with_focus_request(existing.focus_request());
1330 }
1331
1332 provide_context(context)
1333}
1334
1335/// Resolves a binding using explicit prop, [`FieldContext`], then uncontrolled-state precedence.
1336///
1337/// The internal signal hook is called regardless of which source wins so the resolution order can
1338/// change between renders without violating Dioxus's hook ordering rules.
1339pub fn use_binding<T: 'static>(explicit: Option<Binding<T>>, default: T) -> Binding<T> {
1340 let internal = use_signal(|| default);
1341
1342 if let Some(binding) = explicit {
1343 return binding;
1344 }
1345
1346 if let Some(binding) =
1347 try_consume_context::<FieldContext>().and_then(|context| context.try_resolve())
1348 {
1349 return binding;
1350 }
1351
1352 internal.into()
1353}
1354
1355/// Resolves metadata using explicit prop, [`FieldContext`], then standalone-state precedence.
1356///
1357/// The standalone state hook is always called so the source can change between renders without
1358/// violating Dioxus's hook ordering rules.
1359pub fn use_field_meta(explicit: Option<FieldMeta>) -> FieldMeta {
1360 use_resolved_field_meta(explicit.as_ref()).0
1361}
1362
1363/// Where a part's resolved metadata came from.
1364///
1365/// A part that renders a reference *to a control* needs this: an id resolved from metadata nobody
1366/// else holds addresses no rendered element, so the reference would dangle.
1367#[derive(Clone, Copy, PartialEq, Eq)]
1368enum FieldMetaSource {
1369 /// An explicit prop or the Field Context. A control may hold the same metadata, and if the
1370 /// caller passed it deliberately, one is meant to.
1371 Shared,
1372 /// This part's own standalone state, which by construction no control is reading.
1373 Standalone,
1374}
1375
1376/// Resolves metadata as [`use_field_meta`] does, and reports which source won.
1377fn use_resolved_field_meta(explicit: Option<&FieldMeta>) -> (FieldMeta, FieldMetaSource) {
1378 let internal = use_field_meta_state(FieldMetaValues::default());
1379
1380 explicit
1381 .copied()
1382 .or_else(|| try_consume_context::<FieldContext>().and_then(|context| context.meta()))
1383 .map_or((internal, FieldMetaSource::Standalone), |meta| {
1384 (meta, FieldMetaSource::Shared)
1385 })
1386}
1387
1388/// Resolves the current [`FocusRequest`], or creates a standalone slot when no context exists.
1389pub fn use_focus_request() -> FocusRequest {
1390 let internal = use_hook(FocusRequest::default);
1391
1392 try_consume_context::<FieldContext>().map_or(internal, |context| context.focus_request())
1393}
1394
1395/// Registers a widget focus callback with the resolved [`FocusRequest`] for this component's
1396/// lifetime.
1397///
1398/// # The callback must be render-stable
1399///
1400/// Pass a callback whose identity survives a re-render — [`dioxus_hooks::use_callback`] produces
1401/// one. [`Callback::new`] does not: it allocates a fresh generational box per call and compares by
1402/// pointer identity, so calling it in a component body re-registers on **every** render. One slot
1403/// is shared by the whole field, so a widget that re-registers steals the slot from a sibling
1404/// widget that legitimately owns it, and focus ownership ends up decided by render recency rather
1405/// than by structure. Each re-registration also leaks a generational box until the component
1406/// unmounts.
1407///
1408/// # Which element to register
1409///
1410/// Register the element that actually receives focus, and let the callback do nothing when that
1411/// element cannot take focus — a disabled control should not move focus at all. Never focus a
1412/// proxy element and never blur: both hand the user's focus to something they did not ask for,
1413/// and `HTMLElement.focus()` reports success either way, so nothing downstream can detect it.
1414pub fn use_focus_registration(callback: Callback<()>) -> FocusRequest {
1415 let request = use_focus_request();
1416 let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFocusRegistration>)));
1417 let should_replace = active
1418 .borrow()
1419 .as_ref()
1420 .is_none_or(|active| active.request != request || active.callback != callback);
1421
1422 if should_replace {
1423 let registration = request.register(callback);
1424 active.borrow_mut().replace(ActiveFocusRegistration {
1425 request: request.clone(),
1426 callback,
1427 _registration: registration,
1428 });
1429 }
1430
1431 request
1432}
1433
1434struct ActiveFocusRegistration {
1435 request: FocusRequest,
1436 callback: Callback<()>,
1437 _registration: FocusRegistration,
1438}
1439
1440/// Props for the headless [`Field`] context provider.
1441#[derive(Clone, Debug, Props, PartialEq)]
1442pub struct FieldProps {
1443 /// The [`FieldContext`] provided to descendants.
1444 ///
1445 /// Accepts a [`FieldContext`], a [`Binding`], or a [`Signal`]. The prop is named after its
1446 /// payload rather than the binding it may carry, since a context can also hold only metadata.
1447 #[props(into)]
1448 pub context: FieldContext,
1449 /// Attributes forwarded to the rendered `div`.
1450 ///
1451 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1452 /// of any spread list. A forwarded attribute wins its name.
1453 #[props(extends = GlobalAttributes)]
1454 pub attributes: Vec<Attribute>,
1455 /// Field content.
1456 pub children: Element,
1457}
1458
1459/// Provides one [`FieldContext`] and renders an unstyled `div` around its children.
1460///
1461/// On Dioxus 0.7.10, pass listeners through an explicit `attributes: vec![...]` prop so listener
1462/// ordering remains visible at the call site.
1463///
1464/// # Memoization
1465///
1466/// Children authored inline in `rsx!` and inline listener attributes compare unequal on every
1467/// parent render, so a `Field` receiving either re-renders with its parent regardless of
1468/// [`FieldContext`] equality. Forwarding a received element through the `children` prop keeps it
1469/// comparable; context equality then decides whether `Field` re-renders.
1470#[allow(non_snake_case)]
1471#[allow(
1472 clippy::missing_errors_doc,
1473 reason = "Dioxus Element uses Result as its renderer protocol"
1474)]
1475pub fn Field(props: FieldProps) -> Element {
1476 let has_meta_values = props.context.meta_values.is_some();
1477 let meta_values = props.context.meta_values.clone().unwrap_or_default();
1478 let synced_meta = use_synced_field_meta_state(&meta_values);
1479 let mut context = props.context;
1480
1481 if has_meta_values {
1482 context = context.with_meta(synced_meta);
1483 }
1484
1485 let focus_request = use_hook(|| context.focus_request());
1486 provide_context(context.with_focus_request(focus_request));
1487 let mut attributes = props.attributes;
1488 normalize_attributes(&mut attributes);
1489
1490 rsx! {
1491 div { ..attributes, {props.children} }
1492 }
1493}
1494
1495/// Props for the headless [`Label`] part.
1496#[derive(Clone, Debug, Props, PartialEq)]
1497pub struct LabelProps {
1498 /// The rendered `label`'s own id, registered with the resolved metadata for this part's
1499 /// lifetime. Defaults to a generated id.
1500 ///
1501 /// The control reaches this id through `aria-labelledby`, which is the only naming path
1502 /// available to a control rooted on an element `<label for>` cannot address — `for` requires a
1503 /// labelable element, and widgets rooted on a `div` are not one.
1504 #[props(default)]
1505 pub id: Option<Rc<str>>,
1506 /// Explicit metadata, which wins over Field Context metadata.
1507 #[props(default)]
1508 pub meta: Option<FieldMeta>,
1509 /// Explicit invalid state, which wins over the metadata state.
1510 #[props(default)]
1511 pub invalid: Option<bool>,
1512 /// Explicit disabled state, which wins over the metadata state.
1513 #[props(default)]
1514 pub disabled: Option<bool>,
1515 /// Explicit required state, which wins over the metadata state.
1516 #[props(default)]
1517 pub required: Option<bool>,
1518 /// Attributes forwarded to the rendered `label`.
1519 ///
1520 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1521 /// of any spread list. A forwarded attribute wins its name.
1522 #[props(extends = GlobalAttributes)]
1523 pub attributes: Vec<Attribute>,
1524 /// Label content.
1525 pub children: Element,
1526}
1527
1528/// Renders an unstyled `label` associated with the resolved metadata's control id.
1529///
1530/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1531/// Running standalone means no control shares the metadata, so no `for` is emitted — the label
1532/// still carries its own id, which is the reference a control would reach through
1533/// `aria-labelledby`.
1534#[allow(non_snake_case)]
1535#[allow(
1536 clippy::missing_errors_doc,
1537 reason = "Dioxus Element uses Result as its renderer protocol"
1538)]
1539pub fn Label(props: LabelProps) -> Element {
1540 let (meta, source) = use_resolved_field_meta(props.meta.as_ref());
1541 let id = use_part_id(props.id, "label");
1542 use_field_meta_id_registration(&meta, RegisteredIdKind::Label, Rc::clone(&id));
1543 // Metadata the label resolved for itself alone addresses no rendered control, so pointing
1544 // `for` at its generated id would dangle. Emitting nothing is what 0.1.0 did, and is honest.
1545 let control_id = (source == FieldMetaSource::Shared).then(|| meta.id().to_string());
1546 let attributes = part_attributes(
1547 &meta,
1548 FieldStateOverrides {
1549 invalid: props.invalid,
1550 disabled: props.disabled,
1551 required: props.required,
1552 },
1553 props.attributes,
1554 );
1555
1556 rsx! {
1557 label { id: id.to_string(), r#for: control_id, ..attributes, {props.children} }
1558 }
1559}
1560
1561/// Props for the headless [`FieldDescription`] part.
1562#[derive(Clone, Debug, Props, PartialEq)]
1563pub struct FieldDescriptionProps {
1564 /// The rendered description's own id, registered with the resolved field metadata for this
1565 /// part's lifetime. Defaults to a generated id.
1566 #[props(default)]
1567 pub id: Option<Rc<str>>,
1568 /// Explicit metadata, which wins over Field Context metadata.
1569 #[props(default)]
1570 pub meta: Option<FieldMeta>,
1571 /// Explicit invalid state, which wins over the metadata state.
1572 #[props(default)]
1573 pub invalid: Option<bool>,
1574 /// Explicit disabled state, which wins over the metadata state.
1575 #[props(default)]
1576 pub disabled: Option<bool>,
1577 /// Explicit required state, which wins over the metadata state.
1578 #[props(default)]
1579 pub required: Option<bool>,
1580 /// Attributes forwarded to the rendered description `div`.
1581 ///
1582 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1583 /// of any spread list. A forwarded attribute wins its name.
1584 #[props(extends = GlobalAttributes)]
1585 pub attributes: Vec<Attribute>,
1586 /// Description content.
1587 pub children: Element,
1588}
1589
1590/// Renders an unstyled description and registers its id for `aria-describedby` chaining.
1591///
1592/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1593///
1594/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1595#[allow(non_snake_case)]
1596#[allow(
1597 clippy::missing_errors_doc,
1598 reason = "Dioxus Element uses Result as its renderer protocol"
1599)]
1600pub fn FieldDescription(props: FieldDescriptionProps) -> Element {
1601 let meta = use_field_meta(props.meta);
1602 let id = use_part_id(props.id, "description");
1603 use_field_meta_id_registration(&meta, RegisteredIdKind::Description, Rc::clone(&id));
1604 let attributes = part_attributes(
1605 &meta,
1606 FieldStateOverrides {
1607 invalid: props.invalid,
1608 disabled: props.disabled,
1609 required: props.required,
1610 },
1611 props.attributes,
1612 );
1613
1614 rsx! {
1615 div { id: id.to_string(), ..attributes, {props.children} }
1616 }
1617}
1618
1619/// Props for the headless [`FieldError`] part.
1620#[derive(Clone, Debug, Props, PartialEq)]
1621pub struct FieldErrorProps {
1622 /// The rendered error region's own id, registered with the resolved field metadata for this
1623 /// part's lifetime. Defaults to a generated id.
1624 #[props(default)]
1625 pub id: Option<Rc<str>>,
1626 /// Explicit metadata, which wins over Field Context metadata.
1627 #[props(default)]
1628 pub meta: Option<FieldMeta>,
1629 /// Explicit invalid state, which wins over the metadata state.
1630 #[props(default)]
1631 pub invalid: Option<bool>,
1632 /// Explicit disabled state used by data-state attributes.
1633 #[props(default)]
1634 pub disabled: Option<bool>,
1635 /// Explicit required state used by data-state attributes.
1636 #[props(default)]
1637 pub required: Option<bool>,
1638 /// Attributes forwarded to the rendered error `div`.
1639 ///
1640 /// Sorted by attribute name and deduplicated with the part's own, which `dioxus-core` requires
1641 /// of any spread list. A forwarded attribute wins its name.
1642 #[props(extends = GlobalAttributes)]
1643 pub attributes: Vec<Attribute>,
1644}
1645
1646/// Renders pre-formatted field errors in an unstyled polite live region, one element per error.
1647///
1648/// The live region stays mounted while the field is valid, holding no children. A live region that
1649/// enters the accessibility tree in the same update as its content is not announced reliably, so
1650/// the region has to exist before the first error arrives.
1651///
1652/// When no id is supplied, this part generates one that remains stable for its mounted lifetime.
1653///
1654/// This part can resolve metadata from Field Context, accept it explicitly, or run standalone.
1655#[allow(non_snake_case)]
1656#[allow(
1657 clippy::missing_errors_doc,
1658 reason = "Dioxus Element uses Result as its renderer protocol"
1659)]
1660pub fn FieldError(props: FieldErrorProps) -> Element {
1661 let meta = use_field_meta(props.meta);
1662 let id = use_part_id(props.id, "error");
1663 use_field_meta_id_registration(&meta, RegisteredIdKind::Error, Rc::clone(&id));
1664 let invalid = props.invalid.unwrap_or_else(|| meta.invalid());
1665 let errors = if invalid { meta.errors() } else { Vec::new() };
1666 let attributes = part_attributes(
1667 &meta,
1668 FieldStateOverrides {
1669 invalid: props.invalid,
1670 disabled: props.disabled,
1671 required: props.required,
1672 },
1673 props.attributes,
1674 );
1675
1676 rsx! {
1677 div {
1678 id: id.to_string(),
1679 aria_live: "polite",
1680 ..attributes,
1681 for error in errors {
1682 div { "{error}" }
1683 }
1684 }
1685 }
1686}
1687
1688/// Builds a field part's `data-*` state attributes and appends the caller's forwarded ones.
1689///
1690/// Forwarded attributes go last, so a caller's attribute wins its name once
1691/// [`normalize_attributes`] resolves the result.
1692fn part_attributes(
1693 meta: &FieldMeta,
1694 overrides: FieldStateOverrides,
1695 forwarded: Vec<Attribute>,
1696) -> Vec<Attribute> {
1697 let required = overrides.required.unwrap_or_else(|| meta.required());
1698 let disabled = overrides.disabled.unwrap_or_else(|| meta.disabled());
1699 let invalid = overrides.invalid.unwrap_or_else(|| meta.invalid());
1700 let mut attributes = Vec::new();
1701
1702 push_state(&mut attributes, "data-required", required);
1703 push_state(&mut attributes, "data-disabled", disabled);
1704 push_state(&mut attributes, "data-invalid", invalid);
1705 push_state(&mut attributes, "data-touched", meta.touched());
1706 push_state(&mut attributes, "data-dirty", meta.dirty());
1707
1708 attributes.extend(forwarded);
1709 normalize_attributes(&mut attributes);
1710
1711 attributes
1712}
1713
1714fn use_field_meta_id_registration(meta: &FieldMeta, kind: RegisteredIdKind, id: Rc<str>) {
1715 let active = use_hook(|| Rc::new(RefCell::new(None::<ActiveFieldMetaIdRegistration>)));
1716 let should_replace = active
1717 .borrow()
1718 .as_ref()
1719 .is_none_or(|active| active.meta != *meta || active.kind != kind || active.id != id);
1720
1721 if should_replace {
1722 let mut writable_meta = *meta;
1723 let registration = writable_meta.register_id(kind, id.clone());
1724 active.borrow_mut().replace(ActiveFieldMetaIdRegistration {
1725 meta: *meta,
1726 kind,
1727 id,
1728 _registration: registration,
1729 });
1730 }
1731}
1732
1733struct ActiveFieldMetaIdRegistration {
1734 meta: FieldMeta,
1735 kind: RegisteredIdKind,
1736 id: Rc<str>,
1737 _registration: FieldMetaIdRegistration,
1738}
1739
1740#[derive(Clone)]
1741struct BindingIdentity(Rc<dyn ComparableIdentity>);
1742
1743impl BindingIdentity {
1744 fn new<I: PartialEq + 'static>(identity: I) -> Self {
1745 Self(Rc::new(identity))
1746 }
1747}
1748
1749impl PartialEq for BindingIdentity {
1750 fn eq(&self, other: &Self) -> bool {
1751 self.0.equals(other.0.as_ref())
1752 }
1753}
1754
1755trait ComparableIdentity: Any {
1756 fn equals(&self, other: &dyn ComparableIdentity) -> bool;
1757}
1758
1759impl<I: PartialEq + 'static> ComparableIdentity for I {
1760 fn equals(&self, other: &dyn ComparableIdentity) -> bool {
1761 let other = other as &dyn Any;
1762 other.downcast_ref::<I>().is_some_and(|other| self == other)
1763 }
1764}