Skip to main content

yew_tincture/components/
typed_input.rs

1use web_sys::HtmlInputElement;
2use yew::{
3    events::KeyboardEvent, html, Callback, Component, Context, Html, InputEvent, Properties,
4    TargetCast,
5};
6
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9use std::str::FromStr;
10
11// TODO: reconsider and rework this as a result of the Components API change in
12// yew 0.19. In particular: do we need to keep a clone of the `storage` field?
13
14#[derive(Debug)]
15pub struct TypedInputParseError {}
16
17impl std::error::Error for TypedInputParseError {}
18
19impl std::fmt::Display for TypedInputParseError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
21        write!(f, "parse error for typed input")
22    }
23}
24
25#[derive(Clone)]
26pub struct RawAndParsed<T>
27where
28    T: FromStr + Clone,
29    <T as FromStr>::Err: Clone,
30{
31    raw_value: String,
32    parsed: Result<T, <T as FromStr>::Err>,
33}
34
35impl<T> RawAndParsed<T>
36where
37    T: FromStr + Clone,
38    <T as FromStr>::Err: Clone,
39{
40    /// Create a RawAndParsed with empty value.
41    fn empty() -> Self
42    where
43        T: std::fmt::Display,
44    {
45        let raw_value = "".to_string();
46        let parsed = raw_value.parse();
47        Self { raw_value, parsed }
48    }
49
50    /// Create a RawAndParsed with a good value.
51    fn from_initial(value: T) -> Self
52    where
53        T: std::fmt::Display,
54    {
55        let raw_value = format!("{}", value);
56        let parsed = Ok(value);
57        Self { raw_value, parsed }
58    }
59}
60
61fn new_focus_state() -> Rc<Cell<FocusState>> {
62    Rc::new(Cell::new(FocusState::IsBlurred))
63}
64
65#[derive(PartialEq, Clone)]
66pub struct TypedInputStorage<T>
67where
68    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
69    <T as FromStr>::Err: Clone,
70{
71    rc: Rc<RefCell<TypedInputStorageInner<T>>>,
72}
73
74impl<T> Default for TypedInputStorage<T>
75where
76    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
77    <T as FromStr>::Err: Clone,
78{
79    fn default() -> Self {
80        TypedInputStorage::empty()
81    }
82}
83
84impl<T> TypedInputStorage<T>
85where
86    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
87    <T as FromStr>::Err: Clone,
88    Result<T, <T as FromStr>::Err>: Clone,
89{
90    /// Create a TypedInputStorage with an empty value.
91    pub fn empty() -> Self
92    where
93        T: std::fmt::Display,
94    {
95        Self::create(RawAndParsed::empty())
96    }
97
98    /// Create a TypedInputStorage with an empty value.
99    pub fn from_initial(value: T) -> Self
100    where
101        T: std::fmt::Display,
102    {
103        Self::create(RawAndParsed::from_initial(value))
104    }
105
106    /// Create a TypedInputStorage with an empty value.
107    fn create(raw_and_parsed: RawAndParsed<T>) -> Self
108    where
109        T: std::fmt::Display,
110    {
111        let inner = TypedInputStorageInner {
112            raw_and_parsed,
113            focus_state: new_focus_state(),
114            link: None,
115        };
116        let me = Rc::new(RefCell::new(inner));
117        Self { rc: me }
118    }
119
120    pub fn parsed(&self) -> Result<T, <T as FromStr>::Err> {
121        self.rc.borrow().raw_and_parsed.parsed.clone()
122    }
123
124    fn set_link(&mut self, link: yew::html::Scope<TypedInput<T>>) {
125        let mut inner = self.rc.borrow_mut();
126        inner.link = Some(link);
127    }
128
129    /// Modify the value.
130    ///
131    /// See also the [Self::set_if_not_focused] method.
132    pub fn modify<F>(&mut self, f: F) -> Result<(), TypedInputParseError>
133    where
134        F: Fn(&mut T),
135        T: std::fmt::Display,
136    {
137        let mut inner = self.rc.borrow_mut();
138        let raw_value = match &mut inner.raw_and_parsed.parsed {
139            Ok(value) => {
140                f(value);
141                format!("{}", value)
142            }
143            Err(_e) => {
144                return Err(TypedInputParseError {});
145            }
146        };
147        if let Some(link) = &inner.link {
148            link.send_message(Msg::NewValue(raw_value));
149        }
150        Ok(())
151    }
152
153    /// Get the value.
154    pub fn get(&self) -> Result<T, <T as FromStr>::Err> {
155        self.rc.borrow_mut().raw_and_parsed.parsed.clone()
156    }
157
158    /// Update the value if the user is not editing it.
159    ///
160    /// See also the [Self::modify] method.
161    pub fn set_if_not_focused(&mut self, value: T)
162    where
163        T: std::fmt::Display,
164    {
165        use std::ops::Deref;
166        {
167            let mut inner = self.rc.borrow_mut();
168
169            match (*(inner.focus_state).deref()).get() {
170                FocusState::IsFocused => {}
171                FocusState::IsBlurred => {
172                    inner.raw_and_parsed.raw_value = format!("{}", value);
173                    inner.raw_and_parsed.parsed = Ok(value);
174                }
175            }
176        }
177    }
178}
179
180struct TypedInputStorageInner<T>
181where
182    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
183    <T as FromStr>::Err: Clone,
184{
185    raw_and_parsed: RawAndParsed<T>,
186    // TODO: does this need to be Rc<Cell<_>> or can I make it &'a _?
187    focus_state: Rc<Cell<FocusState>>,
188    link: Option<yew::html::Scope<TypedInput<T>>>,
189}
190
191impl<T> PartialEq for TypedInputStorageInner<T>
192where
193    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
194    <T as FromStr>::Err: Clone,
195{
196    fn eq(&self, rhs: &Self) -> bool {
197        // I am not sure when yew uses this. Here is my
198        // best effort implementation.
199        Rc::ptr_eq(&self.focus_state, &rhs.focus_state)
200            && self.raw_and_parsed.raw_value == rhs.raw_and_parsed.raw_value
201        // TODO: compare link?
202    }
203}
204
205#[derive(PartialEq, Clone, Copy, Debug, Default)]
206enum FocusState {
207    IsFocused,
208    #[default]
209    IsBlurred,
210}
211
212pub struct TypedInput<T>
213where
214    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
215    <T as FromStr>::Err: Clone,
216{
217    storage: TypedInputStorage<T>,
218}
219
220pub enum Msg {
221    NewValue(String),
222    OnFocus,
223    OnBlur,
224    SendValueIfValid,
225    Ignore,
226}
227
228#[derive(PartialEq, Properties)]
229pub struct Props<T>
230where
231    T: FromStr + Clone + PartialEq + std::fmt::Display + 'static,
232    <T as FromStr>::Err: Clone,
233{
234    /// The backing store for the data.
235    pub storage: TypedInputStorage<T>,
236    /// The placeholder text displayed when the input field is blank.
237    #[prop_or_default]
238    pub placeholder: String,
239    /// Called when the user wants to send a valid value
240    #[prop_or_default]
241    pub on_send_valid: Option<Callback<T>>,
242    /// Called whenever the user changes the value
243    #[prop_or_default]
244    pub on_input: Option<Callback<RawAndParsed<T>>>,
245}
246
247impl<T> TypedInput<T>
248where
249    T: std::fmt::Display + FromStr + PartialEq + Clone,
250    <T as FromStr>::Err: Clone,
251{
252    fn send_value_if_valid(&mut self, on_send_valid: Option<&Callback<T>>) {
253        if let Some(callback) = on_send_valid {
254            if let Ok(value) = &self.storage.rc.borrow().raw_and_parsed.parsed {
255                callback.emit(value.clone());
256            }
257        }
258    }
259}
260
261impl<T> Component for TypedInput<T>
262where
263    T: 'static + Clone + PartialEq + FromStr + std::fmt::Display,
264    <T as FromStr>::Err: Clone,
265{
266    type Message = Msg;
267    type Properties = Props<T>;
268
269    fn create(ctx: &Context<Self>) -> Self {
270        let mut storage = ctx.props().storage.clone();
271        let link: yew::html::Scope<TypedInput<T>> = ctx.link().clone();
272        storage.set_link(link);
273        Self { storage }
274    }
275
276    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
277        match msg {
278            Msg::NewValue(raw_value) => {
279                let parsed = raw_value.parse();
280                let stor2 = {
281                    let mut stor = self.storage.rc.borrow_mut();
282                    stor.raw_and_parsed.raw_value = raw_value;
283                    stor.raw_and_parsed.parsed = parsed;
284                    stor.raw_and_parsed.clone()
285                };
286
287                if let Some(ref callback) = &ctx.props().on_input {
288                    callback.emit(stor2);
289                }
290            }
291            Msg::OnFocus => {
292                let stor = self.storage.rc.borrow_mut();
293                stor.focus_state.replace(FocusState::IsFocused);
294            }
295            Msg::OnBlur => {
296                {
297                    let stor = self.storage.rc.borrow_mut();
298                    stor.focus_state.replace(FocusState::IsBlurred);
299                }
300                self.send_value_if_valid(ctx.props().on_send_valid.as_ref());
301            }
302            Msg::SendValueIfValid => {
303                self.send_value_if_valid(ctx.props().on_send_valid.as_ref());
304                return false;
305            }
306            Msg::Ignore => {
307                return false; // no need to rerender DOM
308            }
309        }
310        true
311    }
312
313    fn view(&self, ctx: &Context<Self>) -> Html {
314        let value = self.storage.rc.borrow().raw_and_parsed.raw_value.clone();
315        let input_class = match &self.storage.rc.borrow().raw_and_parsed.parsed {
316            Ok(_) => "ranged-value-input",
317            Err(_) => "ranged-value-input-error",
318        };
319
320        html! {
321            <input type="text"
322                class={input_class}
323                placeholder={ctx.props().placeholder.clone()}
324                value={value}
325                oninput={ctx.link().callback(|e: InputEvent| {
326                    let input: HtmlInputElement = e.target_unchecked_into();
327                    Msg::NewValue(input.value())
328                })}
329                onfocus={ctx.link().callback(|_| Msg::OnFocus)}
330                onblur={ctx.link().callback(|_| Msg::OnBlur)}
331                onkeypress={ctx.link().callback(|e: KeyboardEvent| {
332                    if e.key() == "Enter" { Msg::SendValueIfValid } else { Msg::Ignore }
333                })}
334                />
335        }
336    }
337}