Skip to main content

runique/forms/fields/
choice.rs

1//! Selection field: `ChoiceField` with groups, multi-select, and dynamic options.
2use crate::forms::base::{CommonFieldConfig, FieldConfig, FormField};
3use crate::utils::aliases::ATera;
4use crate::utils::trad::{t, tf};
5use async_trait::async_trait;
6use serde::Serialize;
7use serde_json::{Value, json};
8
9/// Option for choice fields
10#[derive(Clone, Debug, Serialize)]
11pub struct ChoiceOption {
12    pub value: String,
13    pub label: String,
14    pub selected: bool,
15}
16
17impl ChoiceOption {
18    /// Creates a new option with the given submitted value and display label.
19    pub fn new(value: &str, label: &str) -> Self {
20        Self {
21            value: value.to_string(),
22            label: label.to_string(),
23            selected: false,
24        }
25    }
26
27    /// Marks this option as pre-selected.
28    pub fn selected(mut self) -> Self {
29        self.selected = true;
30        self
31    }
32}
33
34/// Dropdown `<select>` field. Use [`ChoiceField::multiple`] for multi-select.
35#[derive(Clone, Serialize, Debug)]
36pub struct ChoiceField {
37    pub base: FieldConfig,
38    pub choices: Vec<ChoiceOption>,
39    pub multiple: bool,
40}
41
42impl ChoiceField {
43    /// Creates an empty dropdown. Add options with [`choices`](ChoiceField::choices) or [`add_choice`](ChoiceField::add_choice).
44    pub fn new(name: &str) -> Self {
45        Self {
46            base: FieldConfig::new(name, "select", "base_select.html"),
47            choices: Vec::new(),
48            multiple: false,
49        }
50    }
51
52    /// Enables `<select multiple>`. Submitted value is a comma-separated string.
53    pub fn multiple(mut self) -> Self {
54        self.multiple = true;
55        self.base.type_field = "select-multiple".to_string();
56        self
57    }
58
59    /// Replaces all options at once.
60    pub fn choices(mut self, choices: Vec<ChoiceOption>) -> Self {
61        self.choices = choices;
62        self
63    }
64
65    /// Appends a single option.
66    pub fn add_choice(mut self, value: &str, label: &str) -> Self {
67        self.choices.push(ChoiceOption::new(value, label));
68        self
69    }
70
71    /// Overrides the auto-generated label.
72    pub fn label(mut self, label: &str) -> Self {
73        self.base.label = label.to_string();
74        self
75    }
76
77    /// Marks the field as required (empty selection fails validation).
78    pub fn required(mut self) -> Self {
79        self.set_required(true, None);
80        self
81    }
82}
83
84impl CommonFieldConfig for ChoiceField {
85    fn get_field_config(&self) -> &FieldConfig {
86        &self.base
87    }
88
89    fn get_field_config_mut(&mut self) -> &mut FieldConfig {
90        &mut self.base
91    }
92}
93
94#[async_trait]
95impl FormField for ChoiceField {
96    /// A `multiple()` select sends one value per selected option, which reach
97    /// the field joined as `"a,b"`: each one is marked selected for re-display.
98    fn set_value(&mut self, value: &str) {
99        self.base.value = value.to_string();
100        if self.multiple {
101            let selected: Vec<&str> = value.split(',').map(str::trim).collect();
102            for choice in &mut self.choices {
103                choice.selected = selected.contains(&choice.value.as_str());
104            }
105        }
106    }
107
108    async fn validate(&mut self) -> bool {
109        let val = self.base.value.trim();
110
111        if self.base.is_required.choice && val.is_empty() {
112            let msg = self
113                .base
114                .is_required
115                .message
116                .clone()
117                .unwrap_or_else(|| t("forms.select_required").to_string());
118            self.set_error(msg);
119            return false;
120        }
121
122        if !val.is_empty() && self.multiple {
123            // Every selected value must be one of the choices, not the joined string.
124            for selected in val.split(',').map(str::trim) {
125                if !self.choices.iter().any(|c| c.value == selected) {
126                    self.set_error(tf("forms.choice_invalid_value", &[selected]));
127                    return false;
128                }
129            }
130        } else if !val.is_empty() {
131            // Check that the value exists in the choices
132            let valid = self.choices.iter().any(|c| c.value == val);
133            if !valid {
134                self.set_error(t("forms.choice_invalid").to_string());
135                return false;
136            }
137        }
138
139        self.clear_error();
140        true
141    }
142
143    fn render(&self, tera: &ATera) -> Result<String, String> {
144        let mut context = self.base_context();
145        context.insert("choices", &self.choices);
146        context.insert("multiple", &self.multiple);
147
148        tera.render(&self.base.template_name, &context)
149            .map_err(|e| {
150                tf(
151                    "forms.finalize_error",
152                    &[&self.base.template_name, &e.to_string()],
153                )
154                .to_string()
155            })
156    }
157}
158
159/// A group of `<input type="radio">` buttons where one option can be selected.
160#[derive(Clone, Serialize, Debug)]
161pub struct RadioField {
162    pub base: FieldConfig,
163    pub choices: Vec<ChoiceOption>,
164}
165
166impl RadioField {
167    /// Creates an empty radio group. Add options with [`choices`](RadioField::choices) or [`add_choice`](RadioField::add_choice).
168    pub fn new(name: &str) -> Self {
169        Self {
170            base: FieldConfig::new(name, "radio", "base_radio.html"),
171            choices: Vec::new(),
172        }
173    }
174
175    /// Replaces all options at once.
176    pub fn choices(mut self, choices: Vec<ChoiceOption>) -> Self {
177        self.choices = choices;
178        self
179    }
180
181    /// Appends a single option.
182    pub fn add_choice(mut self, value: &str, label: &str) -> Self {
183        self.choices.push(ChoiceOption::new(value, label));
184        self
185    }
186
187    /// Overrides the auto-generated label.
188    pub fn label(mut self, label: &str) -> Self {
189        self.base.label = label.to_string();
190        self
191    }
192
193    /// Marks the field as required (no selection fails validation).
194    pub fn required(mut self) -> Self {
195        self.set_required(true, None);
196        self
197    }
198}
199
200impl CommonFieldConfig for RadioField {
201    fn get_field_config(&self) -> &FieldConfig {
202        &self.base
203    }
204
205    fn get_field_config_mut(&mut self) -> &mut FieldConfig {
206        &mut self.base
207    }
208}
209
210#[async_trait]
211impl FormField for RadioField {
212    async fn validate(&mut self) -> bool {
213        let val = self.base.value.trim();
214
215        if self.base.is_required.choice && val.is_empty() {
216            let msg = self
217                .base
218                .is_required
219                .message
220                .clone()
221                .unwrap_or_else(|| t("forms.select_required").to_string());
222            self.set_error(msg);
223            return false;
224        }
225
226        if !val.is_empty() {
227            let valid = self.choices.iter().any(|c| c.value == val);
228            if !valid {
229                self.set_error(t("forms.choice_invalid").to_string());
230                return false;
231            }
232        }
233
234        self.clear_error();
235        true
236    }
237
238    fn render(&self, tera: &ATera) -> Result<String, String> {
239        let mut context = self.base_context();
240        context.insert("choices", &self.choices);
241        context.insert("meta", &self.to_json_meta());
242        tera.render(&self.base.template_name, &context)
243            .map_err(|e| {
244                tf(
245                    "forms.finalize_error",
246                    &[&self.base.template_name, &e.to_string()],
247                )
248                .to_string()
249            })
250    }
251
252    fn to_json_value(&self) -> Value {
253        json!(self.base.value)
254    }
255
256    fn to_json_required(&self) -> Value {
257        json!(self.base.is_required)
258    }
259
260    fn to_json_attributes(&self) -> Value {
261        json!(self.base.html_attributes)
262    }
263}
264
265/// A group of checkboxes where multiple options can be selected simultaneously. Submitted value is a comma-separated string.
266#[derive(Clone, Serialize, Debug)]
267pub struct CheckboxField {
268    pub base: FieldConfig,
269    pub choices: Vec<ChoiceOption>,
270}
271
272impl CheckboxField {
273    /// Creates an empty checkbox group. Add options with [`choices`](CheckboxField::choices) or [`add_choice`](CheckboxField::add_choice).
274    pub fn new(name: &str) -> Self {
275        Self {
276            base: FieldConfig::new(name, "checkbox", "base_checkbox.html"),
277            choices: Vec::new(),
278        }
279    }
280
281    /// Replaces all options at once.
282    pub fn choices(mut self, choices: Vec<ChoiceOption>) -> Self {
283        self.choices = choices;
284        self
285    }
286
287    /// Appends a single option.
288    pub fn add_choice(mut self, value: &str, label: &str) -> Self {
289        self.choices.push(ChoiceOption::new(value, label));
290        self
291    }
292
293    /// Overrides the auto-generated label.
294    pub fn label(mut self, label: &str) -> Self {
295        self.base.label = label.to_string();
296        self
297    }
298
299    /// Marks the field as required (no selection fails validation).
300    pub fn required(mut self) -> Self {
301        self.set_required(true, None);
302        self
303    }
304}
305impl CommonFieldConfig for CheckboxField {
306    fn get_field_config(&self) -> &FieldConfig {
307        &self.base
308    }
309
310    fn get_field_config_mut(&mut self) -> &mut FieldConfig {
311        &mut self.base
312    }
313}
314
315#[async_trait]
316impl FormField for CheckboxField {
317    fn set_value(&mut self, value: &str) {
318        // Expected format: "value1,value2,value3"
319        self.base.value = value.to_string();
320        let selected_values: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
321
322        for choice in &mut self.choices {
323            choice.selected = selected_values.contains(&choice.value.as_str());
324        }
325    }
326
327    async fn validate(&mut self) -> bool {
328        let val = self.base.value.trim();
329
330        if self.base.is_required.choice && val.is_empty() {
331            let msg = self
332                .base
333                .is_required
334                .message
335                .clone()
336                .unwrap_or_else(|| t("forms.checkbox_required").to_string());
337            self.set_error(msg);
338            return false;
339        }
340
341        if !val.is_empty() {
342            // Check that all selected values exist
343            let selected_values: Vec<&str> = val.split(',').map(|s| s.trim()).collect();
344            for sel_val in selected_values {
345                if !self.choices.iter().any(|c| c.value == sel_val) {
346                    self.set_error(tf("forms.choice_invalid_value", &[sel_val]));
347                    return false;
348                }
349            }
350        }
351
352        self.set_error("".into());
353        true
354    }
355
356    fn render(&self, tera: &ATera) -> Result<String, String> {
357        let mut context = self.base_context();
358        context.insert("choices", &self.choices);
359        context.insert("meta", &self.to_json_meta());
360
361        tera.render(&self.base.template_name, &context)
362            .map_err(|e| {
363                tf(
364                    "forms.finalize_error",
365                    &[&self.base.template_name, &e.to_string()],
366                )
367                .to_string()
368            })
369    }
370}