Skip to main content

fusor/
bind.rs

1//! What a `bind` attribute means for the Rust value it names. The HTML compiler
2//! picks the control from the markup; the bound value's type decides how the
3//! control reads and edits it, so rustc checks the pairing. Reads track, so
4//! server rendering, hydration and browser effects all use the same methods.
5use crate::Signal;
6use std::{fmt::Display, str::FromStr};
7
8/// A value edited as text: text-like and numeric inputs, textarea, a single
9/// select, and the value a radio group chooses.
10///
11/// `Signal<T>` shows `T` with `Display` and parses edits with `FromStr`. Text
12/// that does not parse, such as a half-typed number, leaves the signal unchanged.
13#[diagnostic::on_unimplemented(
14    message = "`bind` on this control needs a `Signal<T>` where `T: FromStr + Display + PartialEq`, or a form field",
15    label = "`{Self}` cannot be edited as text"
16)]
17pub trait TextValue: Clone + 'static {
18    /// The text the control shows.
19    fn text(&self) -> String;
20    /// Whether the control's `text` already shows this value. Such an edit is
21    /// never written back, which keeps the cursor and partial input intact.
22    fn shows(&self, text: &str) -> bool;
23    /// Apply the control's text after a user edit.
24    fn edit(&self, text: String);
25    /// The control lost focus.
26    fn touch(&self) {}
27}
28
29impl<T: FromStr + Display + PartialEq + 'static> TextValue for Signal<T> {
30    fn text(&self) -> String {
31        self.with(ToString::to_string)
32    }
33    fn shows(&self, text: &str) -> bool {
34        text.parse::<T>()
35            .is_ok_and(|parsed| self.with(|value| *value == parsed))
36    }
37    fn edit(&self, text: String) {
38        if let Ok(value) = text.parse::<T>() {
39            self.set(value);
40        }
41    }
42}
43
44/// A checkbox. `Signal<bool>` follows its checked state; `Signal<Vec<T>>` holds
45/// the `value` of every checked box bound to it.
46#[diagnostic::on_unimplemented(
47    message = "`bind` on a checkbox needs a `Signal<bool>` or a `Signal<Vec<T>>`",
48    label = "`{Self}` cannot follow a checkbox"
49)]
50pub trait Checkbox: Clone + 'static {
51    /// Whether the box whose `value` is `choice` is checked.
52    fn checked(&self, choice: &str) -> bool;
53    /// The user checked or cleared the box whose `value` is `choice`.
54    fn check(&self, choice: &str, checked: bool);
55}
56
57impl Checkbox for Signal<bool> {
58    fn checked(&self, _: &str) -> bool {
59        self.get()
60    }
61    fn check(&self, _: &str, checked: bool) {
62        self.set(checked);
63    }
64}
65
66impl<T: FromStr + PartialEq + 'static> Checkbox for Signal<Vec<T>> {
67    fn checked(&self, choice: &str) -> bool {
68        selected(self, choice)
69    }
70    fn check(&self, choice: &str, checked: bool) {
71        let Ok(choice) = choice.parse::<T>() else {
72            return;
73        };
74        if self.with_untracked(|values| values.contains(&choice)) != checked {
75            self.update(|values| {
76                if checked {
77                    values.push(choice);
78                } else {
79                    values.retain(|value| *value != choice);
80                }
81            });
82        }
83    }
84}
85
86/// Whether values bound to a `<select multiple>` or to several checkboxes
87/// include the option or box whose `value` is `choice`.
88pub fn selected<T: FromStr + PartialEq>(values: &Signal<Vec<T>>, choice: &str) -> bool {
89    choice
90        .parse::<T>()
91        .is_ok_and(|choice| values.with(|values| values.contains(&choice)))
92}