本文へスキップ
バージョン: 0.21

イベント

はじめに

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を実装している限り、目的の型にたどり着くことができます。実行時チェックと、OptionResultなどの失敗タイプを伴う慎重な方法と、危険な方法があります。

説明はこれくらいにして、コードを見ていきましょう。

Cargo.toml
[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_intounchecked_intoであり、おそらくお分かりのとおり、これらによってEventTargetからHtmlInputElementに移動できました。dyn_intoメソッドは慎重な方法であり、実行時に型が実際にHtmlInputElementであるかどうかをチェックし、そうでない場合はErr(JsValue)を返します。 JsValueは万能型であり、本質的にオブジェクトを返して再試行させます。

この時点で、「危険なバージョンはいつ使用しても問題ないのか?」と考えているかもしれません。上記のケースでは、Callbackが子を持たない要素に設定されているため、ターゲットは同じ要素のみになるため、安全です1

1 JSの世界に関わるものとしては、安全といえる範囲で。

TargetCastの使用

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

注記

TargetCastJsCastと非常に似た挙動になるように設計されています。これは、新しいユーザーがイベントとそのターゲットの範囲が小さいJsCastの動作を理解できるようにするためです。

TargetCastJsCastは純粋に好みの問題です。TargetCastJsCastを使用する場合と同様の機能を実装していることがわかります。

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_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();
// 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_withweb-syswasm-bindgen APIを使用してリスナーを追加できます。

以下の例では、架空のcustardイベントにリスナーを追加する方法を示します。Yewでサポートされていないイベントやカスタムイベントはすべて、web_sys::Eventとして表現できます。カスタムイベントまたはサポートされていないイベントの特定のメソッドまたはフィールドにアクセスする必要がある場合は、JsCastのメソッドを使用して必要な型に変換できます。

Closureの使用(冗長)

web-syswasm-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-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>() {
// 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イベント型
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