1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use std::{future::Future, pin::Pin, rc::Rc};

use yew::{html::Scope, prelude::*};
use yew_commons::fn_prop::FnProp;

use crate::{
    autocomplete_state::{AutocompleteState, HighlightDirection},
    view::{self, InputCallbacks, RenderHtml},
};

/// The async result of the [`ItemResolver`]
pub type ItemResolverResult<T> = Pin<Box<dyn Future<Output = Result<Vec<T>, ()>>>>;

/// An async function that can be passed as a Prop, that takes the current value of the
/// [`Autocomplete`] input and returns a Vec of Ts
pub type ItemResolver<T> = FnProp<String, ItemResolverResult<T>>;

/// A Yew.rs [Component] with highly configurable auto completion capabilites
pub struct Autocomplete<T: Clone + PartialEq + RenderHtml + 'static> {
    state: AutocompleteState<T, AsyncMessageCallback<Self>>,
}

/// Properties of the [Autocomplete] component
#[derive(PartialEq, Properties, Clone)]
pub struct Props<T: PartialEq> {
    pub resolve_items: ItemResolver<T>,
    pub onchange: Callback<Vec<T>>,
    pub children: Children, // TODO: typed children?

    #[prop_or(true)]
    pub auto: bool,
    #[prop_or(false)]
    pub show_selected: bool,
    #[prop_or(false)]
    pub multi_select: bool,
}

/// Internal messages of the [Autocomplete] component
#[derive(Debug, PartialEq)]
pub enum Msg<T> {
    OnInput(String),
    OnKeydown(u32),
    SetItems(Vec<T>),
    SelectItem(usize),
    Noop,
}

/// An synchronously executed async callback
pub(crate) trait AsyncCallback<T> {
    /// Synchronously dispatches  the given future
    fn dispatch(&self, future: Pin<Box<dyn Future<Output = T>>>);
}

/// An async callback that is capable of sending an async message (a [`Component::Message`] wrapped in a Future) to
/// a component
struct AsyncMessageCallback<C: Component> {
    link: Scope<C>,
}

impl<C: Component> AsyncMessageCallback<C> {
    /// Creates a new [`AsyncMessageCallback`] from the [Scope] of a [Component]
    fn new(link: Scope<C>) -> Self {
        Self { link }
    }
}

impl<C: Component> AsyncCallback<C::Message> for AsyncMessageCallback<C> {
    fn dispatch(&self, future: Pin<Box<dyn Future<Output = C::Message>>>) {
        self.link.send_future(future);
    }
}

impl<T> Component for Autocomplete<T>
where
    T: 'static + PartialEq + Clone + RenderHtml,
{
    type Message = Msg<T>;

    type Properties = Props<T>;

    fn create(ctx: &Context<Self>) -> Self {
        Self {
            state: AutocompleteState::new(
                ctx.props().multi_select,
                ctx.props().onchange.clone(),
                AsyncMessageCallback::new(ctx.link().clone()),
                ctx.props().resolve_items.clone(),
            ),
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::OnInput(value) => {
                self.state.oninput(value.as_str());
                true
            }
            Msg::OnKeydown(key) => {
                match key {
                    13 => self.state.select_current(),
                    38 => self.state.set_highlight_item(&HighlightDirection::Previous),
                    40 => self.state.set_highlight_item(&HighlightDirection::Next),
                    _ => (), // Noop
                };
                true
            }
            Msg::SetItems(items) => {
                self.state.set_items(items);
                true
            }
            Msg::SelectItem(index) => {
                self.state.select_item(index);
                true
            }
            Msg::Noop => false,
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let input_callbacks = InputCallbacks {
            on_input: ctx.link().callback(Msg::OnInput),
            on_keydown: ctx.link().callback(|e: KeyboardEvent| {
                let code = e.which();

                match code {
                    // This is not tested in cypres because `type`'s behaviour when hitting up and
                    // down arrow was different, it didn't move the cursor. While in the browser it
                    // jumped from beginning of the test to the end While in the browser it jumped
                    // from beginning of the test to the end
                    13 | 38 | 40 => e.prevent_default(),
                    _ => (),
                };

                Msg::OnKeydown(code)
            }),
            select_item: ctx.link().callback(Msg::SelectItem),
        };
        let selected_items = if ctx.props().show_selected {
            Rc::new(self.state.selected_items())
        } else {
            Rc::new(Vec::new())
        };

        let view_context = view::Context {
            value: self.state.input(),
            callbacks: input_callbacks,
            items: Rc::new(self.state.items()),
            highlighted: self.state.highlighted_item(),
            selected_items,
            auto: ctx.props().auto,
        };

        html! {
            <ContextProvider<view::Context<T>> context={view_context}>
                {for ctx.props().children.iter() }
            </ContextProvider<view::Context<T>>>
        }
    }
}