yewでinput要素を扱う方法は公式ドキュメントのEventsについてのページに詳しく書かれています.イベント型から取得できるEventTarget
をweb_sys::HtmlInputElement
に変換してvalue
やclicked
などを取得する方法と,NodeRef
をInput要素に結び付けてNodeRef
をweb_sys::HtmlInputElement
に変換する方法が書かれています.今回はその二つの方法で複数のinput要素(<input type="text"/>
と<input type="checkbox">
)から得られた値の親コンポーネントへの送信(送信は更新ボタンで行う)を実装してみます.コード全体はこちらにあります.
共通部分
checkboxのために列挙体を定義します.列挙体と文字列の変換や列挙体のイテレーターへの変換,バリアントのカウントなどができるstrum を利用します.Mozillaのリファレンスの例を利用しています.
use strum::EnumCount;
use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter as EnumIterMacro, EnumString};
#[derive(Display, EnumCountMacro, EnumString, EnumIterMacro, Clone, Copy)]
pub enum CheckBoxName {
Scales,
Horns,
}
コンポーネントへ渡すpropsは以下のようにします.String
の代わりにyew::prelude::AttrValue
を使っています.
use yew::prelude::*;
/// 親子コンポーネント間で渡し合うデータ
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DoubleInputValue {
pub text: AttrValue,
pub checkbox: Vec<bool>,
}
impl Default for DoubleInputValue {
fn default() -> Self {
Self {
text: AttrValue::default(),
checkbox: {
let mut bool_vec = vec![false; CheckBoxName::COUNT];
bool_vec[CheckBoxName::Scales as usize] = true;
bool_vec
},
}
}
}
/// props
#[derive(Properties, PartialEq, Clone)]
pub struct DouubleInputProps {
pub double_input_value: DoubleInputValue,
pub onsubmit: Callback<DoubleInputValue>,
}
JsCastを利用する方法
公式ドキュメントにあるようにEventTarget
をweb_sys::HtmlInputElement
などに変換する方法です.素直に要素のvalue
などに対応する状態を利用して,onchange
で変更を追跡します.更新ボタンのクリックでMultiInputValue
を親コンポーネントへ送信します.
use strum::IntoEnumIterator;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[function_component(DoubleInputV1)]
pub fn double_input_v1(double_input_props: &DouubleInputProps) -> Html {
let double_input_value = &double_input_props.double_input_value;
let checkbox_handle = use_mut_ref(|| double_input_value.checkbox.clone());
let text_handle = use_mut_ref(|| double_input_value.text.clone());
let checkbox_onchange = {
let checkbox_handle = checkbox_handle.clone();
Callback::from(move |e: Event| {
let input_element = e
.target()
.expect("Event should have a target when dispatched")
.dyn_into::<HtmlInputElement>()
.expect("This EventTarget should cast into HtmlInputElement");
let changed_checkbox_name: CheckBoxName = input_element
.value()
.parse()
.expect("This Value should parse into CheckBoxName");
checkbox_handle.borrow_mut()[changed_checkbox_name as usize] = input_element.checked();
})
};
let text_onchange = {
let text_handle = text_handle.clone();
Callback::from(move |e: Event| {
let input_element = e
.target()
.expect("Event should have a target when dispatched")
.dyn_into::<HtmlInputElement>()
.expect("This EventTarget should cast into HtmlInputElement");
*text_handle.borrow_mut() = AttrValue::from(input_element.value());
})
};
let onsubmit = {
let parent_onsubmit = double_input_props.onsubmit.clone();
let checkbox_handle = checkbox_handle;
let text_handle = text_handle;
Callback::from(move |_: MouseEvent| {
let double_input_value = DoubleInputValue {
checkbox: checkbox_handle.replace_with(|value| value.clone()),
text: text_handle.replace_with(|value| value.clone()),
};
parent_onsubmit.emit(double_input_value);
})
};
html! {
<fieldset class="double-form">
<legend>{"Double nput V1"}</legend>
<div>
{
for CheckBoxName::iter().map(|check_box_name|{
html_nested!{
<label>
{check_box_name.to_string()}
<input
type="checkbox"
onchange={checkbox_onchange.clone()}
checked={double_input_value.checkbox[check_box_name as usize]}
value={check_box_name.to_string()}
/>
</label>
}
})
}
</div>
<div>
<label>
{"Name:"}
<input
type="text"
onchange={text_onchange}
value={double_input_value.text.clone()}
/>
</label>
</div>
<div>
<button onclick={onsubmit}>{"更新"}</button>
</div>
</fieldset>
}
}
長いため一つずつ説明します.状態の初期化の部分は以下です.今回はinput要素の値に応じて状態を更新するだけであるためuse_mut_ref
を用いています.use_mut_ref
が返すのはRc<RefCell<T>>
です.つまりこのコンポーネントの再レンダリングのタイミングは親コンポーネントが渡すpropsが変更された時のみです.
バリデーションを走らせてその結果を表示したりするなど状態の更新ごとに再レンダリングが必要な場合はuse_state
やuse_reducer
を使います.
let checkbox_handle = use_mut_ref(|| double_input_value.checkbox.clone());
let text_handle = use_mut_ref(|| double_input_value.text.clone());
changeイベントに結び付けるコールバックの定義は以下です.Result
やOption
が返りますが,Err
やNone
となるのは結びつけるhtml要素やイベントを間違えた時であるためexpect
でアンラップしています.checkboxではvalue
の値を先ほど定義した列挙体にパースしていますが,こちらもhtml!
内でvalue
の与え方を間違えなければエラーとはならないはずです.
let checkbox_onchange = {
let checkbox_handle = checkbox_handle.clone();
Callback::from(move |e: Event| {
let input_element = e
.target()
.expect("Event should have a target when dispatched")
.dyn_into::<HtmlInputElement>()
.expect("This EventTarget should cast into HtmlInputElement");
let changed_checkbox_name: CheckBoxName = input_element
.value()
.parse()
.expect("This Value should parse into CheckBoxName");
checkbox_handle.borrow_mut()[changed_checkbox_name as usize] = input_element.checked();
})
};
let text_onchange = {
let text_handle = text_handle.clone();
Callback::from(move |e: Event| {
let input_element = e
.target()
.expect("Event should have a target when dispatched")
.dyn_into::<HtmlInputElement>()
.expect("This EventTarget should cast into HtmlInputElement");
*text_handle.borrow_mut() = AttrValue::from(input_element.value());
})
};
更新ボタンに結びつける(親コンポーネントに値を送信する)コールバックは以下です.Rc<RefCell<T>>
からclone
で値を取り出しています.
let onsubmit = {
let parent_onsubmit = double_input_props.onsubmit.clone();
let checkbox_handle = checkbox_handle;
let text_handle = text_handle;
Callback::from(move |_: MouseEvent| {
let double_input_value = DoubleInputValue {
checkbox: checkbox_handle.replace_with(|value| value.clone()),
text: text_handle.replace_with(|value| value.clone()),
};
parent_onsubmit.emit(double_input_value);
})
};
html!
におけるチェックボックスの部分は以下です.for
とhtml_nested!
を用いて複数のチェックボックスを定義します.strumを用いることで列挙体のイテレーターへの変換やバリアントのto_string
が利用できるためvalue
に間違った値を与える心配はありません.これはselect要素や<input type="radio"/>
などを扱うときにも便利です.
{
for CheckBoxName::iter().map(|check_box_name|{
html_nested!{
<label>
{check_box_name.to_string()}
<input
type="checkbox"
onchange={checkbox_onchange.clone()}
checked={double_input_value.checkbox[check_box_name as usize]}
value={check_box_name.to_string()}
/>
</label>
}
})
}
テキストボックスの部分と更新ボタンの部分は公式ドキュメントの例と大差ないため割愛します.
この方法ではchangeイベントごとにコールバックを実行するため,今回の例では冗長に感じます.更新ボタンのclickイベントに紐付けたコールバックの中でinputイベントのvalue
などを取得できれば,状態として保持し変更を追跡させる必要もありません.その場合は次に説明するNodeRef
を利用できます.
NodeRefを利用する方法
公式ドキュメントにあるようにuse_node_ref
で得られたNodeRef
をhtml要素に結び付け,コールバック内でweb_sys::HtmlInputElement
などに変更する方法です.コールバックはonsubmit
のみであり,その中でvalue
などを取得します.
use strum::{EnumCount, IntoEnumIterator};
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[function_component(DoubleInputV2)]
pub fn double_input_v2(double_input_props: &DouubleInputProps) -> Html {
let double_input_value = &double_input_props.double_input_value;
let checkbox_refs: [NodeRef; CheckBoxName::COUNT] = [use_node_ref(), use_node_ref()];
let text_ref = use_node_ref();
let onsubmit = {
let parent_onsubmit = double_input_props.onsubmit.clone();
let checkbox_refs = checkbox_refs.clone();
let text_ref = text_ref.clone();
Callback::from(move |_| {
let checkbox = checkbox_refs
.iter()
.map(|node_ref| {
node_ref
.cast::<HtmlInputElement>()
.expect("This NodeRef should cast into HtmlInputElement")
.checked()
})
.collect::<Vec<_>>();
let text: AttrValue = text_ref
.cast::<HtmlInputElement>()
.expect("This NodeRef should cast into HtmlInputElement")
.value()
.into();
let value = DoubleInputValue { checkbox, text };
parent_onsubmit.emit(value);
})
};
html! {
<fieldset class="double-form">
<legend>{"Double nput V2"}</legend>
<div>
{
for CheckBoxName::iter().map(|check_box_name|{
html_nested!{
<label>
{check_box_name.to_string()}
<input
type="checkbox"
ref={checkbox_refs[check_box_name as usize].clone()}
checked={double_input_value.checkbox[check_box_name as usize]}
value={check_box_name.to_string()}
/>
</label>
}
})
}
</div>
<div>
<label>
{"Name:"}
<input
type="text"
ref={text_ref}
value={double_input_value.text.clone()}
/>
</label>
</div>
<div>
<button onclick={onsubmit}>{"更新"}</button>
</div>
</fieldset>
}
}
長いため一つずつ説明します.状態の初期化の部分は以下です.use_node_ref
でNodeRef
を取得します.チェックボックスはその要素の数だけNodeRef
が必要になります.CheckBoxName
列挙体をイテレーションしてhtml要素と結びつけるため,この配列のNodeRef
の順番は列挙体のバリアントの順番と対応します.
let checkbox_refs: [NodeRef; CheckBoxName::COUNT] = [use_node_ref(), use_node_ref()];
// let checkbox_refs: [NodeRef; CheckBoxName::COUNT] = Default::default(); // こちらでも良い
let text_ref = use_node_ref();
更新ボタンに結びつける(親コンポーネントに値を送信する)コールバックは以下です.NodeRef
と結びつけるhtml要素を間違えなければNodeRef::cast
がNone
を返すことは無いため,expect
でアンラップしています.
let onsubmit = {
let parent_onsubmit = double_input_props.onsubmit.clone();
let checkbox_refs = checkbox_refs.clone();
let text_ref = text_ref.clone();
Callback::from(move |_| {
let checkbox = checkbox_refs
.iter()
.map(|node_ref| {
node_ref
.cast::<HtmlInputElement>()
.expect("This NodeRef should cast into HtmlInputElement")
.checked()
})
.collect::<Vec<_>>();
let text: AttrValue = text_ref
.cast::<HtmlInputElement>()
.expect("This NodeRef should cast into HtmlInputElement")
.value()
.into();
let value = DoubleInputValue { checkbox, text };
parent_onsubmit.emit(value);
})
html!
におけるチェックボックスの部分は以下です.JsCast
を用いた例と異なる点はref
を用いてNodeRef
と結びつけていることとonchange
を利用していないことです.
{
for CheckBoxName::iter().map(|check_box_name|{
html_nested!{
<label>
{check_box_name.to_string()}
<input
type="checkbox"
ref={checkbox_refs[check_box_name as usize].clone()}
checked={double_input_value.checkbox[check_box_name as usize]}
value={check_box_name.to_string()}
/>
</label>
}
})
}
テキストボックスの部分の部分は以下です.onchange
を用いずにref
を用いています.
<label>
{"Name:"}
<input
type="text"
ref={text_ref}
value={double_input_value.text.clone()}
/>
</label>
親コンポーネント
親コンポーネントでは以下のように利用できます.double_input_onsubmit
で子コンポーネントから送られたDoubleInputvalue
を利用したコールバックを定義しています.
use yew::prelude::*;
#[function_component(App)]
fn app() -> Html {
let double_input_value_handle = use_state(DoubleInputValue::default);
let double_input_onsubmit = {
let double_input_value_handle = double_input_value_handle.clone();
Callback::from(move |double_input_value| {
double_input_value_handle.set(double_input_value);
})
};
let double_input_props = DouubleInputProps {
double_input_value: (*double_input_value_handle).clone(),
onsubmit: double_input_onsubmit,
};
html! {
<>
<DoubleInputV1 ..double_input_props.clone()/>
<DoubleInputV2 ..double_input_props/>
<div>{format!("{:?}", *double_input_value_handle)}</div>
</>
}
}