[Yew] ジェネリックコンポーネントでフォームのバリデーション

今回は，Yewを用いて以下のように入力をバリデーションしてメッセージを表示させてみます．`change`イベントごとにバリデーションする方法(上部分)と送信ボタンの`click`イベントでバリデーションする方法(下半分)の二つの方法でinput要素に対応するコンポーネントを作成してみます．コード全体は[こちら](https://github.com/deepgreenAN/yew-validation-input)
![validation_input.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1578803/5a7724b0-bd08-cbf8-6afe-b6d8e6e54f07.gif)
Rustで入力データなどのバリデーションを行う方法として，プリミティブな型からドメイン固有の型への`TryFrom`トレイトを実装し`Result`で返す方法があります．例えば郵便番号を文字列から作成する場合に以下のように`TryFrom<String>`を実装するとします(必ずしも`FromStr`を噛ませる必要はありません)．これによって`String`から`try_into`メソッドによって`PostalCode`に変換でき，失敗した場合`DomainError`が返ります．
```rust:domain.rs
/// 郵便番号
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct PostalCode(String);

impl FromStr for PostalCode {
    type Err = DomainError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // - で分ける
        let (first, second) = s.split_once('-').ok_or(DomainError::DomainParseError)?;
        if first.chars().count() == 3_usize
            && first.chars().all(|c| c.is_ascii_digit())
            && second.chars().count() == 4_usize
            && second.chars().all(|c| c.is_ascii_digit())
        {
            Ok(PostalCode(s.to_string()))
        } else {
            Err(DomainError::DomainParseError)
        }
    }
}

impl TryFrom<String> for PostalCode {
    type Error = DomainError;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        value.parse()
    }
}
```
ここで`DomainError`は以下とします．今回は表示するメッセージをコンポーネントの引数で渡すため全て`DomainParseError`ですが，より詳細なメッセージを表示したい場合は`DomainParseError`により細かいバリアントを定義して`Display`トレイトや文字列を返す独自のトレイトを利用するのが良いと思います．
```rust:domain_error.rs
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum DomainError {
	/// TryFromの実装時に利用
    #[error("DomainParseError")]
    DomainParseError,

	/// フォーム型の変換時に利用
    #[error("ValidateFormError")]
    ValidateFormError,
}
```
以上のような`TryFrom<String>`を用いて入力をバリデーションするようなInput要素を持つコンポーネントを作っていきます．

## changeイベントごとにバリデーションする方法
### ValidationInputコンポーネント
`change`イベントごとにバリデーションするコンポーネントは以下です．ジェネリックコンポーネントになっています．`T`は文字列から変換する任意のドメイン固有型でありプロパティとして渡すため`PartialEq`が必要です．`E`は`try_into`で返るエラー型で同じく`PartialEq`が必要です．親コンポーネント(フォーム全体)から渡されたコールバックに`change`イベントごとに`Option<T>`を`emit`します(成功:`Some()`, 失敗:`None`)．失敗した際にエラーメッセージ用の`state`に値を`set`します(成功:`None`, 失敗:`Some()`)．

```rust:validation_input_v1.rs
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;

#[derive(Properties, PartialEq, Clone)]
pub struct ValidationInputPropsV1<T, E>
where
    T: TryFrom<String, Error = E> + PartialEq + 'static,
    E: PartialEq,
{
    // 値の更新時に実行するコールバック
    pub onchange: Callback<Option<T>>,
    // バリデーションのエラーメッセージ
    pub error_message: String,
    // 必須のフィールドであるかどうか
    #[prop_or(false)]
    pub required: bool,
}

#[function_component(ValidationInputV1)]
pub fn validation_input_v1<T, E>(
    ValidationInputPropsV1 {
        onchange,
        error_message,
        required,
    }: &ValidationInputPropsV1<T, E>,
) -> Html
where
    T: TryFrom<String, Error = E> + PartialEq + 'static,
    E: PartialEq,
{
    let error_message = error_message.clone();
    let required = *required;
    let error_message_handle: UseStateHandle<Option<String>> =
        use_state(|| Some("必須のフィールドです".to_string()));

    let text_onchange = {
        let parent_onchange = onchange.clone();
        let error_message_handle = error_message_handle.clone();

        let try_into_func = move |s: String| -> Result<T, String> {
            // required
            if required {
                let _required_s: RequiredString = s
                    .clone()
                    .try_into()
                    .map_err(|_| "必須のフィールドです".to_string())?;
            }
            let domain_value: T = s.try_into().map_err(|_| error_message.clone())?;
            Ok(domain_value)
        };

        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 input_text = input_element.value();

            let try_into_res = try_into_func(input_text);
            match try_into_res {
                Ok(domain_value) => { // 成功時
                    error_message_handle.set(None);
                    parent_onchange.emit(Some(domain_value));
                }
                Err(error_message) => { // 失敗時
                    error_message_handle.set(Some(error_message));
                    parent_onchange.emit(None);
                }
            }
        })
    };

    html! {
        <>
        <input type="text" onchange={text_onchange}/>
        if let Some(s) = (*error_message_handle).clone() {
            <span>{s}</span>
        }
        </>
    }
}
```

`change`イベントに結び付けるコールバックのうち，バリデーションを行う関数は以下です．メッセージを取得しやすいように`Result<T, String>`で返しています．`RequiredString`は入力を保証する文字列であり後で説明します．
```rust
let try_into_func = move |s: String| -> Result<T, String> {
	// required
	if required {
		let _required_s: RequiredString = s
			.clone()
			.try_into()
			.map_err(|_| "必須のフィールドです".to_string())?;
	}
	let domain_value: T = s.try_into().map_err(|_| error_message.clone())?;
	Ok(domain_value)
};
```

`RequiredString`は以下のようにシンプルに空白文字列でないことを保証するだけの型です．
```rust:required.rs
/// 何か入力が必要なString
pub struct RequiredString(String);

pub struct RequiredError;

impl TryFrom<String> for RequiredString {
    type Error = RequiredError;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        if value != String::default() {
            Ok(RequiredString(value))
        } else {
            Err(RequiredError)
        }
    }
}
```

コールバックの中身は以下です．`HtmlInputElement`の`value`を先ほどの`try_into_func`関数で`Result<T, String>`に変換しバリデーションが成功・失敗したときの処理を行っています．成功した場合はエラーメッセージに`None`，親のコールバックに`Some`を渡し，失敗した場合はエラーメッセージに`Some`，親のコールバックに`None`を渡します．
```rust
let input_element = e
	.target()
	.expect("Event should have a target when dispatched")
	.dyn_into::<HtmlInputElement>()
	.expect("This EventTarget should cast into HtmlInputElement");

let input_text = input_element.value();

let try_into_res = try_into_func(input_text);
match try_into_res {
	Ok(domain_value) => { // 成功時
		error_message_handle.set(None);
		parent_onchange.emit(Some(domain_value));
	}
	Err(error_message) => { // 失敗時
		error_message_handle.set(Some(error_message));
		parent_onchange.emit(None);
	}
}
```
### Formコンポーネント
親コンポーネントであるFormコンポーネントの説明の前に，バリデーションが成功したことをチェックする部分を説明します．この部分は一つめの方法と二つ目の方法どちらからも利用します．ドメイン固有型をフィールドに持つ`DomainForm`とそれぞれのフィールドが`Option`の`DomainFormOpt`を用意します．`DomainFormOpt`の`default`メソッドは全てのフィールドが`None`となります．
```rust:domain_form.rs
#[derive(Default, Clone)]
pub struct DomainFormOpt {
    pub date: Option<Date>,
    pub postal_code: Option<PostalCode>,
}

#[derive(Debug)]
pub struct DomainForm {
    pub date: Date,
    pub postal_code: PostalCode,
}
```
そして以下の`TryForm<DomainFormOpt>`が成功すれば，`Date`,`PostalCode`のバリデーションが成功した(全てでそれぞれ`Some()`が得られた)ことになります．
```rust:domain_form.rs
impl TryFrom<DomainFormOpt> for DomainForm {
    type Error = DomainError;
    fn try_from(opt: DomainFormOpt) -> Result<Self, Self::Error> {
        Ok(Self {
            date: opt.date.ok_or(DomainError::ValidateFormError)?,
            postal_code: opt.postal_code.ok_or(DomainError::ValidateFormError)?,
        })
    }
}
```

これと`ValidationInputV1`を用いたFormコンポーネントは以下です．実際には送信時にFetch APIなどを利用することになると思いますが，今回はバリデーションが成功して得られた`DomainForm`をログとしてコンソールに表示しています．
```rust:form_v1.rs
use log::info;
use yew::prelude::*;

#[function_component(FormV1)]
pub fn form_v1() -> Html {
    let domain_form_opt_handle = use_mut_ref(DomainFormOpt::default);
    let onclick = {
        let domain_form_opt_handle = domain_form_opt_handle.clone();
        Callback::from(move |_: MouseEvent| {
            let res: Result<DomainForm, _> = domain_form_opt_handle
                .replace_with(|domain_form_opt| domain_form_opt.clone())
                .try_into();
            if let Ok(domain_form) = res {
                // 送信の処理
                info!("form v1:{:?}", domain_form);
            }
        })
    };

    html! {
        <>
        <div>
            <label>
                {"郵便番号を入力"}
                <ValidationInputV1<PostalCode, _>
                    onchange={
                        let domain_form_opt_handle = domain_form_opt_handle.clone();
                        move |opt|{domain_form_opt_handle.borrow_mut().postal_code = opt;}
                    }
                    error_message={"有効な郵便番号ではありません"}
                    required={true}
                />
            </label>
        </div>
        <div>
            <label>
                {"年月日を入力"}
                <ValidationInputV1<Date, _>
                    onchange={
                        let domain_form_opt_handle = domain_form_opt_handle.clone();
                        move |opt|{domain_form_opt_handle.borrow_mut().date = opt;}
                    }
                    error_message={"有効な年月日ではありません"}
                    required={true}
                />
            </label>
        </div>
        <div>
            <button {onclick}>{"送信"}</button>
        </div>
        </>
    }
}

```

## 送信ボタンのclickイベントでバリデーションする方法
### ValidationInputコンポーネント
親コンポーネントの送信ボタンの`click`イベントでバリデーションするコンポーネントは以下です．`yew::prelude::NodeRef`と`gloo_events::EventListener`を用いて，プロパティとして与えられたボタンの`click`イベントにクロージャ―を結び付けいてる点と，`<input type="text"/>`からの`value`の取得にも`NodeRef`を用いている点が異なります．`submit_ref`について，このコンポーネント内でhtml要素に`ref`で渡していないにも関わらず`NodeRef::cast`が成功するのは少し奇妙に思えます．しかし`use_effect_with_deps`で与えたコールバックは最初のレンダリングの後に行われるため(そして`deps`に`()`を渡すことによって一回だけ実行される)，親コンポーネントで適切なhtml要素に`ref`で渡していればこのタイミングでも`NodeRef::cast`が成功します．
```rust:validation_input_v2.rs
use gloo_events::EventListener;
use web_sys::{HtmlElement, HtmlInputElement};
use yew::prelude::*;

#[derive(Properties, PartialEq, Clone)]
pub struct ValidationInputPropsV2<T, E>
where
    T: TryFrom<String, Error = E> + PartialEq + 'static,
    E: PartialEq,
{
    // 送信時に実行するコールバック
    pub onsubmit: Callback<Option<T>>,
    // 送信イベントを結びつけるボタンのNodeRef
    pub submit_ref: NodeRef,
    // バリデーションのエラーメッセージ
    pub error_message: String,
    // 必須のフィールドであるかどうか
    #[prop_or(false)]
    pub required: bool,
}

#[function_component(ValidationInputV2)]
pub fn validation_input_v2<T, E>(
    ValidationInputPropsV2 {
        onsubmit,
        submit_ref,
        error_message,
        required,
    }: &ValidationInputPropsV2<T, E>,
) -> Html
where
    T: TryFrom<String, Error = E> + PartialEq + 'static,
    E: PartialEq,
{
    let error_message = error_message.clone();
    let required = *required;

    let error_message_handle: UseStateHandle<Option<String>> = use_state(|| None);
    let text_ref = use_node_ref();

    use_effect_with_deps(
        {
            let parent_onsubmit = onsubmit.clone();
            let error_message_handle = error_message_handle.clone();
            let text_ref = text_ref.clone();
            let submit_ref = submit_ref.clone();

            let try_into_func = move |s: String| -> Result<T, String> {
                if required {
                    let _required_s: RequiredString = s
                        .clone()
                        .try_into()
                        .map_err(|_| "必須のフィールドです".to_string())?;
                }
                let domain_value: T = s.try_into().map_err(|_| error_message.clone())?;
                Ok(domain_value)
            };

            move |_| {
                let submit_node = submit_ref
                    .cast::<HtmlElement>()
                    .expect("This NodeRef should cast into HtmlElement");

                let listener = EventListener::new(&submit_node, "click", move |_| {
                    let input_text = text_ref
                        .cast::<HtmlInputElement>()
                        .expect("This NodeRef should cast into HtmlInputElement")
                        .value();

                    let try_into_res = try_into_func(input_text);
                    match try_into_res {
                        Ok(domain_value) => {
                            error_message_handle.set(None);
                            parent_onsubmit.emit(Some(domain_value));
                        }
                        Err(error_message) => {
                            error_message_handle.set(Some(error_message));
                            parent_onsubmit.emit(None);
                        }
                    }
                });
                move || drop(listener)
            }
        },
        (), //最初のレンダリング時に実行
    );

    html! {
        <>
        <input type="text" ref={text_ref}/>
        if let Some(s) = (*error_message_handle).clone() {
            <span>{s}</span>
        }
        </>
    }
}
```

### Formコンポーネント
`ValidationInputV2`を用いたFormコンポーネントは以下です．バリデーションの終了チェックと送信の関数が`ValidationInputV2`に渡すコールバック内で実行されているのが異なる点です．しかし重複して送信しないように送信の処理内で`domain_form_opt_handle`を初期化しています．また`ValidationInputV2`に渡す`submit_ref`をボタン要素に`ref`で結びつけています．
```rust
use log::info;
use std::cell::RefCell;
use std::rc::Rc;
use yew::prelude::*;

fn check_and_send(domain_form_opt_handle: Rc<RefCell<DomainFormOpt>>) {
    let res: Result<DomainForm, _> = domain_form_opt_handle
        .replace_with(|opt| opt.clone())
        .try_into();
    if let Ok(domain_form) = res {
        // 送信の処理
        info!("form v2:{:?}", domain_form);
        *domain_form_opt_handle.borrow_mut() = DomainFormOpt::default();
    }
}

#[function_component(FormV2)]
pub fn form_v2() -> Html {
    let submit_ref = use_node_ref();
    let domain_form_opt_handle = use_mut_ref(DomainFormOpt::default);

    html! {
        <>
        <div>
            <label>{"郵便番号を入力"}
                <ValidationInputV2<PostalCode, _>
                    onsubmit={
                        let domain_form_opt_handle = domain_form_opt_handle.clone();
                        move |opt|{
                            domain_form_opt_handle.borrow_mut().postal_code = opt;
                            check_and_send(domain_form_opt_handle.clone());
                        }
                    }
                    submit_ref={submit_ref.clone()}
                    error_message={"有効な郵便番号ではありません"}
                    required={true}
                />
            </label>
        </div>
        <div>
            <label>{"年月日を入力"}
                <ValidationInputV2<Date, _>
                    onsubmit={
                        let domain_form_opt_handle = domain_form_opt_handle.clone();
                        move |opt|{
                            domain_form_opt_handle.borrow_mut().date = opt;
                            check_and_send(domain_form_opt_handle.clone());
                        }
                    }
                    submit_ref={submit_ref.clone()}
                    error_message={"有効な年月日ではありません"}
                    required={true}
                />
            </label>
        </div>
        <div>
            <button ref={submit_ref}>{"送信"}</button>
        </div>
        </>
    }
}
```

実際には上記のような例に当てはまらず複数のinput要素からドメイン固有型を作成する場合や，ドメイン固有型を設けるのが冗長な場合があると思いますが，その場合もrustの強力なエラーハンドリングツールである`Result`は役に立つと思います．