Skip to main content
Version: Next

事件

介绍

Yew 与 web-sys crate 集成,并使用该 crate 中的事件。下面的表格列出了在 html! 宏中接受的所有 web-sys 事件。

您仍然可以为下表中未列出的事件添加 Callback,请参见手动事件监听器

事件类型

提示

所有的事件类型都在 yew::events 下重新导出。 使用 yew::events 中的类型比手动将 web-sys 作为依赖项包含在您的 crate 中更容易确保版本兼容性, 因为您不会使用与 Yew 指定的版本冲突的版本。

事件监听器的名称是在 html 宏中添加事件 Callback 时预期的名称:

use yew::prelude::*;

html! {
<button onclick={Callback::from(|_| ())}>
// ^^^^^^^ event listener name
{ "Click me!" }
</button>
};

事件名称是监听器名称去掉 "on" 前缀,因此 onclick 事件监听器监听 click 事件。查看本页末尾的完整事件列表及其类型。

事件捕获

Yew 调度的事件遵循虚拟 DOM 层次结构,向上冒泡到监听器。目前,仅支持监听器的冒泡阶段。请注意,虚拟 DOM 层次结构通常(但并非总是)与实际 DOM 层次结构相同。在处理传送门和其他更高级技术时,这一区别很重要。对于良好实现的组件,直觉应该是事件从子组件冒泡到父组件。这样,您在 html! 中编写的层次结构就是事件处理程序观察到的层次结构。

如果您不想要事件冒泡,可以通过调用

yew::set_event_bubbling(false);

在启动应用程序之前。这会加快事件处理速度,但某些组件可能会因未收到预期的事件而中断。请谨慎使用!

事件委托

可能会让人惊讶的是,事件监听器并不是直接注册在它们被渲染的元素上。相反,事件是从 Yew 应用的子树根节点委托的。不过,事件仍然以其原生形式传递,并且不会创建任何合成形式。这可能会导致 HTML 监听器中预期的事件与 Yew 中出现的事件之间的不匹配。

  • Event::current_target 指向 Yew 子树根节点,而不是添加监听器的元素。如果您想访问底层的 HtmlElement,请使用 NodeRef
  • Event::event_phase 始终是 Event::CAPTURING_PHASE。在内部,事件将表现得像是在冒泡阶段,事件传播将被重放,并且事件会向上冒泡,即虚拟 DOM 中较高的事件监听器将在较低的事件监听器之后触发。目前,Yew 不支持捕获监听器。

这也意味着由 Yew 注册的事件通常会在其他事件监听器之前触发。

具备类型的事件目标

警告

在本节中,target (Event.target) 始终指的是事件从其派发的元素。

不一定总是指代 Callback 所放置的元素。

在事件 Callback 中,您可能希望获取该事件的目标。例如,change 事件没有提供任何信息,但用于通知某些内容已更改。

在 Yew 中,以正确的类型获取目标元素可以通过几种方式完成,我们将在这里逐一介绍。调用事件上的 web_sys::Event::target 返回一个可选的 web_sys::EventTarget 类型,当您想知道输入元素的值时,这可能看起来不是很有用。

在下面的所有方法中,我们将解决相同的问题,以便清楚地了解方法的不同之处,而不是手头的问题。

问题:

我们在 <input> 元素上有一个 onchange Callback,每次调用时,我们希望向组件发送一个更新 Msg

我们的 Msg 枚举如下所示:

pub enum Msg {
InputValue(String),
}

使用 JsCast

wasm-bindgen crate 有一个有用的 trait:JsCast,它允许我们在类型之间直接转换,只要它实现了 JsCast 就行。我们可以谨慎地转换,这涉及运行时检查和处理 OptionResult 的逻辑,或者我们也可以冒险直接强行转换。

多说无益,看代码:

Cargo.toml
[dependencies]
# 需要 wasm-bindgen 用于调用 JsCast
wasm-bindgen = "0.2"
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlInputElement};
use yew::prelude::*;

