1use super::error::ValidationErrors;
7use super::field::{FieldKind, FormField, InputType, SelectOption};
8use super::render::FormRenderer;
9
10#[derive(Debug, Clone)]
33pub struct FormBuilder<'a> {
34 pub(crate) action: String,
36 pub(crate) method: String,
38 pub(crate) id: Option<String>,
40 pub(crate) class: Option<String>,
42 pub(crate) csrf_token: Option<String>,
44 pub(crate) enctype: Option<String>,
46 pub(crate) fields: Vec<FormField>,
48 pub(crate) submit_text: Option<String>,
50 pub(crate) submit_class: Option<String>,
52 pub(crate) errors: Option<&'a ValidationErrors>,
54 pub(crate) htmx: HtmxFormAttrs,
56 pub(crate) custom_attrs: Vec<(String, String)>,
58 pub(crate) htmx_validate: bool,
60 pub(crate) novalidate: bool,
62}
63
64#[derive(Debug, Clone, Default)]
66pub struct HtmxFormAttrs {
67 pub get: Option<String>,
69 pub post: Option<String>,
71 pub put: Option<String>,
73 pub delete: Option<String>,
75 pub patch: Option<String>,
77 pub target: Option<String>,
79 pub swap: Option<String>,
81 pub trigger: Option<String>,
83 pub indicator: Option<String>,
85 pub push_url: Option<String>,
87 pub confirm: Option<String>,
89 pub disabled_elt: Option<String>,
91}
92
93impl<'a> FormBuilder<'a> {
94 #[must_use]
96 pub fn new(action: impl Into<String>, method: impl Into<String>) -> Self {
97 Self {
98 action: action.into(),
99 method: method.into(),
100 id: None,
101 class: None,
102 csrf_token: None,
103 enctype: None,
104 fields: Vec::new(),
105 submit_text: None,
106 submit_class: None,
107 errors: None,
108 htmx: HtmxFormAttrs::default(),
109 custom_attrs: Vec::new(),
110 htmx_validate: false,
111 novalidate: false,
112 }
113 }
114
115 #[must_use]
117 pub fn id(mut self, id: impl Into<String>) -> Self {
118 self.id = Some(id.into());
119 self
120 }
121
122 #[must_use]
124 pub fn class(mut self, class: impl Into<String>) -> Self {
125 self.class = Some(class.into());
126 self
127 }
128
129 #[must_use]
131 pub fn csrf_token(mut self, token: impl Into<String>) -> Self {
132 self.csrf_token = Some(token.into());
133 self
134 }
135
136 #[must_use]
138 pub fn enctype(mut self, enctype: impl Into<String>) -> Self {
139 self.enctype = Some(enctype.into());
140 self
141 }
142
143 #[must_use]
145 pub fn multipart(mut self) -> Self {
146 self.enctype = Some("multipart/form-data".into());
147 self
148 }
149
150 #[must_use]
152 pub const fn errors(mut self, errors: &'a ValidationErrors) -> Self {
153 self.errors = Some(errors);
154 self
155 }
156
157 #[must_use]
159 pub fn submit(mut self, text: impl Into<String>) -> Self {
160 self.submit_text = Some(text.into());
161 self
162 }
163
164 #[must_use]
166 pub fn submit_class(mut self, class: impl Into<String>) -> Self {
167 self.submit_class = Some(class.into());
168 self
169 }
170
171 #[must_use]
173 pub const fn novalidate(mut self) -> Self {
174 self.novalidate = true;
175 self
176 }
177
178 #[must_use]
180 pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
181 self.custom_attrs.push((name.into(), value.into()));
182 self
183 }
184
185 #[must_use]
191 pub fn htmx_get(mut self, url: impl Into<String>) -> Self {
192 self.htmx.get = Some(url.into());
193 self
194 }
195
196 #[must_use]
198 pub fn htmx_post(mut self, url: impl Into<String>) -> Self {
199 self.htmx.post = Some(url.into());
200 self
201 }
202
203 #[must_use]
205 pub fn htmx_put(mut self, url: impl Into<String>) -> Self {
206 self.htmx.put = Some(url.into());
207 self
208 }
209
210 #[must_use]
212 pub fn htmx_delete(mut self, url: impl Into<String>) -> Self {
213 self.htmx.delete = Some(url.into());
214 self
215 }
216
217 #[must_use]
219 pub fn htmx_patch(mut self, url: impl Into<String>) -> Self {
220 self.htmx.patch = Some(url.into());
221 self
222 }
223
224 #[must_use]
226 pub fn htmx_target(mut self, selector: impl Into<String>) -> Self {
227 self.htmx.target = Some(selector.into());
228 self
229 }
230
231 #[must_use]
233 pub fn htmx_swap(mut self, strategy: impl Into<String>) -> Self {
234 self.htmx.swap = Some(strategy.into());
235 self
236 }
237
238 #[must_use]
240 pub fn htmx_trigger(mut self, trigger: impl Into<String>) -> Self {
241 self.htmx.trigger = Some(trigger.into());
242 self
243 }
244
245 #[must_use]
247 pub fn htmx_indicator(mut self, selector: impl Into<String>) -> Self {
248 self.htmx.indicator = Some(selector.into());
249 self
250 }
251
252 #[must_use]
254 pub fn htmx_push_url(mut self, url: impl Into<String>) -> Self {
255 self.htmx.push_url = Some(url.into());
256 self
257 }
258
259 #[must_use]
261 pub fn htmx_confirm(mut self, message: impl Into<String>) -> Self {
262 self.htmx.confirm = Some(message.into());
263 self
264 }
265
266 #[must_use]
268 pub fn htmx_disabled_elt(mut self, selector: impl Into<String>) -> Self {
269 self.htmx.disabled_elt = Some(selector.into());
270 self
271 }
272
273 #[must_use]
275 pub const fn htmx_validate(mut self) -> Self {
276 self.htmx_validate = true;
277 self
278 }
279
280 #[must_use]
286 pub fn field(self, name: impl Into<String>, input_type: InputType) -> FieldBuilder<'a> {
287 FieldBuilder::new(self, FormField::input(name, input_type))
288 }
289
290 #[must_use]
309 pub fn file(mut self, name: impl Into<String>) -> FileFieldBuilder<'a> {
310 if self.enctype.is_none() {
312 self.enctype = Some("multipart/form-data".into());
313 }
314 FileFieldBuilder::new(self, FormField::input(name, InputType::File))
315 }
316
317 #[must_use]
319 pub fn textarea(self, name: impl Into<String>) -> TextareaBuilder<'a> {
320 TextareaBuilder::new(self, FormField::textarea(name))
321 }
322
323 #[must_use]
325 pub fn select(self, name: impl Into<String>) -> SelectBuilder<'a> {
326 SelectBuilder::new(self, FormField::select(name))
327 }
328
329 #[must_use]
331 pub fn checkbox(self, name: impl Into<String>) -> CheckboxBuilder<'a> {
332 CheckboxBuilder::new(self, FormField::checkbox(name))
333 }
334
335 #[must_use]
337 pub fn hidden(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
338 let mut field = FormField::input(name, InputType::Hidden);
339 field.value = Some(value.into());
340 self.fields.push(field);
341 self
342 }
343
344 #[must_use]
346 pub fn add_field(mut self, field: FormField) -> Self {
347 self.fields.push(field);
348 self
349 }
350
351 #[must_use]
356 pub fn build(self) -> String {
357 FormRenderer::render(&self)
358 }
359
360 pub fn build_with_templates(
383 self,
384 templates: &crate::template::framework::FrameworkTemplates,
385 ) -> Result<String, super::template_render::FormRenderError> {
386 let renderer = super::template_render::TemplateFormRenderer::new(templates);
387 renderer.render(&self)
388 }
389
390 pub fn build_with_templates_and_options(
396 self,
397 templates: &crate::template::framework::FrameworkTemplates,
398 options: super::render::FormRenderOptions,
399 ) -> Result<String, super::template_render::FormRenderError> {
400 let renderer = super::template_render::TemplateFormRenderer::with_options(templates, options);
401 renderer.render(&self)
402 }
403}
404
405pub struct FieldBuilder<'a> {
411 form: FormBuilder<'a>,
412 field: FormField,
413}
414
415impl<'a> FieldBuilder<'a> {
416 const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
417 Self { form, field }
418 }
419
420 #[must_use]
422 pub fn label(mut self, label: impl Into<String>) -> Self {
423 self.field.label = Some(label.into());
424 self
425 }
426
427 #[must_use]
429 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
430 self.field.placeholder = Some(placeholder.into());
431 self
432 }
433
434 #[must_use]
436 pub fn value(mut self, value: impl Into<String>) -> Self {
437 self.field.value = Some(value.into());
438 self
439 }
440
441 #[must_use]
443 pub const fn required(mut self) -> Self {
444 self.field.flags.required = true;
445 self
446 }
447
448 #[must_use]
450 pub const fn disabled(mut self) -> Self {
451 self.field.flags.disabled = true;
452 self
453 }
454
455 #[must_use]
457 pub const fn readonly(mut self) -> Self {
458 self.field.flags.readonly = true;
459 self
460 }
461
462 #[must_use]
464 pub const fn autofocus(mut self) -> Self {
465 self.field.flags.autofocus = true;
466 self
467 }
468
469 #[must_use]
471 pub fn autocomplete(mut self, value: impl Into<String>) -> Self {
472 self.field.autocomplete = Some(value.into());
473 self
474 }
475
476 #[must_use]
478 pub const fn min_length(mut self, len: usize) -> Self {
479 self.field.min_length = Some(len);
480 self
481 }
482
483 #[must_use]
485 pub const fn max_length(mut self, len: usize) -> Self {
486 self.field.max_length = Some(len);
487 self
488 }
489
490 #[must_use]
492 pub fn min(mut self, value: impl Into<String>) -> Self {
493 self.field.min = Some(value.into());
494 self
495 }
496
497 #[must_use]
499 pub fn max(mut self, value: impl Into<String>) -> Self {
500 self.field.max = Some(value.into());
501 self
502 }
503
504 #[must_use]
506 pub fn step(mut self, value: impl Into<String>) -> Self {
507 self.field.step = Some(value.into());
508 self
509 }
510
511 #[must_use]
513 pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
514 self.field.pattern = Some(pattern.into());
515 self
516 }
517
518 #[must_use]
520 pub fn class(mut self, class: impl Into<String>) -> Self {
521 self.field.class = Some(class.into());
522 self
523 }
524
525 #[must_use]
527 pub fn id(mut self, id: impl Into<String>) -> Self {
528 self.field.id = Some(id.into());
529 self
530 }
531
532 #[must_use]
534 pub fn help(mut self, text: impl Into<String>) -> Self {
535 self.field.help_text = Some(text.into());
536 self
537 }
538
539 #[must_use]
541 pub fn data(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
542 self.field.data_attrs.push((name.into(), value.into()));
543 self
544 }
545
546 #[must_use]
548 pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
549 self.field.custom_attrs.push((name.into(), value.into()));
550 self
551 }
552
553 #[must_use]
556 pub fn htmx_get(mut self, url: impl Into<String>) -> Self {
557 self.field.htmx.get = Some(url.into());
558 self
559 }
560
561 #[must_use]
563 pub fn htmx_post(mut self, url: impl Into<String>) -> Self {
564 self.field.htmx.post = Some(url.into());
565 self
566 }
567
568 #[must_use]
570 pub fn htmx_target(mut self, selector: impl Into<String>) -> Self {
571 self.field.htmx.target = Some(selector.into());
572 self
573 }
574
575 #[must_use]
577 pub fn htmx_swap(mut self, strategy: impl Into<String>) -> Self {
578 self.field.htmx.swap = Some(strategy.into());
579 self
580 }
581
582 #[must_use]
584 pub fn htmx_trigger(mut self, trigger: impl Into<String>) -> Self {
585 self.field.htmx.trigger = Some(trigger.into());
586 self
587 }
588
589 #[must_use]
591 pub fn done(mut self) -> FormBuilder<'a> {
592 self.form.fields.push(self.field);
593 self.form
594 }
595}
596
597pub struct TextareaBuilder<'a> {
603 form: FormBuilder<'a>,
604 field: FormField,
605}
606
607impl<'a> TextareaBuilder<'a> {
608 const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
609 Self { form, field }
610 }
611
612 #[must_use]
614 pub fn label(mut self, label: impl Into<String>) -> Self {
615 self.field.label = Some(label.into());
616 self
617 }
618
619 #[must_use]
621 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
622 self.field.placeholder = Some(placeholder.into());
623 self
624 }
625
626 #[must_use]
628 pub fn value(mut self, value: impl Into<String>) -> Self {
629 self.field.value = Some(value.into());
630 self
631 }
632
633 #[must_use]
635 pub const fn required(mut self) -> Self {
636 self.field.flags.required = true;
637 self
638 }
639
640 #[must_use]
642 pub const fn disabled(mut self) -> Self {
643 self.field.flags.disabled = true;
644 self
645 }
646
647 #[must_use]
649 pub const fn rows(mut self, rows: u32) -> Self {
650 if let FieldKind::Textarea {
651 rows: ref mut r, ..
652 } = self.field.kind
653 {
654 *r = Some(rows);
655 }
656 self
657 }
658
659 #[must_use]
661 pub const fn cols(mut self, cols: u32) -> Self {
662 if let FieldKind::Textarea {
663 cols: ref mut c, ..
664 } = self.field.kind
665 {
666 *c = Some(cols);
667 }
668 self
669 }
670
671 #[must_use]
673 pub fn class(mut self, class: impl Into<String>) -> Self {
674 self.field.class = Some(class.into());
675 self
676 }
677
678 #[must_use]
680 pub fn id(mut self, id: impl Into<String>) -> Self {
681 self.field.id = Some(id.into());
682 self
683 }
684
685 #[must_use]
687 pub fn help(mut self, text: impl Into<String>) -> Self {
688 self.field.help_text = Some(text.into());
689 self
690 }
691
692 #[must_use]
694 pub fn done(mut self) -> FormBuilder<'a> {
695 self.form.fields.push(self.field);
696 self.form
697 }
698}
699
700pub struct SelectBuilder<'a> {
706 form: FormBuilder<'a>,
707 field: FormField,
708 selected_value: Option<String>,
709}
710
711impl<'a> SelectBuilder<'a> {
712 const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
713 Self {
714 form,
715 field,
716 selected_value: None,
717 }
718 }
719
720 #[must_use]
722 pub fn label(mut self, label: impl Into<String>) -> Self {
723 self.field.label = Some(label.into());
724 self
725 }
726
727 #[must_use]
729 pub fn option(mut self, value: impl Into<String>, label: impl Into<String>) -> Self {
730 if let FieldKind::Select { ref mut options, .. } = self.field.kind {
731 options.push(SelectOption::new(value, label));
732 }
733 self
734 }
735
736 #[must_use]
738 pub fn placeholder_option(mut self, label: impl Into<String>) -> Self {
739 if let FieldKind::Select { ref mut options, .. } = self.field.kind {
740 options.insert(0, SelectOption::disabled("", label));
741 }
742 self
743 }
744
745 #[must_use]
747 pub fn selected(mut self, value: impl Into<String>) -> Self {
748 self.selected_value = Some(value.into());
749 self
750 }
751
752 #[must_use]
754 pub const fn required(mut self) -> Self {
755 self.field.flags.required = true;
756 self
757 }
758
759 #[must_use]
761 pub const fn disabled(mut self) -> Self {
762 self.field.flags.disabled = true;
763 self
764 }
765
766 #[must_use]
768 pub const fn multiple(mut self) -> Self {
769 if let FieldKind::Select {
770 ref mut multiple, ..
771 } = self.field.kind
772 {
773 *multiple = true;
774 }
775 self
776 }
777
778 #[must_use]
780 pub fn class(mut self, class: impl Into<String>) -> Self {
781 self.field.class = Some(class.into());
782 self
783 }
784
785 #[must_use]
787 pub fn id(mut self, id: impl Into<String>) -> Self {
788 self.field.id = Some(id.into());
789 self
790 }
791
792 #[must_use]
794 pub fn done(mut self) -> FormBuilder<'a> {
795 self.field.value = self.selected_value;
797 self.form.fields.push(self.field);
798 self.form
799 }
800}
801
802pub struct CheckboxBuilder<'a> {
808 form: FormBuilder<'a>,
809 field: FormField,
810}
811
812impl<'a> CheckboxBuilder<'a> {
813 const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
814 Self { form, field }
815 }
816
817 #[must_use]
819 pub fn label(mut self, label: impl Into<String>) -> Self {
820 self.field.label = Some(label.into());
821 self
822 }
823
824 #[must_use]
826 pub fn value(mut self, value: impl Into<String>) -> Self {
827 self.field.value = Some(value.into());
828 self
829 }
830
831 #[must_use]
833 pub const fn checked(mut self) -> Self {
834 if let FieldKind::Checkbox {
835 ref mut checked, ..
836 } = self.field.kind
837 {
838 *checked = true;
839 }
840 self
841 }
842
843 #[must_use]
845 pub const fn required(mut self) -> Self {
846 self.field.flags.required = true;
847 self
848 }
849
850 #[must_use]
852 pub const fn disabled(mut self) -> Self {
853 self.field.flags.disabled = true;
854 self
855 }
856
857 #[must_use]
859 pub fn class(mut self, class: impl Into<String>) -> Self {
860 self.field.class = Some(class.into());
861 self
862 }
863
864 #[must_use]
866 pub fn id(mut self, id: impl Into<String>) -> Self {
867 self.field.id = Some(id.into());
868 self
869 }
870
871 #[must_use]
873 pub fn done(mut self) -> FormBuilder<'a> {
874 self.form.fields.push(self.field);
875 self.form
876 }
877}
878
879pub struct FileFieldBuilder<'a> {
888 form: FormBuilder<'a>,
889 field: FormField,
890}
891
892impl<'a> FileFieldBuilder<'a> {
893 const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
894 Self { form, field }
895 }
896
897 #[must_use]
899 pub fn label(mut self, label: impl Into<String>) -> Self {
900 self.field.label = Some(label.into());
901 self
902 }
903
904 #[must_use]
926 pub fn accept(mut self, types: impl Into<String>) -> Self {
927 self.field.file_attrs.accept = Some(types.into());
928 self
929 }
930
931 #[must_use]
933 pub const fn multiple(mut self) -> Self {
934 self.field.file_attrs.multiple = true;
935 self
936 }
937
938 #[must_use]
943 pub const fn max_size_mb(mut self, size_mb: u32) -> Self {
944 self.field.file_attrs.max_size_mb = Some(size_mb);
945 self
946 }
947
948 #[must_use]
950 pub const fn show_preview(mut self) -> Self {
951 self.field.file_attrs.show_preview = true;
952 self
953 }
954
955 #[must_use]
957 pub const fn drag_drop(mut self) -> Self {
958 self.field.file_attrs.drag_drop = true;
959 self
960 }
961
962 #[must_use]
977 pub fn progress_endpoint(mut self, endpoint: impl Into<String>) -> Self {
978 self.field.file_attrs.progress_endpoint = Some(endpoint.into());
979 self
980 }
981
982 #[must_use]
984 pub const fn required(mut self) -> Self {
985 self.field.flags.required = true;
986 self
987 }
988
989 #[must_use]
991 pub const fn disabled(mut self) -> Self {
992 self.field.flags.disabled = true;
993 self
994 }
995
996 #[must_use]
998 pub fn class(mut self, class: impl Into<String>) -> Self {
999 self.field.class = Some(class.into());
1000 self
1001 }
1002
1003 #[must_use]
1005 pub fn id(mut self, id: impl Into<String>) -> Self {
1006 self.field.id = Some(id.into());
1007 self
1008 }
1009
1010 #[must_use]
1012 pub fn help(mut self, text: impl Into<String>) -> Self {
1013 self.field.help_text = Some(text.into());
1014 self
1015 }
1016
1017 #[must_use]
1019 pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1020 self.field.custom_attrs.push((name.into(), value.into()));
1021 self
1022 }
1023
1024 #[must_use]
1026 pub fn done(mut self) -> FormBuilder<'a> {
1027 self.form.fields.push(self.field);
1028 self.form
1029 }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035
1036 #[test]
1037 fn test_form_builder_basic() {
1038 let form = FormBuilder::new("/test", "POST");
1039 assert_eq!(form.action, "/test");
1040 assert_eq!(form.method, "POST");
1041 }
1042
1043 #[test]
1044 fn test_form_builder_with_id() {
1045 let form = FormBuilder::new("/test", "POST").id("my-form");
1046 assert_eq!(form.id.as_deref(), Some("my-form"));
1047 }
1048
1049 #[test]
1050 fn test_form_builder_csrf() {
1051 let form = FormBuilder::new("/test", "POST").csrf_token("token123");
1052 assert_eq!(form.csrf_token.as_deref(), Some("token123"));
1053 }
1054
1055 #[test]
1056 fn test_field_builder() {
1057 let form = FormBuilder::new("/test", "POST")
1058 .field("email", InputType::Email)
1059 .label("Email")
1060 .required()
1061 .placeholder("test@example.com")
1062 .done();
1063
1064 assert_eq!(form.fields.len(), 1);
1065 let field = &form.fields[0];
1066 assert_eq!(field.name, "email");
1067 assert_eq!(field.label.as_deref(), Some("Email"));
1068 assert!(field.flags.required);
1069 assert_eq!(field.placeholder.as_deref(), Some("test@example.com"));
1070 }
1071
1072 #[test]
1073 fn test_textarea_builder() {
1074 let form = FormBuilder::new("/test", "POST")
1075 .textarea("content")
1076 .label("Content")
1077 .rows(10)
1078 .cols(50)
1079 .done();
1080
1081 assert_eq!(form.fields.len(), 1);
1082 let field = &form.fields[0];
1083 assert!(matches!(
1084 field.kind,
1085 FieldKind::Textarea {
1086 rows: Some(10),
1087 cols: Some(50)
1088 }
1089 ));
1090 }
1091
1092 #[test]
1093 fn test_select_builder() {
1094 let form = FormBuilder::new("/test", "POST")
1095 .select("country")
1096 .label("Country")
1097 .option("us", "United States")
1098 .option("ca", "Canada")
1099 .selected("us")
1100 .done();
1101
1102 assert_eq!(form.fields.len(), 1);
1103 let field = &form.fields[0];
1104 assert!(field.is_select());
1105 assert_eq!(field.value.as_deref(), Some("us"));
1106 }
1107
1108 #[test]
1109 fn test_checkbox_builder() {
1110 let form = FormBuilder::new("/test", "POST")
1111 .checkbox("terms")
1112 .label("I agree")
1113 .checked()
1114 .done();
1115
1116 assert_eq!(form.fields.len(), 1);
1117 let field = &form.fields[0];
1118 assert!(matches!(field.kind, FieldKind::Checkbox { checked: true }));
1119 }
1120
1121 #[test]
1122 fn test_hidden_field() {
1123 let form = FormBuilder::new("/test", "POST").hidden("user_id", "123");
1124
1125 assert_eq!(form.fields.len(), 1);
1126 let field = &form.fields[0];
1127 assert!(matches!(field.kind, FieldKind::Input(InputType::Hidden)));
1128 assert_eq!(field.value.as_deref(), Some("123"));
1129 }
1130
1131 #[test]
1132 fn test_htmx_form_attrs() {
1133 let form = FormBuilder::new("/test", "POST")
1134 .htmx_post("/api/test")
1135 .htmx_target("#result")
1136 .htmx_swap("innerHTML")
1137 .htmx_indicator("#spinner");
1138
1139 assert_eq!(form.htmx.post.as_deref(), Some("/api/test"));
1140 assert_eq!(form.htmx.target.as_deref(), Some("#result"));
1141 assert_eq!(form.htmx.swap.as_deref(), Some("innerHTML"));
1142 assert_eq!(form.htmx.indicator.as_deref(), Some("#spinner"));
1143 }
1144}