メインコンテンツまでスキップ
Version: Next

イベント

紹介

Yew は web-sys クレートと統合されており、このクレートのイベントを使用します。以下のには、html! マクロで受け入れられるすべての web-sys イベントが一覧表示されています。

下記の表に記載されていないイベントについても、Callback を追加してリッスンすることができます。詳細は手動イベントリスナーを参照してください。

イベントタイプ

tip

すべてのイベントタイプは 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 によって登録されたイベントは通常、他のイベントリスナーよりも先にトリガーされるということです。

型付きイベントターゲット

caution

このセクションでは、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 クレートには便利なトレイトがあります:JsCast。これにより、型が JsCast を実装している限り、型間の直接キャストが可能になります。慎重にキャストすることもできますが、これはランタイムチェックと OptionResult のロジックを処理することを伴います。また、強制的にキャストすることもできます。

コードを見てみましょう:

Cargo.toml
[dependencies]
# JsCast を呼び出すために wasm-bindgen が必要です
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 から HtmlInputElement への変換が可能になります。dyn_into メソッドは慎重で、実行時に型が実際に HtmlInputElement であるかどうかをチェックし、そうでない場合は Err(JsValue) を返します。JsValue は汎用型で、元のオブジェクトを返し、別の型への変換を再試行することができます。

ここで、危険なバージョンを使用するタイミングについて考えるかもしれません。上記のケースでは、子要素のない要素に Callback を設定しているため、ターゲットは同じ要素である必要があるため、安全です1

1 JS の領域に関わる限り、安全です。

TargetCast の使用

JsCast の使用 を先に読むことを強くお勧めします!

note

TargetCast は新しいユーザーが 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| {
// 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 についてすでに知っているか、このトレイトに精通している場合、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 を使用することで、常に 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();
// 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| {
// カスタードに対して何かを行う..
});

// 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| {
// カスタードに対して何かを行う..
});

// 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