Skip to main content

vertigo_forms/
search_panel.rs

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