vertigo_forms/form/
mod.rs1use std::rc::Rc;
9use vertigo::{AttrGroup, Computed, Css, Value, bind, bind_rc, component, css, dom};
10
11use crate::{TabsParams, ValidationErrors};
12
13mod data;
14pub use data::*;
15
16mod render;
17pub use render::*;
18
19#[derive(Clone)]
20pub struct FormParams<T: 'static> {
21 pub css: Css,
22 pub add_css: Css,
23 pub add_section_css: Css,
24 pub submit_label: Rc<String>,
25 pub on_delete: Option<Rc<dyn Fn()>>,
26 pub delete_label: Rc<String>,
27 pub validate: Option<ValidateFunc<T>>,
28 pub validation_errors: Value<ValidationErrors>,
29 pub operation: Option<Value<Operation>>,
30 pub saving_label: Rc<String>,
31 pub saved_label: Rc<String>,
32 pub tabs_params: Option<TabsParams>,
33}
34
35impl<T: 'static> Default for FormParams<T> {
36 fn default() -> Self {
37 Self {
38 css: css! { "
39 display: grid;
40 grid-template-rows: auto 1fr;
41 gap: 5px;
42 " },
43 add_css: Css::default(),
44 add_section_css: Css::default(),
45 submit_label: Rc::new("Submit".to_string()),
46 on_delete: None,
47 delete_label: Rc::new("Delete".to_string()),
48 validate: None,
49 validation_errors: Default::default(),
50 operation: Default::default(),
51 saving_label: Rc::new("Saving...".to_string()),
52 saved_label: Rc::new("Saved".to_string()),
53 tabs_params: None,
54 }
55 }
56}
57
58#[component]
66pub fn ModelForm<T: Clone + PartialEq>(
67 model: Computed<T>,
68 on_submit: Rc<dyn Fn(T)>,
69 params: FormParams<T>,
70 f: AttrGroup,
71 s: AttrGroup,
72) where
73 FormData: From<T>,
74 T: From<FormExport> + 'static,
75{
76 model.render_value(move |model| {
77 let form_data = Rc::new(FormData::from(model));
78
79 let on_submit = bind_rc!(on_submit, form_data, |form_export: FormExport| {
80 on_submit(T::from(form_export));
81 });
82
83 let mut form_component = Form {
84 form_data,
85 on_submit,
86 params: params.clone(),
87 }
88 .into_component();
89
90 form_component.f = f.clone();
91 form_component.s = s.clone();
92
93 form_component.mount()
94 })
95}
96
97#[component]
104pub fn Form<T>(
105 form_data: Rc<FormData>,
106 on_submit: Rc<dyn Fn(FormExport)>,
107 params: FormParams<T>,
108 f: AttrGroup,
110 s: AttrGroup,
112) where
113 T: From<FormExport> + 'static,
114{
115 let subgrid_css = css! {"
116 display: grid;
117 grid-template-columns: subgrid;
118 grid-column: span 2 / span 2;
119 "};
120
121 let validation_errors = params.validation_errors.clone();
122
123 let controls = |params: &FormParams<T>, c_config: &ControlsConfig| {
124 let mut controls = vec![];
125
126 let ctrl_item_css = css! {"
127 margin: 5px;
128 "};
129
130 if c_config.submit {
131 controls.push(dom! {
132 <input css={&ctrl_item_css} type="submit" value={¶ms.submit_label} />
133 });
134 }
135 if c_config.delete
136 && let Some(on_click) = params.on_delete.clone()
137 {
138 controls.push(dom! {
139 <input css={&ctrl_item_css} type="submit" value={¶ms.delete_label} on_click={move |_| on_click()} />
140 });
141 }
142
143 let errors = validation_errors
144 .render_value_option(|errs| errs.get("submit").map(|err| dom! { <span>{err}</span> }));
145
146 let operation_str = params.operation.as_ref().map(|operation| {
147 bind!(
148 params.saving_label,
149 params.saved_label,
150 operation.render_value_option(move |oper| {
151 let mut css = ctrl_item_css.clone();
152 match oper {
153 Operation::Saving => Some(saving_label.clone()),
154 Operation::Success => Some(saved_label.clone()),
155 Operation::Error(err) => {
156 css += css! {"color: red;"};
157 Some(err)
158 }
159 _ => None,
160 }
161 .map(|operation_str| dom! { <span {css}>{operation_str}</span> })
162 })
163 )
164 });
165
166 if controls.is_empty() {
167 None
168 } else {
169 let mut css_controls = css!("grid-column: span 2;");
170 if let Some(custom_css) = &c_config.css {
171 css_controls += custom_css;
172 }
173 Some(dom! {
174 <div css={css_controls}>
175 {..controls}
176 {errors}
177 {..operation_str}
178 </div>
179 })
180 }
181 };
182
183 let top_controls = controls(¶ms, &form_data.top_controls);
184 let bottom_controls = controls(¶ms, &form_data.bottom_controls);
185
186 let section_css = subgrid_css + params.add_section_css;
187
188 let fields = fields(
189 &form_data.sections,
190 &s,
191 validation_errors.clone(),
192 §ion_css,
193 );
194
195 let tabs = tabs(
196 &form_data.tabs,
197 ¶ms.tabs_params,
198 &s,
199 validation_errors.clone(),
200 §ion_css,
201 ¶ms.css.clone(),
202 );
203
204 let form_css = params.css + params.add_css;
205
206 let on_submit = bind_rc!(form_data, validation_errors, || {
207 params
208 .operation
209 .as_ref()
210 .inspect(|operation| operation.set(Operation::Saving));
211 let model = form_data.export();
212 let valid = if let Some(validate) = ¶ms.validate {
213 validate(&model.clone().into(), validation_errors.clone())
214 } else {
215 true
216 };
217 if valid {
218 on_submit(model);
219 }
220 });
221
222 dom! {
223 <form css={form_css} on_submit={on_submit} {..f}>
224 {..top_controls}
225 {..fields}
226 {..tabs}
227 {..bottom_controls}
228 </form>
229 }
230}