vertigo_forms/form/data/
form_data.rs

1use std::{collections::HashMap, rc::Rc};
2use vertigo::{Computed, Css, DomElement, Value, transaction};
3
4use super::{
5    DataFieldValue, FormExport,
6    data_field::{BoolValue, DictValue, ImageValue, ListValue, MultiValue, StringValue},
7};
8
9/// Used to define structure of a [Form](super::Form).
10///
11/// Example:
12///
13/// ```rust
14/// use vertigo_forms::form::{DataSection, FieldsetStyle, FormData};
15///
16/// #[derive(Clone, PartialEq)]
17/// pub struct MyModel {
18///     pub slug: String,
19///     pub name: String,
20///     pub dimension_x: String,
21///     pub dimension_y: String,
22/// }
23///
24/// impl From<&MyModel> for FormData {
25///     fn from(value: &MyModel) -> Self {
26///         Self::default()
27///             .with(DataSection::with_string_field("Slug", "slug", &value.slug))
28///             .with(DataSection::with_string_field("Name", "name", &value.name))
29///             .with(
30///                 DataSection::with_string_field("Dimensions", "dimension_x", &value.dimension_x)
31///                     .add_string_field("dimension_y", &value.dimension_y)
32///                     .set_fieldset_style(FieldsetStyle::Dimensions),
33///             )
34///     }
35/// }
36/// ```
37///
38/// See story book for more examples.
39#[derive(Default)]
40pub struct FormData {
41    pub sections: Vec<DataSection>,
42    pub tabs: Vec<(String, Rc<Vec<DataSection>>)>,
43    pub top_controls: ControlsConfig,
44    pub bottom_controls: ControlsConfig,
45}
46
47#[derive(Default)]
48pub struct ControlsConfig {
49    pub css: Option<Css>,
50    pub submit: bool,
51    pub delete: bool,
52}
53
54impl ControlsConfig {
55    pub fn full() -> Self {
56        Self {
57            css: None,
58            submit: true,
59            delete: true,
60        }
61    }
62
63    pub fn with_css(mut self, css: Css) -> Self {
64        self.css = Some(css);
65        self
66    }
67}
68
69impl FormData {
70    /// Add new data section (outside of tabs)
71    pub fn with(mut self, section: DataSection) -> Self {
72        self.sections.push(section);
73        self
74    }
75
76    /// Add new tab with sections
77    pub fn add_tab(mut self, tab_label: impl Into<String>, sections: Vec<DataSection>) -> Self {
78        self.tabs.push((tab_label.into(), Rc::new(sections)));
79        self
80    }
81
82    pub fn add_top_controls(mut self) -> Self {
83        self.top_controls = ControlsConfig::full();
84        self
85    }
86
87    pub fn add_top_controls_styled(mut self, css: Css) -> Self {
88        self.top_controls = ControlsConfig::full().with_css(css);
89        self
90    }
91
92    pub fn add_bottom_controls(mut self) -> Self {
93        self.bottom_controls = ControlsConfig::full();
94        self
95    }
96
97    pub fn add_bottom_controls_styled(mut self, css: Css) -> Self {
98        self.bottom_controls = ControlsConfig::full().with_css(css);
99        self
100    }
101
102    pub fn export(&self) -> FormExport {
103        let mut hash_map = HashMap::new();
104        transaction(|ctx| {
105            for (_, sections) in &self.tabs {
106                for section in sections.iter() {
107                    for field in &section.fields {
108                        hash_map.insert(field.key.clone(), field.value.export(ctx));
109                    }
110                }
111            }
112            for section in &self.sections {
113                for field in &section.fields {
114                    hash_map.insert(field.key.clone(), field.value.export(ctx));
115                }
116            }
117        });
118        FormExport::new(hash_map)
119    }
120}
121
122/// Presets for rendering fields in a field set.
123#[derive(Clone, Copy, Default, PartialEq)]
124pub enum FieldsetStyle {
125    /// Just one after another (piled)
126    #[default]
127    Plain,
128    /// Interspersed with "x" character
129    Dimensions,
130}
131
132/// A section of form with label and a field (or field set).
133#[derive(Default)]
134pub struct DataSection {
135    pub label: String,
136    pub fields: Vec<DataField>,
137    pub error: Option<String>,
138    pub render: Option<Rc<dyn Fn(Vec<DataField>) -> DomElement>>,
139    pub fieldset_style: FieldsetStyle,
140    pub fieldset_css: Option<Css>,
141    pub new_group: bool,
142}
143
144/// A single field in form section.
145#[derive(Clone)]
146pub struct DataField {
147    pub key: String,
148    pub value: DataFieldValue,
149}
150
151impl DataSection {
152    /// Create a new form section without fields.
153    pub fn new(label: impl Into<String>) -> Self {
154        Self {
155            label: label.into(),
156            ..Default::default()
157        }
158    }
159
160    /// Create a new form section with single string field.
161    pub fn with_string_field(
162        label: impl Into<String>,
163        key: impl Into<String>,
164        original_value: impl Into<String>,
165    ) -> Self {
166        let value = original_value.into();
167        Self {
168            label: label.into(),
169            fields: vec![DataField {
170                key: key.into(),
171                value: DataFieldValue::String(StringValue {
172                    value: Value::new(value.clone()),
173                    original_value: Rc::new(value),
174                }),
175            }],
176            ..Default::default()
177        }
178    }
179
180    /// Create a new form section with single optional string field.
181    pub fn with_opt_string_field(
182        label: impl Into<String>,
183        key: impl Into<String>,
184        original_value: &Option<String>,
185    ) -> Self {
186        Self::with_string_field(label, key, original_value.clone().unwrap_or_default())
187    }
188
189    pub fn add_field(mut self, key: impl Into<String>, value: DataFieldValue) -> Self {
190        self.fields.push(DataField {
191            key: key.into(),
192            value,
193        });
194        self
195    }
196
197    /// Add another string field to form section (text input).
198    pub fn add_string_field(
199        mut self,
200        key: impl Into<String>,
201        original_value: impl Into<String>,
202    ) -> Self {
203        let value = original_value.into();
204        self.fields.push(DataField {
205            key: key.into(),
206            value: DataFieldValue::String(StringValue {
207                value: Value::new(value.clone()),
208                original_value: Rc::new(value),
209            }),
210        });
211        self
212    }
213
214    /// Add another optional string field to form section (text input).
215    pub fn add_opt_string_field(
216        self,
217        key: impl Into<String>,
218        original_value: &Option<String>,
219    ) -> Self {
220        self.add_string_field(key, original_value.clone().unwrap_or_default())
221    }
222
223    /// Add another list field to form section (dropdown with options).
224    pub fn add_list_field(
225        mut self,
226        key: impl Into<String>,
227        original_value: Option<impl Into<String>>,
228        options: Vec<String>,
229    ) -> Self {
230        let value = original_value.map(|s| s.into());
231        let options = Computed::from(move |_ctx| options.clone());
232        self.fields.push(DataField {
233            key: key.into(),
234            value: DataFieldValue::List(ListValue {
235                value: Value::new(value.clone().unwrap_or_default()),
236                original_value: value.map(Rc::new),
237                options,
238            }),
239        });
240        self
241    }
242
243    /// Add another dict field to form section based on static (non-reactive) dictionary
244    /// Renders dropdown with options, value are stored as integer.
245    pub fn add_static_dict_field(
246        self,
247        key: impl Into<String>,
248        original_value: Option<i64>,
249        options: Vec<(i64, String)>,
250    ) -> Self {
251        let options = Computed::from(move |_ctx| options.clone());
252        self.add_dict_field(key, original_value, options)
253    }
254
255    /// Add another dict field to form section based on reactive dictionary
256    /// Renders dropdown with options, value are stored as integer.
257    pub fn add_dict_field(
258        mut self,
259        key: impl Into<String>,
260        original_value: Option<i64>,
261        options: Computed<Vec<(i64, String)>>,
262    ) -> Self {
263        self.fields.push(DataField {
264            key: key.into(),
265            value: DataFieldValue::Dict(DictValue {
266                value: Value::new(original_value.unwrap_or_default()),
267                original_value: original_value.map(Rc::new),
268                options,
269            }),
270        });
271        self
272    }
273
274    /// Add multiselect field to form section (multiple search inputs, value stored as integer).
275    pub fn add_multiselect_field(
276        mut self,
277        key: impl Into<String>,
278        original_value: Vec<i64>,
279        options: Computed<HashMap<i64, String>>,
280        add_label: impl Into<String>,
281    ) -> Self {
282        self.fields.push(DataField {
283            key: key.into(),
284            value: DataFieldValue::Multi(MultiValue {
285                value: Value::new(original_value.iter().cloned().map(Value::new).collect()),
286                original_value: Rc::new(original_value),
287                options,
288                add_label: Rc::new(add_label.into()),
289            }),
290        });
291        self
292    }
293
294    /// Add another bool field to form section (checkbox input).
295    pub fn add_bool_field(
296        mut self,
297        key: impl Into<String>,
298        original_value: Option<impl Into<bool>>,
299    ) -> Self {
300        let value = original_value.map(|b| b.into());
301        self.fields.push(DataField {
302            key: key.into(),
303            value: DataFieldValue::Bool(BoolValue {
304                value: Value::new(value.unwrap_or_default()),
305                original_value: value.map(Rc::new),
306            }),
307        });
308        self
309    }
310
311    /// Add another image field to form section.
312    pub fn add_image_field(
313        mut self,
314        key: impl Into<String>,
315        original_value: Option<impl Into<String>>,
316    ) -> Self {
317        let value = original_value.map(|l| l.into());
318        self.fields.push(DataField {
319            key: key.into(),
320            value: DataFieldValue::Image(ImageValue {
321                value: Value::new(None),
322                original_link: value.map(Rc::new),
323                component_params: None,
324            }),
325        });
326        self
327    }
328
329    /// Set [FieldsetStyle] for this section.
330    pub fn set_fieldset_style(mut self, fieldset_style: FieldsetStyle) -> Self {
331        self.fieldset_style = fieldset_style;
332        self
333    }
334
335    /// Set [Css] for fields container for this section.
336    pub fn set_fieldset_css(mut self, fieldset_css: Css) -> Self {
337        self.fieldset_css = Some(fieldset_css);
338        self
339    }
340
341    /// This section starts a new section group (Form adds a horizontal rule)
342    pub fn starts_new_group(mut self) -> Self {
343        self.new_group = true;
344        self
345    }
346}