use super::error::ValidationErrors;
use super::field::{FieldKind, FormField, InputType, SelectOption};
use super::render::FormRenderer;
#[derive(Debug, Clone)]
pub struct FormBuilder<'a> {
pub(crate) action: String,
pub(crate) method: String,
pub(crate) id: Option<String>,
pub(crate) class: Option<String>,
pub(crate) csrf_token: Option<String>,
pub(crate) enctype: Option<String>,
pub(crate) fields: Vec<FormField>,
pub(crate) submit_text: Option<String>,
pub(crate) submit_class: Option<String>,
pub(crate) errors: Option<&'a ValidationErrors>,
pub(crate) htmx: HtmxFormAttrs,
pub(crate) custom_attrs: Vec<(String, String)>,
pub(crate) htmx_validate: bool,
pub(crate) novalidate: bool,
}
#[derive(Debug, Clone, Default)]
pub struct HtmxFormAttrs {
pub get: Option<String>,
pub post: Option<String>,
pub put: Option<String>,
pub delete: Option<String>,
pub patch: Option<String>,
pub target: Option<String>,
pub swap: Option<String>,
pub trigger: Option<String>,
pub indicator: Option<String>,
pub push_url: Option<String>,
pub confirm: Option<String>,
pub disabled_elt: Option<String>,
}
impl<'a> FormBuilder<'a> {
#[must_use]
pub fn new(action: impl Into<String>, method: impl Into<String>) -> Self {
Self {
action: action.into(),
method: method.into(),
id: None,
class: None,
csrf_token: None,
enctype: None,
fields: Vec::new(),
submit_text: None,
submit_class: None,
errors: None,
htmx: HtmxFormAttrs::default(),
custom_attrs: Vec::new(),
htmx_validate: false,
novalidate: false,
}
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.class = Some(class.into());
self
}
#[must_use]
pub fn csrf_token(mut self, token: impl Into<String>) -> Self {
self.csrf_token = Some(token.into());
self
}
#[must_use]
pub fn enctype(mut self, enctype: impl Into<String>) -> Self {
self.enctype = Some(enctype.into());
self
}
#[must_use]
pub fn multipart(mut self) -> Self {
self.enctype = Some("multipart/form-data".into());
self
}
#[must_use]
pub const fn errors(mut self, errors: &'a ValidationErrors) -> Self {
self.errors = Some(errors);
self
}
#[must_use]
pub fn submit(mut self, text: impl Into<String>) -> Self {
self.submit_text = Some(text.into());
self
}
#[must_use]
pub fn submit_class(mut self, class: impl Into<String>) -> Self {
self.submit_class = Some(class.into());
self
}
#[must_use]
pub const fn novalidate(mut self) -> Self {
self.novalidate = true;
self
}
#[must_use]
pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.custom_attrs.push((name.into(), value.into()));
self
}
#[must_use]
pub fn htmx_get(mut self, url: impl Into<String>) -> Self {
self.htmx.get = Some(url.into());
self
}
#[must_use]
pub fn htmx_post(mut self, url: impl Into<String>) -> Self {
self.htmx.post = Some(url.into());
self
}
#[must_use]
pub fn htmx_put(mut self, url: impl Into<String>) -> Self {
self.htmx.put = Some(url.into());
self
}
#[must_use]
pub fn htmx_delete(mut self, url: impl Into<String>) -> Self {
self.htmx.delete = Some(url.into());
self
}
#[must_use]
pub fn htmx_patch(mut self, url: impl Into<String>) -> Self {
self.htmx.patch = Some(url.into());
self
}
#[must_use]
pub fn htmx_target(mut self, selector: impl Into<String>) -> Self {
self.htmx.target = Some(selector.into());
self
}
#[must_use]
pub fn htmx_swap(mut self, strategy: impl Into<String>) -> Self {
self.htmx.swap = Some(strategy.into());
self
}
#[must_use]
pub fn htmx_trigger(mut self, trigger: impl Into<String>) -> Self {
self.htmx.trigger = Some(trigger.into());
self
}
#[must_use]
pub fn htmx_indicator(mut self, selector: impl Into<String>) -> Self {
self.htmx.indicator = Some(selector.into());
self
}
#[must_use]
pub fn htmx_push_url(mut self, url: impl Into<String>) -> Self {
self.htmx.push_url = Some(url.into());
self
}
#[must_use]
pub fn htmx_confirm(mut self, message: impl Into<String>) -> Self {
self.htmx.confirm = Some(message.into());
self
}
#[must_use]
pub fn htmx_disabled_elt(mut self, selector: impl Into<String>) -> Self {
self.htmx.disabled_elt = Some(selector.into());
self
}
#[must_use]
pub const fn htmx_validate(mut self) -> Self {
self.htmx_validate = true;
self
}
#[must_use]
pub fn field(self, name: impl Into<String>, input_type: InputType) -> FieldBuilder<'a> {
FieldBuilder::new(self, FormField::input(name, input_type))
}
#[must_use]
pub fn file(mut self, name: impl Into<String>) -> FileFieldBuilder<'a> {
if self.enctype.is_none() {
self.enctype = Some("multipart/form-data".into());
}
FileFieldBuilder::new(self, FormField::input(name, InputType::File))
}
#[must_use]
pub fn textarea(self, name: impl Into<String>) -> TextareaBuilder<'a> {
TextareaBuilder::new(self, FormField::textarea(name))
}
#[must_use]
pub fn select(self, name: impl Into<String>) -> SelectBuilder<'a> {
SelectBuilder::new(self, FormField::select(name))
}
#[must_use]
pub fn checkbox(self, name: impl Into<String>) -> CheckboxBuilder<'a> {
CheckboxBuilder::new(self, FormField::checkbox(name))
}
#[must_use]
pub fn hidden(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
let mut field = FormField::input(name, InputType::Hidden);
field.value = Some(value.into());
self.fields.push(field);
self
}
#[must_use]
pub fn add_field(mut self, field: FormField) -> Self {
self.fields.push(field);
self
}
#[must_use]
pub fn build(self) -> String {
FormRenderer::render(&self)
}
pub fn build_with_templates(
self,
templates: &crate::template::framework::FrameworkTemplates,
) -> Result<String, super::template_render::FormRenderError> {
let renderer = super::template_render::TemplateFormRenderer::new(templates);
renderer.render(&self)
}
pub fn build_with_templates_and_options(
self,
templates: &crate::template::framework::FrameworkTemplates,
options: super::render::FormRenderOptions,
) -> Result<String, super::template_render::FormRenderError> {
let renderer = super::template_render::TemplateFormRenderer::with_options(templates, options);
renderer.render(&self)
}
}
pub struct FieldBuilder<'a> {
form: FormBuilder<'a>,
field: FormField,
}
impl<'a> FieldBuilder<'a> {
const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
Self { form, field }
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.field.label = Some(label.into());
self
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.field.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn value(mut self, value: impl Into<String>) -> Self {
self.field.value = Some(value.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.field.flags.required = true;
self
}
#[must_use]
pub const fn disabled(mut self) -> Self {
self.field.flags.disabled = true;
self
}
#[must_use]
pub const fn readonly(mut self) -> Self {
self.field.flags.readonly = true;
self
}
#[must_use]
pub const fn autofocus(mut self) -> Self {
self.field.flags.autofocus = true;
self
}
#[must_use]
pub fn autocomplete(mut self, value: impl Into<String>) -> Self {
self.field.autocomplete = Some(value.into());
self
}
#[must_use]
pub const fn min_length(mut self, len: usize) -> Self {
self.field.min_length = Some(len);
self
}
#[must_use]
pub const fn max_length(mut self, len: usize) -> Self {
self.field.max_length = Some(len);
self
}
#[must_use]
pub fn min(mut self, value: impl Into<String>) -> Self {
self.field.min = Some(value.into());
self
}
#[must_use]
pub fn max(mut self, value: impl Into<String>) -> Self {
self.field.max = Some(value.into());
self
}
#[must_use]
pub fn step(mut self, value: impl Into<String>) -> Self {
self.field.step = Some(value.into());
self
}
#[must_use]
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.field.pattern = Some(pattern.into());
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.field.class = Some(class.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.field.id = Some(id.into());
self
}
#[must_use]
pub fn help(mut self, text: impl Into<String>) -> Self {
self.field.help_text = Some(text.into());
self
}
#[must_use]
pub fn data(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.field.data_attrs.push((name.into(), value.into()));
self
}
#[must_use]
pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.field.custom_attrs.push((name.into(), value.into()));
self
}
#[must_use]
pub fn htmx_get(mut self, url: impl Into<String>) -> Self {
self.field.htmx.get = Some(url.into());
self
}
#[must_use]
pub fn htmx_post(mut self, url: impl Into<String>) -> Self {
self.field.htmx.post = Some(url.into());
self
}
#[must_use]
pub fn htmx_target(mut self, selector: impl Into<String>) -> Self {
self.field.htmx.target = Some(selector.into());
self
}
#[must_use]
pub fn htmx_swap(mut self, strategy: impl Into<String>) -> Self {
self.field.htmx.swap = Some(strategy.into());
self
}
#[must_use]
pub fn htmx_trigger(mut self, trigger: impl Into<String>) -> Self {
self.field.htmx.trigger = Some(trigger.into());
self
}
#[must_use]
pub fn done(mut self) -> FormBuilder<'a> {
self.form.fields.push(self.field);
self.form
}
}
pub struct TextareaBuilder<'a> {
form: FormBuilder<'a>,
field: FormField,
}
impl<'a> TextareaBuilder<'a> {
const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
Self { form, field }
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.field.label = Some(label.into());
self
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.field.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn value(mut self, value: impl Into<String>) -> Self {
self.field.value = Some(value.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.field.flags.required = true;
self
}
#[must_use]
pub const fn disabled(mut self) -> Self {
self.field.flags.disabled = true;
self
}
#[must_use]
pub const fn rows(mut self, rows: u32) -> Self {
if let FieldKind::Textarea {
rows: ref mut r, ..
} = self.field.kind
{
*r = Some(rows);
}
self
}
#[must_use]
pub const fn cols(mut self, cols: u32) -> Self {
if let FieldKind::Textarea {
cols: ref mut c, ..
} = self.field.kind
{
*c = Some(cols);
}
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.field.class = Some(class.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.field.id = Some(id.into());
self
}
#[must_use]
pub fn help(mut self, text: impl Into<String>) -> Self {
self.field.help_text = Some(text.into());
self
}
#[must_use]
pub fn done(mut self) -> FormBuilder<'a> {
self.form.fields.push(self.field);
self.form
}
}
pub struct SelectBuilder<'a> {
form: FormBuilder<'a>,
field: FormField,
selected_value: Option<String>,
}
impl<'a> SelectBuilder<'a> {
const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
Self {
form,
field,
selected_value: None,
}
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.field.label = Some(label.into());
self
}
#[must_use]
pub fn option(mut self, value: impl Into<String>, label: impl Into<String>) -> Self {
if let FieldKind::Select { ref mut options, .. } = self.field.kind {
options.push(SelectOption::new(value, label));
}
self
}
#[must_use]
pub fn placeholder_option(mut self, label: impl Into<String>) -> Self {
if let FieldKind::Select { ref mut options, .. } = self.field.kind {
options.insert(0, SelectOption::disabled("", label));
}
self
}
#[must_use]
pub fn selected(mut self, value: impl Into<String>) -> Self {
self.selected_value = Some(value.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.field.flags.required = true;
self
}
#[must_use]
pub const fn disabled(mut self) -> Self {
self.field.flags.disabled = true;
self
}
#[must_use]
pub const fn multiple(mut self) -> Self {
if let FieldKind::Select {
ref mut multiple, ..
} = self.field.kind
{
*multiple = true;
}
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.field.class = Some(class.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.field.id = Some(id.into());
self
}
#[must_use]
pub fn done(mut self) -> FormBuilder<'a> {
self.field.value = self.selected_value;
self.form.fields.push(self.field);
self.form
}
}
pub struct CheckboxBuilder<'a> {
form: FormBuilder<'a>,
field: FormField,
}
impl<'a> CheckboxBuilder<'a> {
const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
Self { form, field }
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.field.label = Some(label.into());
self
}
#[must_use]
pub fn value(mut self, value: impl Into<String>) -> Self {
self.field.value = Some(value.into());
self
}
#[must_use]
pub const fn checked(mut self) -> Self {
if let FieldKind::Checkbox {
ref mut checked, ..
} = self.field.kind
{
*checked = true;
}
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.field.flags.required = true;
self
}
#[must_use]
pub const fn disabled(mut self) -> Self {
self.field.flags.disabled = true;
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.field.class = Some(class.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.field.id = Some(id.into());
self
}
#[must_use]
pub fn done(mut self) -> FormBuilder<'a> {
self.form.fields.push(self.field);
self.form
}
}
pub struct FileFieldBuilder<'a> {
form: FormBuilder<'a>,
field: FormField,
}
impl<'a> FileFieldBuilder<'a> {
const fn new(form: FormBuilder<'a>, field: FormField) -> Self {
Self { form, field }
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.field.label = Some(label.into());
self
}
#[must_use]
pub fn accept(mut self, types: impl Into<String>) -> Self {
self.field.file_attrs.accept = Some(types.into());
self
}
#[must_use]
pub const fn multiple(mut self) -> Self {
self.field.file_attrs.multiple = true;
self
}
#[must_use]
pub const fn max_size_mb(mut self, size_mb: u32) -> Self {
self.field.file_attrs.max_size_mb = Some(size_mb);
self
}
#[must_use]
pub const fn show_preview(mut self) -> Self {
self.field.file_attrs.show_preview = true;
self
}
#[must_use]
pub const fn drag_drop(mut self) -> Self {
self.field.file_attrs.drag_drop = true;
self
}
#[must_use]
pub fn progress_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.field.file_attrs.progress_endpoint = Some(endpoint.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.field.flags.required = true;
self
}
#[must_use]
pub const fn disabled(mut self) -> Self {
self.field.flags.disabled = true;
self
}
#[must_use]
pub fn class(mut self, class: impl Into<String>) -> Self {
self.field.class = Some(class.into());
self
}
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.field.id = Some(id.into());
self
}
#[must_use]
pub fn help(mut self, text: impl Into<String>) -> Self {
self.field.help_text = Some(text.into());
self
}
#[must_use]
pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.field.custom_attrs.push((name.into(), value.into()));
self
}
#[must_use]
pub fn done(mut self) -> FormBuilder<'a> {
self.form.fields.push(self.field);
self.form
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_form_builder_basic() {
let form = FormBuilder::new("/test", "POST");
assert_eq!(form.action, "/test");
assert_eq!(form.method, "POST");
}
#[test]
fn test_form_builder_with_id() {
let form = FormBuilder::new("/test", "POST").id("my-form");
assert_eq!(form.id.as_deref(), Some("my-form"));
}
#[test]
fn test_form_builder_csrf() {
let form = FormBuilder::new("/test", "POST").csrf_token("token123");
assert_eq!(form.csrf_token.as_deref(), Some("token123"));
}
#[test]
fn test_field_builder() {
let form = FormBuilder::new("/test", "POST")
.field("email", InputType::Email)
.label("Email")
.required()
.placeholder("test@example.com")
.done();
assert_eq!(form.fields.len(), 1);
let field = &form.fields[0];
assert_eq!(field.name, "email");
assert_eq!(field.label.as_deref(), Some("Email"));
assert!(field.flags.required);
assert_eq!(field.placeholder.as_deref(), Some("test@example.com"));
}
#[test]
fn test_textarea_builder() {
let form = FormBuilder::new("/test", "POST")
.textarea("content")
.label("Content")
.rows(10)
.cols(50)
.done();
assert_eq!(form.fields.len(), 1);
let field = &form.fields[0];
assert!(matches!(
field.kind,
FieldKind::Textarea {
rows: Some(10),
cols: Some(50)
}
));
}
#[test]
fn test_select_builder() {
let form = FormBuilder::new("/test", "POST")
.select("country")
.label("Country")
.option("us", "United States")
.option("ca", "Canada")
.selected("us")
.done();
assert_eq!(form.fields.len(), 1);
let field = &form.fields[0];
assert!(field.is_select());
assert_eq!(field.value.as_deref(), Some("us"));
}
#[test]
fn test_checkbox_builder() {
let form = FormBuilder::new("/test", "POST")
.checkbox("terms")
.label("I agree")
.checked()
.done();
assert_eq!(form.fields.len(), 1);
let field = &form.fields[0];
assert!(matches!(field.kind, FieldKind::Checkbox { checked: true }));
}
#[test]
fn test_hidden_field() {
let form = FormBuilder::new("/test", "POST").hidden("user_id", "123");
assert_eq!(form.fields.len(), 1);
let field = &form.fields[0];
assert!(matches!(field.kind, FieldKind::Input(InputType::Hidden)));
assert_eq!(field.value.as_deref(), Some("123"));
}
#[test]
fn test_htmx_form_attrs() {
let form = FormBuilder::new("/test", "POST")
.htmx_post("/api/test")
.htmx_target("#result")
.htmx_swap("innerHTML")
.htmx_indicator("#spinner");
assert_eq!(form.htmx.post.as_deref(), Some("/api/test"));
assert_eq!(form.htmx.target.as_deref(), Some("#result"));
assert_eq!(form.htmx.swap.as_deref(), Some("innerHTML"));
assert_eq!(form.htmx.indicator.as_deref(), Some("#spinner"));
}
}