Skip to main content

qframe/widgets/
form_errors.rs

1//! Validation results an application keeps for its form.
2
3use crate::runtime::Command;
4
5/// The problems of a form, in the order the application found them, each under the name of the
6/// control it belongs to.
7///
8/// Validation stays in the application: it checks its values and records a message for every
9/// problem. Use the same name for the error and the control's [`NodeMut::id`](crate::widget::NodeMut::id),
10/// so [`FormErrors::focus_first`] can take the user to the first problem.
11///
12/// ```
13/// use qframe::widgets::FormErrors;
14///
15/// let name = "ab";
16/// let mut errors = FormErrors::new();
17/// errors.check("name", name.chars().count() >= 3, "Use at least 3 characters");
18/// assert_eq!(errors.get("name"), Some("Use at least 3 characters"));
19/// assert_eq!(errors.first(), Some("name"));
20/// ```
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct FormErrors {
23    entries: Vec<(String, String)>,
24}
25
26impl FormErrors {
27    /// No problems.
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Records `message` for control `name`, replacing an earlier message for it.
34    pub fn set(&mut self, name: impl Into<String>, message: impl Into<String>) {
35        let (name, message) = (name.into(), message.into());
36        match self.entries.iter_mut().find(|(existing, _)| *existing == name) {
37            Some(entry) => entry.1 = message,
38            None => self.entries.push((name, message)),
39        }
40    }
41
42    /// Records `message` for `name` when `valid` is false and forgets any problem of `name`
43    /// when it is true.
44    pub fn check(&mut self, name: impl Into<String>, valid: bool, message: impl Into<String>) {
45        let name = name.into();
46        if valid {
47            self.remove(&name);
48        } else {
49            self.set(name, message);
50        }
51    }
52
53    /// Forgets the problem of `name`.
54    pub fn remove(&mut self, name: &str) {
55        self.entries.retain(|(existing, _)| existing != name);
56    }
57
58    /// Forgets every problem.
59    pub fn clear(&mut self) {
60        self.entries.clear();
61    }
62
63    /// The message for `name`, if it has a problem.
64    #[must_use]
65    pub fn get(&self, name: &str) -> Option<&str> {
66        self.entries.iter().find(|(existing, _)| existing == name).map(|(_, message)| message.as_str())
67    }
68
69    /// Whether `name` has a problem.
70    #[must_use]
71    pub fn has(&self, name: &str) -> bool {
72        self.get(name).is_some()
73    }
74
75    /// Whether there are no problems.
76    #[must_use]
77    pub fn is_empty(&self) -> bool {
78        self.entries.is_empty()
79    }
80
81    /// How many problems there are.
82    #[must_use]
83    pub fn len(&self) -> usize {
84        self.entries.len()
85    }
86
87    /// The name of the first problem.
88    #[must_use]
89    pub fn first(&self) -> Option<&str> {
90        self.entries.first().map(|(name, _)| name.as_str())
91    }
92
93    /// Every problem as `(name, message)`, in order.
94    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
95        self.entries.iter().map(|(name, message)| (name.as_str(), message.as_str()))
96    }
97
98    /// Moves focus to the control of the first problem; nothing when there are none.
99    #[must_use]
100    pub fn focus_first<Msg: Send + 'static>(&self) -> Command<Msg> {
101        self.first().map_or_else(Command::none, Command::focus)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn keeps_order_replaces_and_forgets() {
111        let mut errors = FormErrors::new();
112        errors.set("name", "too short");
113        errors.set("engine", "choose one");
114        errors.set("name", "taken");
115        assert_eq!(errors.iter().collect::<Vec<_>>(), vec![("name", "taken"), ("engine", "choose one")]);
116        errors.check("name", true, "unused");
117        assert_eq!(errors.first(), Some("engine"));
118        assert_eq!(errors.len(), 1);
119        errors.clear();
120        assert!(errors.is_empty() && !errors.has("engine"));
121    }
122}