Skip to main content

yew_autocomplete/
autocomplete.rs

1use std::{future::Future, pin::Pin, rc::Rc};
2
3use yew::prelude::*;
4
5use crate::{
6    autocomplete_state::{AutocompleteConfig, AutocompleteState, HighlightDirection},
7    view::{self, InputCallbacks, RenderHtml},
8};
9
10/// The async result of the [`ItemResolver`]
11pub type ItemResolverResult<T> = Pin<Box<dyn Future<Output = Result<Vec<T>, ()>>>>;
12
13/// An async function that can be passed as a Prop, that takes the current value of the
14/// [`Autocomplete`] input and returns a Vec of Ts
15pub type ItemResolver<T> = Callback<String, ItemResolverResult<T>>;
16
17/// A Yew.rs [Component] with highly configurable auto completion capabilites
18pub struct Autocomplete<T: Clone + PartialEq + RenderHtml + 'static> {
19    state: AutocompleteState<T>,
20}
21
22/// Properties of the [Autocomplete] component
23#[derive(PartialEq, Properties, Clone)]
24pub struct Props<T: PartialEq> {
25    pub resolve_items: ItemResolver<T>,
26    pub onchange: Callback<Vec<T>>,
27    pub children: Children, // TODO: typed children?
28
29    #[prop_or(true)]
30    pub auto: bool,
31    #[prop_or(false)]
32    pub show_selected: bool,
33    #[prop_or(false)]
34    pub multi_select: bool,
35}
36
37/// Internal messages of the [Autocomplete] component
38#[derive(Debug, PartialEq)]
39pub enum Msg {
40    OnInput(String),
41    OnKeydown(u32),
42    SelectItem(usize),
43    Resolve,
44    Noop(bool),
45}
46
47impl<T> Component for Autocomplete<T>
48where
49    T: 'static + PartialEq + Clone + RenderHtml,
50{
51    type Message = Msg;
52
53    type Properties = Props<T>;
54
55    fn create(ctx: &Context<Self>) -> Self {
56        Self {
57            state: AutocompleteState::new(AutocompleteConfig::new(
58                ctx.props().auto,
59                ctx.props().multi_select,
60                ctx.props().onchange.clone(),
61                ctx.link().callback(Msg::Noop),
62                ctx.props().resolve_items.clone(),
63            )),
64        }
65    }
66
67    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
68        match msg {
69            Msg::OnInput(value) => {
70                self.state.oninput(value.as_str());
71                true
72            }
73            Msg::OnKeydown(key) => {
74                match key {
75                    13 => {
76                        self.state.select_current();
77                        true
78                    }
79                    38 => {
80                        self.state.set_highlight_item(&HighlightDirection::Previous);
81                        true
82                    }
83                    40 => {
84                        self.state.set_highlight_item(&HighlightDirection::Next);
85                        true
86                    }
87                    _ => false, // Noop
88                }
89            }
90            Msg::SelectItem(index) => {
91                self.state.select_item(index);
92                true
93            }
94            Msg::Resolve => {
95                self.state.resolve();
96                false
97            }
98            Msg::Noop(reload) => reload,
99        }
100    }
101
102    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
103        if old_props != ctx.props() {
104            self.state.update_config(AutocompleteConfig::new(
105                ctx.props().auto,
106                ctx.props().multi_select,
107                ctx.props().onchange.clone(),
108                ctx.link().callback(Msg::Noop),
109                ctx.props().resolve_items.clone(),
110            ));
111        }
112        true
113    }
114
115    #[allow(clippy::let_underscore_untyped)]
116    fn view(&self, ctx: &Context<Self>) -> Html {
117        let input_callbacks = InputCallbacks {
118            on_input: ctx.link().callback(Msg::OnInput),
119            on_keydown: ctx.link().callback(|e: KeyboardEvent| {
120                let code = e.which();
121
122                match code {
123                    // This is not tested in cypres because `type`'s behaviour when hitting up and
124                    // down arrow was different, it didn't move the cursor. While in the browser it
125                    // jumped from beginning of the test to the end While in the browser it jumped
126                    // from beginning of the test to the end
127                    13 | 38 | 40 => e.prevent_default(),
128                    _ => (),
129                };
130
131                Msg::OnKeydown(code)
132            }),
133            resolve: ctx.link().callback(|_| Msg::Resolve),
134            select_item: ctx.link().callback(Msg::SelectItem),
135        };
136        let selected_items = if ctx.props().show_selected {
137            Rc::new(self.state.selected_items())
138        } else {
139            Rc::new(Vec::new())
140        };
141
142        let view_context = view::Context {
143            value: self.state.input(),
144            callbacks: input_callbacks,
145            items: Rc::new(self.state.items()),
146            highlighted: self.state.highlighted_item(),
147            selected_items,
148            auto: ctx.props().auto,
149        };
150
151        html! {
152            <ContextProvider<view::Context<T>> context={view_context}>
153                {for ctx.props().children.iter() }
154            </ContextProvider<view::Context<T>>>
155        }
156    }
157}