Skip to main content

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)]
134// #[derive(Clone)]
135pub struct DataSection {
136    pub label: String,
137    pub fields: Vec<DataField>,
138    pub error: Option<String>,
139    pub render: Option<Rc<dyn Fn(Vec<DataField>) -> DomElement>>,
140    pub fieldset_style: FieldsetStyle,
141    pub fieldset_css: Option<Css>,
142    pub new_group: bool,
143}
144
145/// A single field in form section.
146#[derive(Clone)]
147pub struct DataField {
148    pub key: String,
149    pub value: DataFieldValue,
150}
151
152impl DataSection {
153    /// Create a new form section without fields.
154    pub fn new(label: impl Into<String>) -> Self {
155        Self {
156            label: label.into(),
157            ..Default::default()
158        }
159    }
160
161    /// Create a new form section with single string field.
162    pub fn with_string_field(
163        label: impl Into<String>,
164        key: impl Into<String>,
165        original_value: impl Into<String>,
166    ) -> Self {
167        let value = original_value.into();
168        Self {
169            label: label.into(),
170            fields: vec![DataField {
171                key: key.into(),
172                value: DataFieldValue::String(StringValue {
173                    value: Value::new(value.clone()),
174                    original_value: Rc::new(value),
175                }),
176            }],
177            ..Default::default()
178        }
179    }
180
181    /// Create a new form section with single optional string field.
182    pub fn with_opt_string_field(
183        label: impl Into<String>,
184        key: impl Into<String>,
185        original_value: &Option<String>,
186    ) -> Self {
187        Self::with_string_field(label, key, original_value.clone().unwrap_or_default())
188    }
189
190    pub fn add_field(mut self, key: impl Into<String>, value: DataFieldValue) -> Self {
191        self.fields.push(DataField {
192            key: key.into(),
193            value,
194        });
195        self
196    }
197
198    /// Add another string field to form section (text input).
199    pub fn add_string_field(
200        mut self,
201        key: impl Into<String>,
202        original_value: impl Into<String>,
203    ) -> Self {
204        let value = original_value.into();
205        self.fields.push(DataField {
206            key: key.into(),
207            value: DataFieldValue::String(StringValue {
208                value: Value::new(value.clone()),
209                original_value: Rc::new(value),
210            }),
211        });
212        self
213    }
214
215    /// Add another optional string field to form section (text input).
216    pub fn add_opt_string_field(
217        self,
218        key: impl Into<String>,
219        original_value: &Option<String>,
220    ) -> Self {
221        self.add_string_field(key, original_value.clone().unwrap_or_default())
222    }
223
224    /// Add another list field to form section (dropdown with options).
225    pub fn add_list_field(
226        mut self,
227        key: impl Into<String>,
228        original_value: Option<impl Into<String>>,
229        options: Vec<String>,
230    ) -> Self {
231        let value = original_value.map(|s| s.into());
232        let options = Computed::from(move |_ctx| options.clone());
233        self.fields.push(DataField {
234            key: key.into(),
235            value: DataFieldValue::List(ListValue {
236                value: Value::new(value.clone().unwrap_or_default()),
237                original_value: value.map(Rc::new),
238                options,
239            }),
240        });
241        self
242    }
243
244    /// Add another dict field to form section based on static (non-reactive) dictionary
245    /// Renders dropdown with options, value are stored as integer.
246    pub fn add_static_dict_field(
247        self,
248        key: impl Into<String>,
249        original_value: Option<i64>,
250        options: Vec<(i64, String)>,
251    ) -> Self {
252        let options = Computed::from(move |_ctx| options.clone());
253        self.add_dict_field(key, original_value, options)
254    }
255
256    /// Add another dict field to form section based on reactive dictionary
257    /// Renders dropdown with options, value are stored as integer.
258    pub fn add_dict_field(
259        mut self,
260        key: impl Into<String>,
261        original_value: Option<i64>,
262        options: Computed<Vec<(i64, String)>>,
263    ) -> Self {
264        self.fields.push(DataField {
265            key: key.into(),
266            value: DataFieldValue::Dict(DictValue {
267                value: Value::new(original_value.unwrap_or_default()),
268                original_value: original_value.map(Rc::new),
269                options,
270            }),
271        });
272        self
273    }
274
275    /// Add multiselect field to form section (multiple search inputs, value stored as integer).
276    pub fn add_multiselect_field(
277        mut self,
278        key: impl Into<String>,
279        original_value: Vec<i64>,
280        options: Computed<HashMap<i64, String>>,
281        add_label: impl Into<String>,
282    ) -> Self {
283        self.fields.push(DataField {
284            key: key.into(),
285            value: DataFieldValue::Multi(MultiValue {
286                value: Value::new(original_value.iter().cloned().map(Value::new).collect()),
287                original_value: Rc::new(original_value),
288                options,
289                add_label: Rc::new(add_label.into()),
290            }),
291        });
292        self
293    }
294
295    /// Add another bool field to form section (checkbox input).
296    pub fn add_bool_field(
297        mut self,
298        key: impl Into<String>,
299        original_value: Option<impl Into<bool>>,
300    ) -> Self {
301        let value = original_value.map(|b| b.into());
302        self.fields.push(DataField {
303            key: key.into(),
304            value: DataFieldValue::Bool(BoolValue {
305                value: Value::new(value.unwrap_or_default()),
306                original_value: value.map(Rc::new),
307            }),
308        });
309        self
310    }
311
312    /// Add another image field to form section.
313    pub fn add_image_field(
314        mut self,
315        key: impl Into<String>,
316        original_value: Option<impl Into<String>>,
317    ) -> Self {
318        let value = original_value.map(|l| l.into());
319        self.fields.push(DataField {
320            key: key.into(),
321            value: DataFieldValue::Image(ImageValue {
322                value: Value::new(None),
323                original_link: value.map(Rc::new),
324                component_params: None,
325            }),
326        });
327        self
328    }
329
330    /// Set [FieldsetStyle] for this section.
331    pub fn set_fieldset_style(mut self, fieldset_style: FieldsetStyle) -> Self {
332        self.fieldset_style = fieldset_style;
333        self
334    }
335
336    /// Set [Css] for fields container for this section.
337    pub fn set_fieldset_css(mut self, fieldset_css: Css) -> Self {
338        self.fieldset_css = Some(fieldset_css);
339        self
340    }
341
342    /// This section starts a new section group (Form adds a horizontal rule)
343    pub fn starts_new_group(mut self) -> Self {
344        self.new_group = true;
345        self
346    }
347}