#[function_component]
fn MyComponent() -> Html {
let input_value_handle = use_state(String::default);
let input_value = (*input_value_handle).clone();

let on_cautious_change = {
let input_value_handle = input_value_handle.clone();

Callback::from(move |e: Event| {
// 当事件被创建时,目标是未定义的,只有在派发时才会添加目标。
let target: Option<EventTarget> = e.target();
// 事件可能会冒泡,因此此侦听器可能会捕获不是 HtmlInputElement 类型的子元素的事件。
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());

if let Some(input) = input {
input_value_handle.set(input.value());
}
})
};

let on_dangerous_change = Callback::from(move |e: Event| {
let target: EventTarget = e
.target()
.expect("Event should have a target when dispatched");
// 你必须了解 target 是 HtmlInputElement,否则调用 value 将是未定义行为(UB)。
// 在这里,我们确信这是输入元素,因此我们可以在不检查的情况下将其转换为适当的类型。
input_value_handle.set(target.unchecked_into::<HtmlInputElement>().value());
});

html! {
<>
<label for="cautious-input">
{ "My cautious input:" }
<input onchange={on_cautious_change}
id="cautious-input"
type="text"
value={input_value.clone()}
/>
</label>
<label for="dangerous-input">
{ "My dangerous input:" }
<input onchange={on_dangerous_change}
id="dangerous-input"
type="text"
value={input_value}
/>
</label>
</>
}
}

JsCast 提供的方法是 dyn_intounchecked_into。 如你所见,它们允许我们从 EventTarget 转换为 HtmlInputElementdyn_into 方法是谨慎的,因为它会在运行时检查类型是否实际为 HtmlInputElement,如果不是则返回 Err(JsValue)JsValue 是一个通用类型,将原来的对象返回给你,以便再次尝试转换为别的类型。

这会儿你可能会想,什么时候可以使用危险版本?在上面的情况下,它是安全的1,因为我们将 Callback 设置在一个没有子元素的元素上,所以目标只能是同一个元素。

1 只要涉及到 JS 领域,就是安全的。

使用 TargetCast

强烈建议先阅读 使用 JsCast

备注

TargetCast 的设计目的是让新用户了解 JsCast 的行为,但范围更小,仅涉及事件及其目标。

选用 TargetCastJsCast 纯粹是个人偏好,实际您会发现 TargetCast 的实现和 JsCast 的功能很相似。

TargetCast trait 是在 JsCast 基础之上构建的,专门用于从事件中获取类型化的事件目标。

TargetCast 是 Yew 的一部分,因此无需添加依赖项即可在事件上使用 trait 方法,但它的工作方式与 JsCast 非常相似。

use web_sys::HtmlInputElement;
use yew::prelude::*;

#[function_component]
fn MyComponent() -> Html {
let input_value_handle = use_state(String::default);
let input_value = (*input_value_handle).clone();

let on_cautious_change = {
let input_value_handle = input_value_handle.clone();

Callback::from(move |e: Event| {
let input = e.target_dyn_into::<HtmlInputElement>();

if let Some(input) = input {
input_value_handle.set(input.value());
}
})
};

let on_dangerous_change = Callback::from(move |e: Event| {
// 你必须清楚 target 是 HtmlInputElement,否则调用 value 将是未定义行为(UB)。
input_value_handle.set(e.target_unchecked_into::<HtmlInputElement>().value());
});

html! {
<>
<label for="cautious-input">
{ "My cautious input:" }
<input onchange={on_cautious_change}
id="cautious-input"
type="text"
value={input_value.clone()}
/>
</label>
<label for="dangerous-input">
{ "My dangerous input:" }
<input onchange={on_dangerous_change}
id="dangerous-input"
type="text"
value={input_value}
/>
</label>
</>
}
}

