vertigo_forms/
search_panel.rs

1use std::rc::Rc;
2
3use vertigo::{AutoMap, DomNode, Resource, ToComputed, Value, bind, dom};
4
5pub trait SearchResult {
6    fn is_empty(&self) -> bool;
7}
8
9impl<T> SearchResult for Vec<T> {
10    fn is_empty(&self) -> bool {
11        self.is_empty()
12    }
13}
14
15/// Component that takes query and loads/computes a result.
16pub struct SearchPanel<T, K>
17where
18    T: Clone,
19    K: ToComputed<Resource<Rc<T>>>,
20{
21    pub query: Value<String>,
22    pub cache: AutoMap<String, K>,
23    pub render_results: Rc<dyn Fn(Rc<T>) -> DomNode>,
24    pub params: SearchPanelParams,
25}
26
27#[derive(Clone)]
28pub struct SearchPanelParams {
29    pub min_chars: usize,
30    pub prompt: String,
31    pub hint: String,
32    pub loading_text: String,
33    pub empty_text: String,
34}
35
36impl Default for SearchPanelParams {
37    fn default() -> Self {
38        Self {
39            min_chars: 3,
40            prompt: "Search: ".to_string(),
41            hint: "Enter at least {min_chars} letters".to_string(),
42            loading_text: "Loading...".to_string(),
43            empty_text: "No results".to_string(),
44        }
45    }
46}
47
48impl<T, K> SearchPanel<T, K>
49where
50    T: SearchResult + PartialEq + Clone + 'static,
51    K: ToComputed<Resource<Rc<T>>> + Clone + 'static,
52{
53    pub fn into_component(self) -> Self {
54        self
55    }
56
57    pub fn mount(self) -> DomNode {
58        let Self {
59            query,
60            cache,
61            render_results,
62            params,
63        } = self;
64        let prompt = params.prompt.clone();
65        let content = query.render_value(move |query| {
66            let SearchPanelParams {
67                min_chars,
68                prompt: _,
69                hint,
70                loading_text,
71                empty_text,
72            } = params.clone();
73            if query.len() < min_chars {
74                let msg = hint.replace("{min_chars}", &min_chars.to_string());
75                return dom! { <div>{msg}</div> };
76            }
77            let render_results = render_results.clone();
78            let content = cache
79                .get(&query)
80                .to_computed()
81                .render_value(move |books| match books {
82                    Resource::Loading => dom! { <div>{loading_text.clone()}</div> },
83                    Resource::Ready(dataset) => {
84                        if !dataset.is_empty() {
85                            render_results(dataset)
86                        } else {
87                            dom! {
88                                <div>{empty_text.clone()}</div>
89                            }
90                        }
91                    }
92                    Resource::Error(err) => {
93                        dom! { <div>{err}</div> }
94                    }
95                });
96            dom! { <div>{content}</div> }
97        });
98
99        let on_input = bind!(query, |new_value: String| {
100            query.set(new_value);
101        });
102
103        let value = query.to_computed();
104
105        dom! {
106            <div>
107                { prompt }
108                <input {value} on_input={on_input}/>
109                { content }
110            </div>
111        }
112    }
113}