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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#[macro_use]
mod events;

use wasm_bindgen::JsCast;
use web_sys::{Event, EventTarget};

use crate::Callback;
pub use events::*;

/// Cast [Event] `e` into it's target `T`.
///
/// This function mainly exists to provide type inference in the [impl_action] macro to the compiler
/// and avoid some verbosity by not having to type the signature over and over in closure
/// definitions.
#[inline]
pub(crate) fn cast_event<T>(e: Event) -> T
where
    T: JsCast,
{
    e.unchecked_into()
}

/// A trait to obtain a generic event target.
///
/// The methods in this trait are convenient helpers that use the [`JsCast`] trait internally
/// to do the conversion.
pub trait TargetCast
where
    Self: AsRef<Event>,
{
    /// Performs a dynamic cast (checked at runtime) of this events target into the type `T`.
    ///
    /// This method can return [`None`] for two reasons:
    /// - The event's target was [`None`]
    /// - The event's target type did not match `T`
    ///
    /// # Example
    ///
    /// ```
    /// use yew::prelude::*;
    /// use web_sys::HtmlTextAreaElement;
    /// # enum Msg {
    /// #   Value(String),
    /// # }
    /// # struct Comp;
    /// # impl Component for Comp {
    /// # type Message = Msg;
    /// # type Properties = ();
    /// # fn create(ctx: &Context<Self>) -> Self {
    /// #   Self
    /// # }
    ///
    /// fn view(&self, ctx: &Context<Self>) -> Html {
    ///     html! {
    ///         <div
    ///             onchange={ctx.link().batch_callback(|e: Event| {
    ///                 if let Some(input) = e.target_dyn_into::<HtmlTextAreaElement>() {
    ///                     Some(Msg::Value(input.value()))
    ///                 } else {
    ///                     None
    ///                 }
    ///             })}
    ///         >
    ///             <textarea />
    ///             <input type="text" />
    ///         </div>
    ///     }
    /// }
    /// # }
    /// ```
    /// _Note: if you can apply the [`Callback`] directly onto an element which doesn't have a child
    /// consider using [`TargetCast::target_unchecked_into<T>`]_
    #[inline]
    fn target_dyn_into<T>(&self) -> Option<T>
    where
        T: AsRef<EventTarget> + JsCast,
    {
        self.as_ref()
            .target()
            .and_then(|target| target.dyn_into().ok())
    }

    #[inline]
    /// Performs a zero-cost unchecked cast of this events target into the type `T`.
    ///
    /// This method **does not check whether the event target is an instance of `T`**. If used
    /// incorrectly then this method may cause runtime exceptions in both Rust and JS, this should
    /// be used with caution.
    ///
    /// A common safe usage of this method is within a [`Callback`] that is applied directly to an
    /// element that has no children, thus `T` will be the type of the element the [`Callback`] is
    /// applied to.
    ///
    /// # Example
    ///
    /// ```
    /// use yew::prelude::*;
    /// use web_sys::HtmlInputElement;
    /// # enum Msg {
    /// #   Value(String),
    /// # }
    /// # struct Comp;
    /// # impl Component for Comp {
    /// # type Message = Msg;
    /// # type Properties = ();
    /// # fn create(ctx: &Context<Self>) -> Self {
    /// #   Self
    /// # }
    ///
    /// fn view(&self, ctx: &Context<Self>) -> Html {
    ///     html! {
    ///         <input type="text"
    ///             onchange={ctx.link().callback(|e: Event| {
    ///                 // Safe to use as callback is on an `input` element so this event can
    ///                 // only come from this input!
    ///                 let input: HtmlInputElement = e.target_unchecked_into();
    ///                 Msg::Value(input.value())
    ///             })}
    ///         />
    ///     }
    /// }
    /// # }
    /// ```
    fn target_unchecked_into<T>(&self) -> T
    where
        T: AsRef<EventTarget> + JsCast,
    {
        self.as_ref().target().unwrap().unchecked_into()
    }
}

impl<E: AsRef<Event>> TargetCast for E {}

/// A trait similar to `Into<T>` which allows conversion of a value into a [`Callback`].
/// This is used for event listeners.
pub trait IntoEventCallback<EVENT> {
    /// Convert `self` to `Option<Callback<EVENT>>`
    fn into_event_callback(self) -> Option<Callback<EVENT>>;
}

impl<EVENT> IntoEventCallback<EVENT> for Callback<EVENT> {
    fn into_event_callback(self) -> Option<Callback<EVENT>> {
        Some(self)
    }
}

impl<EVENT> IntoEventCallback<EVENT> for &Callback<EVENT> {
    fn into_event_callback(self) -> Option<Callback<EVENT>> {
        Some(self.clone())
    }
}

impl<EVENT> IntoEventCallback<EVENT> for Option<Callback<EVENT>> {
    fn into_event_callback(self) -> Option<Callback<EVENT>> {
        self
    }
}

impl<T, EVENT> IntoEventCallback<EVENT> for T
where
    T: Fn(EVENT) + 'static,
{
    fn into_event_callback(self) -> Option<Callback<EVENT>> {
        Some(Callback::from(self))
    }
}

impl<T, EVENT> IntoEventCallback<EVENT> for Option<T>
where
    T: Fn(EVENT) + 'static,
{
    fn into_event_callback(self) -> Option<Callback<EVENT>> {
        Some(Callback::from(self?))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn supported_into_event_callback_types() {
        let f = |_: usize| ();
        let cb = Callback::from(f);

        // Callbacks
        let _: Option<Callback<usize>> = cb.clone().into_event_callback();
        let _: Option<Callback<usize>> = (&cb).into_event_callback();
        let _: Option<Callback<usize>> = Some(cb).into_event_callback();

        // Fns
        let _: Option<Callback<usize>> = f.into_event_callback();
        let _: Option<Callback<usize>> = Some(f).into_event_callback();
    }
}