如果您已经了解了 JsCast,或者了解了这个 trait,您可能会发现 TargetCast::target_dyn_intoJsCast::dyn_into 相似,但专门用于事件的目标。TargetCast::target_unchecked_intoJsCast::unchecked_into 类似,因此上面关于 JsCast 的所有警告都适用于 TargetCast

使用 NodeRef

NodeRef 可以代替查询给定给 Callback 的事件。

use web_sys::HtmlInputElement;
use yew::prelude::*;

#[function_component]
fn MyComponent() -> Html {
let input_node_ref = use_node_ref();

let input_value_handle = use_state(String::default);
let input_value = (*input_value_handle).clone();

let onchange = {
let input_node_ref = input_node_ref.clone();

Callback::from(move |_| {
let input = input_node_ref.cast::<HtmlInputElement>();

if let Some(input) = input {
input_value_handle.set(input.value());
}
})
};

html! {
<>
<label for="my-input">
{ "My input:" }
<input ref={input_node_ref}
{onchange}
id="my-input"
type="text"
value={input_value}
/>
</label>
</>
}
}

通过 NodeRef,你可以忽略事件并使用 NodeRef::cast 方法获取一个 Option<HtmlInputElement> - 这是可选的,因为在设置 NodeRef 之前调用 cast,或者类型不匹配时将返回 None

你可能会看到,通过使用 NodeRef,我们不必将 String 发送回状态,因为我们总是访问 input_node_ref - 因此我们可以这样做:

use web_sys::HtmlInputElement;
use yew::prelude::*;

#[function_component]
fn MyComponent() -> Html {
let input_node_ref = use_node_ref();

let onchange = {
let input_node_ref = input_node_ref.clone();

Callback::from(move |_| {
if let Some(input) = input_node_ref.cast::<HtmlInputElement>() {
let value = input.value();
// 对 value 做点什么
}
})
};

html! {
<>
<label for="my-input">
{ "My input:" }
<input ref={input_node_ref}
{onchange}
id="my-input"
type="text"
/>
</label>
</>
}
}

您选择哪种方法取决于您的组件和您的偏好,没有所谓的推荐方法。

手动事件监听器

您可能希望监听 Yew 的 html 宏不支持的事件,查看这里列出的支持的事件

为了手动向某个元素添加事件监听器,我们需要借助 NodeRef,以便在 use_effect_with 中使用 web-syswasm-bindgen API 添加监听器。

以下示例将展示如何为虚构的 custard 事件添加监听器。所有不受 yew 支持的事件或自定义事件都可以表示为 web_sys::Event。如果您需要访问自定义/不受支持事件的特定方法或字段,可以使用 JsCast 的方法将其转换为所需的类型。

使用 Closure(冗长版本)

直接使用 web-syswasm-bindgen 的接口可能有点痛苦……所以要有点心理准备(感谢 gloo,有了更简洁的方法)。

use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::HtmlElement;
use yew::prelude::*;

#[function_component]
fn MyComponent() -> Html {
let div_node_ref = use_node_ref();

use_effect_with(
div_node_ref.clone(),
{
let div_node_ref = div_node_ref.clone();

move |_| {
let mut custard_listener = None;

if let Some(element) = div_node_ref.cast::<HtmlElement>() {
// 创建您通常会创建的 Callback
let oncustard = Callback::from(move |_: Event| {
// 对 custard 做点什么..
});

// 从 Box<dyn Fn> 创建一个 Closure - 这必须是 'static
let listener =
Closure::<dyn Fn(Event)>::wrap(
Box::new(move |e: Event| oncustard.emit(e))
);

element
.add_event_listener_with_callback(
"custard",
listener.as_ref().unchecked_ref()
)
.unwrap();

custard_listener = Some(listener);
}

move || drop(custard_listener)
}
}
);

html! {
<div ref={div_node_ref} id="my-div"></div>
}
}

有关 Closure 的更多信息,请参见 wasm-bindgen 指南

使用 gloo(简洁版本)

