イベント
はじめに
Yewはweb-sys
クレートと統合し、そのクレートのイベントを使用します。下記の表に、html!
マクロで受け入れられるすべてのweb-sys
イベントがリストされています。
下記の表にリストされていないイベントに対しても、Callback
を追加できます。手動イベントリスナーを参照してください。
イベントの種類
以下の表に記載されているすべてのイベントタイプは、yew::events
の下で再エクスポートされます。yew::events
の型を使用すると、クレートにweb-sys
を手動で依存関係として含める場合よりも、バージョン互換性を確保しやすくなります。これは、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によって登録されたイベントが通常、他のイベントリスナーの前に発生することを意味します。
型付きイベントターゲット
このセクションでは、**ターゲット(Event.target
)**は常に、イベントがディスパッチされた要素を参照しています。
これは、Callback
が配置されている要素とは**常に一致するとは限りません**。
イベントCallback
では、そのイベントのターゲットを取得したい場合があります。たとえば、change
イベントは情報を与えませんが、何かが変更されたことを通知するために使用されます。
Yewでは、正しい型のターゲット要素を取得する方法はいくつかあり、ここではそれらを説明します。web_sys::Event::target
をイベントで呼び出すと、オプションのweb_sys::EventTarget
型が返されますが、入力要素の値を知りたい場合はあまり役に立たないかもしれません。
以下のすべての方法では、同じ問題に取り組むので、問題自体ではなく、アプローチの違いが明確になります。
問題
<input>
要素にonchange
Callback
があり、それが呼び出されるたびに、コンポーネントにupdate Msg
を送信したいと考えています。
Msg
enumは次のようになります。
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
は万能型であり、本質的にオブジェクトを返して再試行させます。
この時点で、「危険なバージョンはいつ使用しても問題ないのか?」と考えているかもしれません。上記のケースでは、Callback
が子を持たない要素に設定されているため、ターゲットは同じ要素のみになるため、安全です1。
1 JSの世界に関わるものとしては、安全といえる範囲で。
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
にアクセスできるため、状態にString
を戻す必要がないこともわかるでしょう。そのため、次のようにできます。
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 | Event |
onauxclick | MouseEvent |
onblur | FocusEvent |
oncancel | Event |
oncanplay | Event |
oncanplaythrough | Event |
onchange | Event |
onclick | MouseEvent |
onclose | Event |
oncontextmenu | MouseEvent |
oncuechange | Event |
ondblclick | MouseEvent |
ondrag | DragEvent |
ondragend | DragEvent |
ondragenter | DragEvent |
ondragexit | DragEvent |
ondragleave | DragEvent |
ondragover | DragEvent |
ondragstart | DragEvent |
ondrop | DragEvent |
ondurationchange | Event |
onemptied | Event |
onended | Event |
onerror | Event |
onfocus | FocusEvent |
onfocusin | FocusEvent |
onfocusout | FocusEvent |
onformdata | Event |
oninput | InputEvent |
oninvalid | Event |
onkeydown | KeyboardEvent |
onkeypress | KeyboardEvent |
onkeyup | KeyboardEvent |
onload | Event |
onloadeddata | Event |
onloadedmetadata | Event |
onloadstart | ProgressEvent |
onmousedown | MouseEvent |
onmouseenter | MouseEvent |
onmouseleave | MouseEvent |
onmousemove | MouseEvent |
onmouseout | MouseEvent |
onmouseover | MouseEvent |
onmouseup | MouseEvent |
onpause | Event |
onplay | Event |
onplaying | Event |
onprogress | ProgressEvent |
onratechange | Event |
onreset | Event |
onresize | Event |
onscroll | Event |
onsecuritypolicyviolation | Event |
onseeked | Event |
onseeking | Event |
onselect | Event |
onslotchange | Event |
onstalled | Event |
onsubmit | SubmitEvent |
onsuspend | Event |
ontimeupdate | Event |
ontoggle | Event |
onvolumechange | Event |
onwaiting | Event |
onwheel | WheelEvent |
oncopy | Event |
oncut | Event |
onpaste | Event |
onanimationcancel | AnimationEvent |
onanimationend | AnimationEvent |
onanimationiteration | AnimationEvent |
onanimationstart | AnimationEvent |
ongotpointercapture | PointerEvent |
onloadend | ProgressEvent |
onlostpointercapture | PointerEvent |
onpointercancel | PointerEvent |
onpointerdown | PointerEvent |
onpointerenter | PointerEvent |
onpointerleave | PointerEvent |
onpointerlockchange | Event |
onpointerlockerror | Event |
onpointermove | PointerEvent |
onpointerout | PointerEvent |
onpointerover | PointerEvent |
onpointerup | PointerEvent |
onselectionchange | Event |
onselectstart | Event |
onshow | Event |
ontouchcancel | TouchEvent |
ontouchend | TouchEvent |
ontouchmove | TouchEvent |
ontouchstart | TouchEvent |
ontransitioncancel | TransitionEvent |
ontransitionend | TransitionEvent |
ontransitionrun | TransitionEvent |
ontransitionstart | TransitionEvent |