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](https://rustwasm.github .io/wasm-bindgen/api/wasm_bindgen/trait.JsCast.html),它允許我們在類型之間直接轉換,只要它實現了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