更方便的方法是使用 gloo,更具体地说是 gloo_events, 它是 web-syswasm-bindgen 的高层抽象实现。

gloo_events 提供了 EventListener 类型,可以用于创建和存储事件监听器。

Cargo.toml
[dependencies]
gloo-events = "0.1"
use web_sys::HtmlElement;
use yew::prelude::*;

use gloo::events::EventListener;

#[function_component]
fn MyComponent() -> Html {
let div_node_ref = use_node_ref();

use_effect_with(
div_node_ref.clone(),
{
let div_node_ref = div_node_ref.clone();

move |_| {
let mut custard_listener = None;

if let Some(element) = div_node_ref.cast::<HtmlElement>() {
// 创建您通常会创建的 Callback
let oncustard = Callback::from(move |_: Event| {
// 对 custard 做点什么..
});

// 从 Box<dyn Fn> 创建一个 Closure - 这必须是 'static
let listener = EventListener::new(
&element,
"custard",
move |e| oncustard.emit(e.clone())
);

custard_listener = Some(listener);
}

move || drop(custard_listener)
}
}
);

html! {
<div ref={div_node_ref} id="my-div"></div>
}
}

有关 EventListener 的更多信息,请参见 gloo_events docs.rs

可用事件的完整列表

侦听器名称web_sys 事件类型
onabortEvent
onauxclickMouseEvent
onblurFocusEvent
oncancelEvent
oncanplayEvent
oncanplaythroughEvent
onchangeEvent
onclickMouseEvent
oncloseEvent
oncontextmenuMouseEvent
oncuechangeEvent
ondblclickMouseEvent
ondragDragEvent
ondragendDragEvent
ondragenterDragEvent
ondragexitDragEvent
ondragleaveDragEvent
ondragoverDragEvent
ondragstartDragEvent
ondropDragEvent
ondurationchangeEvent
onemptiedEvent
onendedEvent
onerrorEvent
onfocusFocusEvent
onfocusinFocusEvent
onfocusoutFocusEvent
onformdataEvent
oninputInputEvent
oninvalidEvent
onkeydownKeyboardEvent
onkeypressKeyboardEvent
onkeyupKeyboardEvent
onloadEvent
onloadeddataEvent
onloadedmetadataEvent
onloadstartProgressEvent
onmousedownMouseEvent
onmouseenterMouseEvent
onmouseleaveMouseEvent
onmousemoveMouseEvent
onmouseoutMouseEvent
onmouseoverMouseEvent
onmouseupMouseEvent
onpauseEvent
onplayEvent
onplayingEvent
onprogressProgressEvent
onratechangeEvent
onresetEvent
onresizeEvent
onscrollEvent
onsecuritypolicyviolationEvent
onseekedEvent
onseekingEvent
onselectEvent
onslotchangeEvent
onstalledEvent
onsubmitSubmitEvent
onsuspendEvent
ontimeupdateEvent
ontoggleEvent
onvolumechangeEvent
onwaitingEvent
onwheelWheelEvent
oncopyEvent
oncutEvent
onpasteEvent
onanimationcancelAnimationEvent
onanimationendAnimationEvent
onanimationiterationAnimationEvent
onanimationstartAnimationEvent
ongotpointercapturePointerEvent
onloadendProgressEvent
onlostpointercapturePointerEvent
onpointercancelPointerEvent
onpointerdownPointerEvent
onpointerenterPointerEvent
onpointerleavePointerEvent
onpointerlockchangeEvent
onpointerlockerrorEvent
onpointermovePointerEvent
onpointeroutPointerEvent
onpointeroverPointerEvent
onpointerupPointerEvent
onselectionchangeEvent
onselectstartEvent
onshowEvent
ontouchcancelTouchEvent
ontouchendTouchEvent
ontouchmoveTouchEvent
ontouchstartTouchEvent
ontransitioncancelTransitionEvent
ontransitionendTransitionEvent
ontransitionrunTransitionEvent
ontransitionstartTransitionEvent