yew-bulma 0.0.1

Yew components compatible with the Bulma CSS framework
Documentation
use super::storage::FormStorage;
use std::{collections::HashMap, rc::Rc, str::FromStr};
use web_sys::HtmlInputElement;
use yew::prelude::*;

pub struct TextInput<T, V>
where
    T: Copy + std::hash::Hash + Eq + PartialEq + std::fmt::Debug + 'static,
    V: Clone + FromStr + ToString + std::fmt::Debug + PartialEq + 'static,
{
    props: Props<T, V>,
    text_value: String,
    input: NodeRef,
    link: ComponentLink<Self>,
}

#[derive(Clone, Properties)]
pub struct Props<T, V>
where
    T: Copy + std::hash::Hash + Eq + PartialEq + std::fmt::Debug + 'static,
    V: Clone + FromStr + ToString + std::fmt::Debug + PartialEq + 'static,
{
    #[prop_or_default]
    pub on_value_changed: Callback<Option<V>>,
    pub storage: FormStorage<Option<V>>,
    pub field: T,
    pub errors: Option<Rc<HashMap<T, Vec<Rc<Html>>>>>,
    #[prop_or_default]
    pub placeholder: String,
    #[prop_or_default]
    pub disabled: bool,
    #[prop_or_default]
    pub readonly: bool,
}

pub enum Message {
    KeyPressed,
}

impl<T, V> Component for TextInput<T, V>
where
    T: Copy + std::hash::Hash + Eq + PartialEq + std::fmt::Debug + 'static,
    V: Clone + FromStr + ToString + std::fmt::Debug + PartialEq + 'static,
{
    type Message = Message;
    type Properties = Props<T, V>;

    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
        let text_value = props
            .storage
            .value()
            .unwrap_or(None)
            .map(|v| v.to_string())
            .unwrap_or_default();
        TextInput {
            props,
            link,
            input: NodeRef::default(),
            text_value,
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Message::KeyPressed => {
                if let Some(input) = self.input.cast::<HtmlInputElement>() {
                    self.text_value = input.value();
                    if self.text_value.is_empty() {
                        self.props.storage.update_with_invalid_hint(None, false);
                        self.props.storage.update_invalid_hint(false);
                        self.props.on_value_changed.emit(None);
                    } else if let Ok(value) = V::from_str(&self.text_value) {
                        self.props
                            .storage
                            .update_with_invalid_hint(Some(value.clone()), false);
                        self.props.storage.update_invalid_hint(false);
                        self.props.on_value_changed.emit(Some(value));
                    } else {
                        self.props.storage.update_invalid_hint(true);
                        self.props
                            .on_value_changed
                            .emit(self.props.storage.value().unwrap_or_default());
                    }
                }
            }
        }
        false
    }

    fn view(&self) -> Html {
        let errors = self
            .props
            .errors
            .as_ref()
            .map(|errors| errors.get(&self.props.field).cloned());
        let css_class = match &errors {
            Some(errors) => match errors {
                Some(_) => "input is-danger",
                None => "input",
            },
            None => "input",
        };
        html! {
            <div class="control">
                <input class=css_class ref=self.input.clone() type="text" value=self.text_value placeholder=&self.props.placeholder onchange=self.link.callback(|_| Message::KeyPressed) oninput=self.link.callback(|_| Message::KeyPressed) disabled=self.props.disabled readonly=self.props.readonly />
            </div>
        }
    }
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        self.text_value = props
            .storage
            .unchecked_value()
            .map(|v| v.to_string())
            .unwrap_or_default();
        self.props = props;
        true
    }
}