事件
簡介
Yew 整合 web-sys
板條箱並使用該板條箱中的事件。 下表 列出 web-sys
中所有在 html!
巨集中可接受的事件。
您仍可以針對下表中未列出的事件加入 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 app 的子樹根節點委派事件。即便如此,事件還是會以原生形式傳遞,且不會建立合成表單。這可能導致在 HTML 監聽器預期的事件,以及 Yew 中顯示的事件之間不匹配。
-
Event::current_target
指向的是 Yew 子樹根節點,而不是監聽器所加入的元素。如果您想要存取底層的HtmlElement
,請使用NodeRef
。 -
Event::event_phase
永遠都是Event::CAPTURING_PHASE
。在內部,事件會表現得好像在冒泡階段 (bubbling phase) 一樣,事件傳播會被重播,而事件會 「向上」冒泡,換句話說,在虛擬 DOM 中位階較高的事件監聽器會在它們下方的事件監聽器 之後 觸發。目前,Yew 不支援攔截監聽器。這也表示 Yew 註冊的事件通常會在其他事件監聽器之前觸發。
已輸入型別的事件目標
在此區段,目標 (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
箱子中有一個有用的特性 JsCast
,只要這個特性實作了 JsCast
,就能使用這個特性跳躍並轉換至我們想要的種類。我們可以謹慎點,使用一些執行時期檢查和失敗類型,例如 Option
以及 Result
,或者我們可以危險地進行這項動作。
講得夠多了,來看看實際程式碼
[dependencies]
# need wasm-bindgen for 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| {
// When events are created the target is undefined, it's only
// when dispatched does the target get added.
let target: Option<EventTarget> = e.target();
// Events can bubble so this listener might catch events from child
// elements which are not of type 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");
// You must KNOW target is a HtmlInputElement, otherwise
// the call to value would be Undefined Behaviour (UB).
// Here we are sure that this is input element so we can convert it to the appropriate type without checking
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_into
與 unchecked_into
,你可能會看到這些方法能讓我們從 EventTarget
轉換到 HtmlInputElement
。dyn_into
方法很謹慎,因為它會在執行時期檢查此種類是否真的是 HtmlInputElement
,如果不是,則會傳回一個 Err(JsValue)
,JsValue
是一個通用的類型,基本上會提供你一個物件,讓你再試一次。
在這個階段,你可能會想... 什麼時候使用危險的版本才是正確的?以上面的例子來說,使用危險版本是安全的1,因為我們已經在一個沒有子代元素的元素上設定了 Callback
,所以目標只能是同一個元素。
1 在 JavaScript 土地上,能安全的地方就叫做安全。
使用 TargetCast
強烈建議先閱讀 使用 JsCast!
TargetCast
的設計宗旨與 JsCast
非常類似,目的是讓新使用者能了解 JsCast
的行為,不過範圍縮小到事件及其目標。
TargetCast
與 JsCast
純粹是喜好問題,你會發現 TargetCast
執行的動作與你使用 JsCast
很類似。
TargetCast
特性是建立在 JsCast
之上,特別用來從事件中取得已類型化的事件目標。
TargetCast
是 Yew 的附屬函式庫,所以不需要加入依賴就能在事件中使用此特性的方法,但它的運作方式與 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| {
// You must KNOW target is a HtmlInputElement, otherwise
// the call to value would be Undefined Behaviour (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
的內容,或是了解該特性,你可能就看得出來,TargetCast::target_dyn_into
有點類似於 JsCast::dyn_into
,但會特別針對事件的目標進行強制轉型。TargetCast::target_unchecked_into
類似於 JsCast::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
來判斷我們不需要再將 字串
傳回狀態,因為我們總是會存取 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();
// do something with 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-sys
和 wasm-bindgen API 加入一個監聽器。
以下範例會顯示如何新增用於自訂 custard
事件的監聽器。所有由 yew 不支援或自訂的事件都可以表示為 web_sys::Event
。如果你需要存取自訂/不支援事件中的特定方法或欄位,則可以使用 JsCast
的方法才能轉換為所需的類型。
使用 Closure
(冗長)
直接使用 web-sys
和 wasm-bindgen
API 來執行這件事可能會有點痛苦... 因此請做好心理準備(感謝 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>() {
// Create your Callback as you normally would
let oncustard = Callback::from(move |_: Event| {
// do something about custard..
});
// Create a Closure from a Box<dyn Fn> - this has to be '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-sys
、wasm-bindgen
的抽象化延伸。
gloo_events
具有 EventListener
類型,可使用它建立和儲存事件聆聽器。
[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>() {
// Create your Callback as you normally would
let oncustard = Callback::from(move |_: Event| {
// do something about custard..
});
// Create a Closure from a Box<dyn Fn> - this has to be '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 事件類型 |
---|---|
onabort | 事件 |
onauxclick | 滑鼠事件 |
onblur | 焦點事件 |
oncancel | 事件 |
oncanplay | 事件 |
oncanplaythrough | 事件 |
onchange | 事件 |
onclick | 滑鼠事件 |
onclose | 事件 |
oncontextmenu | 滑鼠事件 |
oncuechange | 事件 |
ondblclick | 滑鼠事件 |
ondrag | 拖曳事件 |
ondragend | 拖曳事件 |
ondragenter | 拖曳事件 |
ondragexit | 拖曳事件 |
ondragleave | 拖曳事件 |
ondragover | 拖曳事件 |
ondragstart | 拖曳事件 |
ondrop | 拖曳事件 |
ondurationchange | 事件 |
onemptied | 事件 |
onended | 事件 |
onerror | 事件 |
onfocus | 焦點事件 |
onfocusin | 焦點事件 |
onfocusout | 焦點事件 |
onformdata | 事件 |
oninput | 輸入事件 |
oninvalid | 事件 |
onkeydown | 鍵盤事件 |
onkeypress | 鍵盤事件 |
onkeyup | 鍵盤事件 |
onload | 事件 |
onloadeddata | 事件 |
onloadedmetadata | 事件 |
onloadstart | 進度事件 |
onmousedown | 滑鼠事件 |
onmouseenter | 滑鼠事件 |
onmouseleave | 滑鼠事件 |
onmousemove | 滑鼠事件 |
onmouseout | 滑鼠事件 |
onmouseover | 滑鼠事件 |
onmouseup | 滑鼠事件 |
onpause | 事件 |
onplay | 事件 |
onplaying | 事件 |
onprogress | 進度事件 |
onratechange | 事件 |
onreset | 事件 |
onresize | 事件 |
onscroll | 事件 |
onsecuritypolicyviolation | 事件 |
onseeked | 事件 |
onseeking | 事件 |
onselect | 事件 |
onslotchange | 事件 |
onstalled | 事件 |
onsubmit | 提交事件 |
onsuspend | 事件 |
ontimeupdate | 事件 |
ontoggle | 事件 |
onvolumechange | 事件 |
onwaiting | 事件 |
onwheel | 滾輪事件 |
oncopy | 事件 |
oncut | 事件 |
onpaste | 事件 |
onanimationcancel | 動畫事件 |
onanimationend | 動畫事件 |
onanimationiteration | 動畫事件 |
onanimationstart | 動畫事件 |
ongotpointercapture | 指標事件 |
onloadend | 進度事件 |
onlostpointercapture | 指標事件 |
onpointercancel | 指標事件 |
onpointerdown | 指標事件 |
onpointerenter | 指標事件 |
onpointerleave | 指標事件 |
onpointerlockchange | 事件 |
onpointerlockerror | 事件 |
onpointermove | 指標事件 |
onpointerout | 指標事件 |
onpointerover | 指標事件 |
onpointerup | 指標事件 |
onselectionchange | 事件 |
onselectstart | 事件 |
onshow | 事件 |
ontouchcancel | 觸控事件 |
ontouchend | 觸控事件 |
ontouchmove | 觸控事件 |
ontouchstart | 觸控事件 |
ontransitioncancel | 切換事件 |
ontransitionend | 切換事件 |
ontransitionrun | 切換事件 |
ontransitionstart | 切換事件 |