vertigo_forms/
search_panel.rs

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