seed-bootstrap 0.2.0

Components for using Bootstrap toolkit with Seed framework
use seed::{prelude::*, *};
use std::borrow::Cow;
use std::fmt;
use std::rc::Rc;

pub struct FormGroup<'a, Ms: 'static> {
    id: Cow<'a, str>,
    label: Option<Cow<'a, str>>,
    value: Option<Cow<'a, str>>,
    input_event: Option<Rc<dyn Fn(String) -> Ms>>,
    input_type: InputType,
    is_invalid: bool,
    invalid_feedback: Option<Cow<'a, str>>,
    is_warning: bool,
    warning_feedback: Option<Cow<'a, str>>,
    help_text: Vec<Node<Ms>>,
    group_attrs: Attrs,
    input_attrs: Attrs,
}

impl<'a, Ms> FormGroup<'a, Ms> {
    pub fn new(id: impl Into<Cow<'a, str>>) -> Self {
        Self {
            id: id.into(),
            label: None,
            value: None,
            input_event: None,
            input_type: InputType::Text,
            is_invalid: false,
            invalid_feedback: None,
            is_warning: false,
            warning_feedback: None,
            help_text: Vec::new(),
            group_attrs: Attrs::empty(),
            input_attrs: Attrs::empty(),
        }
    }

    pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
        self.label = Some(label.into());
        self
    }

    pub fn value(mut self, value: impl Into<Cow<'a, str>>) -> Self {
        self.value = Some(value.into());
        self
    }

    pub fn on_input(mut self, input_event: impl Fn(String) -> Ms + Clone + 'static) -> Self {
        self.input_event = Some(Rc::new(input_event));
        self
    }

    pub fn text(mut self) -> Self {
        self.input_type = InputType::Text;
        self
    }
    pub fn number(mut self) -> Self {
        self.input_type = InputType::Number;
        self
    }
    pub fn password(mut self) -> Self {
        self.input_type = InputType::Password;
        self
    }
    pub fn textarea(mut self) -> Self {
        self.input_type = InputType::Textarea;
        self
    }
    pub fn checkbox(mut self) -> Self {
        self.input_type = InputType::Checkbox;
        self
    }

    pub fn select(mut self, options: Vec<(String, String)>, include_none_option: bool) -> Self {
        self.input_type = InputType::Select {
            options,
            include_none_option,
        };
        self
    }

    pub fn invalid(mut self, is_invalid: bool) -> Self {
        self.is_invalid = is_invalid;
        self
    }

    pub fn invalid_feedback(mut self, invalid_feedback: Option<impl Into<Cow<'a, str>>>) -> Self {
        self.invalid_feedback = invalid_feedback.map(|s| s.into());
        self
    }

    pub fn warning(mut self, is_warning: bool) -> Self {
        self.is_warning = is_warning;
        self
    }

    pub fn warning_feedback(mut self, warning_feedback: Option<impl Into<Cow<'a, str>>>) -> Self {
        self.warning_feedback = warning_feedback.map(|s| s.into());
        self
    }

    pub fn help_text(mut self, help_text: impl Into<Cow<'static, str>>) -> Self {
        self.help_text = Node::new_text(help_text).into_nodes();
        self
    }
    pub fn help_nodes(mut self, help_nodes: impl IntoNodes<Ms>) -> Self {
        self.help_text = help_nodes.into_nodes();
        self
    }

    pub fn group_attrs(mut self, attrs: Attrs) -> Self {
        self.group_attrs.merge(attrs);
        self
    }

    pub fn input_attrs(mut self, attrs: Attrs) -> Self {
        self.input_attrs.merge(attrs);
        self
    }

    pub fn view(self) -> Node<Ms> {
        if self.input_type == InputType::Checkbox {
            self.view_checkbox()
        } else {
            self.view_textfield()
        }
    }

    fn view_checkbox(self) -> Node<Ms> {
        let is_checked = self
            .value
            .as_ref()
            .map(|value| value == "true")
            .unwrap_or(false);
        let click_event_text = if is_checked {
            "false".to_string()
        } else {
            "true".to_string()
        };
        div![
            C!["form-group form-check"],
            &self.group_attrs,
            input![
                C!["form-check-input", IF!(self.is_invalid => "is-invalid")],
                &self.input_attrs,
                id![&self.id],
                attrs![
                    At::Type => "checkbox",
                    At::Value => "true",
                    At::Checked => is_checked.as_at_value()
                ],
                self.input_event.clone().map(|input_event| {
                    input_ev(Ev::Input, move |_event| input_event(click_event_text))
                })
            ],
            self.label.as_ref().map(|label| {
                label![
                    C!["form-check-label"],
                    attrs![
                        At::For => self.id
                    ],
                    label
                ]
            }),
            if !self.help_text.is_empty() {
                small![C!["form-text text-muted"], &self.help_text]
            } else {
                empty![]
            },
            self.invalid_feedback
                .as_ref()
                .filter(|_| self.is_invalid)
                .map(|err| div![C!["invalid-feedback"], err]),
            self.warning_feedback
                .as_ref()
                .filter(|_| self.is_warning)
                .map(|err| small![C!["form-text text-warning"], err])
        ]
    }

    fn view_textfield(self) -> Node<Ms> {
        div![
            C!["form-group"],
            &self.group_attrs,
            self.label.as_ref().map(|label| {
                label![
                    attrs![
                        At::For => self.id
                    ],
                    label
                ]
            }),
            match &self.input_type {
                InputType::Text | InputType::Number | InputType::Password => input![
                    C!["form-control", IF!(self.is_invalid => "is-invalid")],
                    &self.input_attrs,
                    id![&self.id],
                    attrs![
                        At::Type => &self.input_type,
                    ],
                    self.value.as_ref().map(|value| attrs![At::Value => value]),
                    self.input_event.clone().map(|input_event| {
                        input_ev(Ev::Input, move |event| input_event(event))
                    })
                ],
                InputType::Textarea => textarea![
                    C!["form-control", IF!(self.is_invalid => "is-invalid")],
                    &self.input_attrs,
                    id![&self.id],
                    self.value.as_ref().map(
                        |value| attrs![At::Value => value, At::Rows => value.split('\n').count(), At::Wrap => "off"]
                    ),
                    self.input_event.clone().map(|input_event| {
                        input_ev(Ev::Input, move |event| input_event(event))
                    })
                ],
                InputType::Select { options, include_none_option } => select![
                    C!["custom-select", IF!(self.is_invalid => "is-invalid")],
                    if *include_none_option { option![
                        attrs! {
                            At::Value => "",
                            At::Selected => self.value.is_none().as_at_value()
                        }
                    ] } else { empty![] },
                    options.iter().map(|(key, display)| {
                        option![
                            attrs! {
                                At::Value => &key,
                                At::Selected => self.value.as_ref().map(|value| value == key).unwrap_or(false).as_at_value()
                            },
                            &display
                        ]
                    }),
                    self.input_event.clone().map(|input_event| {
                        input_ev(Ev::Input, move |event| input_event(event))
                    })
                ],
                InputType::Checkbox => empty![],
            },
            if !self.help_text.is_empty() { small![C!["form-text text-muted"], &self.help_text] } else { empty![] },
            self.invalid_feedback
                .as_ref()
                .filter(|_| self.is_invalid)
                .map(|err| div![C!["invalid-feedback"], err]),
            self.warning_feedback
                .as_ref()
                .filter(|_| self.is_warning)
                .map(|err| small![C!["form-text text-warning"], err])
        ]
    }
}

impl<Ms> UpdateEl<Ms> for FormGroup<'_, Ms> {
    fn update_el(self, el: &mut El<Ms>) {
        self.view().update_el(el)
    }
}

#[derive(PartialEq)]
enum InputType {
    Text,
    Number,
    Password,
    Textarea,
    Checkbox,
    Select {
        options: Vec<(String, String)>,
        include_none_option: bool,
    },
}

impl fmt::Display for InputType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
        match self {
            Self::Text => write!(f, "text"),
            Self::Number => write!(f, "number"),
            Self::Password => write!(f, "password"),
            Self::Textarea => write!(f, "textarea"),
            Self::Checkbox => write!(f, "checkbox"),
            Self::Select { .. } => write!(f, "select"),
        }
    }
}