LoginSignup
1
1

More than 1 year has passed since last update.

yewで複数のInputを扱う

Last updated at Posted at 2022-12-20

yewでinput要素を扱う方法は公式ドキュメントのEventsについてのページに詳しく書かれています.イベント型から取得できるEventTargetweb_sys::HtmlInputElementに変換してvalueclickedなどを取得する方法と,NodeRefをInput要素に結び付けてNodeRefweb_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を利用する方法

公式ドキュメントにあるようにEventTargetweb_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_stateuse_reducerを使います.

let checkbox_handle = use_mut_ref(|| double_input_value.checkbox.clone());
let text_handle = use_mut_ref(|| double_input_value.text.clone());

changeイベントに結び付けるコールバックの定義は以下です.ResultOptionが返りますが,ErrNoneとなるのは結びつける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!におけるチェックボックスの部分は以下です.forhtml_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_refNodeRefを取得します.チェックボックスはその要素の数だけ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::castNoneを返すことは無いため,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>
        </>
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1