Yewでモーダルウィンドウ

Reactでモーダルウィンドウを作成するREFECTさんの[記事](https://reffect.co.jp/react/react-modal)が分かりやすかったのでそちらを参考に[Yew](https://github.com/yewstack/yew)でモーダルウィンドウを作成します．関数コンポーネントを用いるためreactのものとほとんど同じですが，スクロールの禁止についてはweb_sysのラッパーである[gloo](https://github.com/rustwasm/gloo) を利用しています．コード全体は[こちら](https://github.com/deepgreenAN/yew-simple-modal-dialog)

## モーダル
モーダルの表示・解除部分は以下です．状態の変更ごとに再レンダリングする`use_state`
を用いて，「モーダルを表示」ボタンをクリックした場合にモーダルを開き，「キャンセル」ボタンやモーダルの外側のオーバーレイをクリックした場合にモーダルを閉じます．各要素の位置や重ね順，子コンポーネント(`modal-content`)のイベントの親コンポーネント(`modal-overlay`)への伝播の阻止などは元の記事が分かりやすいです．
`use_state`の返す`UseStateHandle`は新しい値を`set`するごとに再レンダリングします．clickイベントに設定するコールバック内で値を`set`することによってイベントが発火するごとに再レンダリングを行います．`html`マクロ内ではif文を用いて要素を`is_show_modal`に依存させます．
```rust:main.rs
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    // モーダルの表示
    let is_show_modal = use_state(|| false);
    let show_modal = {
        let is_show_modal = is_show_modal.clone();
        Callback::from(move |_e| {
            is_show_modal.set(true);
        })
    };
    let close_modal = {
        let is_show_modal = is_show_modal.clone();
        Callback::from(move |_e| {
            is_show_modal.set(false);
        })
    };

    html! {
        <>
        <button onclick={show_modal}>{"モーダルを表示"}</button>
        if *is_show_modal {
            <div id="modal-overlay" onclick={close_modal.clone()}>
                <div id="modal-content" onclick={|e:MouseEvent|{e.stop_propagation();}}>
                    <p>{"これがモーダルウィンドウです。"}</p>
                    <p><button onclick={close_modal}>{"キャンセル"}</button></p>
                </div>
            </div>
        }
        </>
    }
}
```

```scss:style.scss
#modal-overlay {
    /*　画面全体を覆う設定　*/
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);

    /*　画面の中央に要素を表示させる設定　*/
    display: flex;
    align-items: center;
    justify-content: center;

}

#modal-content {
    z-index: 2;
    width: 50%;
    padding: 1em;
    background: #fff;
}
```

## スクロールの禁止
javascriptで書くと
```js
function handle(event) {
    event.preventDefault();
}
document.addEventListener('wheel', handle, { passive: false });
```
をモーダルの表示時に行い
```js
document.removeEventListener('wheel', handle, { passive: false });
```
をモーダルの解除時に行います．
こちらを`gloo_events::EventListener`を用いて実装します．使い方は似ていますが，rustらしく`EventListener`のdrop時に渡したクロージャ―は登録が解除されメモリから削除されます．そのためコンポーネントの状態として保持する必要があります．今回は`use_memo`を用いて，ある依存(`is_show_modal`)が変更されるごとに再計算されるようにします．

```rust:main.rs
use gloo_events::{EventListener, EventListenerOptions};

// スクロールの禁止
let _lock_scroll = use_memo(
	|is_show_modal| {
		if **is_show_modal {
			let document = gloo_utils::document();
			let options = EventListenerOptions {
				passive: false,
				..Default::default()
			};
			Some(vec![
				EventListener::new_with_options(&document, "wheel", options, move |e| {
					e.prevent_default();
				}),
				EventListener::new_with_options(&document, "touchmove", options, move |e| {
					e.prevent_default();
				}),
			])
		} else {
			None
		}
	},
	is_show_modal.clone(),
);
```

このままだとスクロールが禁止されているかどうか分かりずらいので，以下を追加します．

```rust:main.rs
<div id="main-content">{"メインコンテンツ"}</div>
```
```scss:style.scss
#main-content {
    height: 2000px;
    background-color: #ddd;
}
```
関数コンポーネントとglooを使えばjsと同じように実装できて便利ですね

## 補足

スクロールの禁止の処理は`use_memo`ではなく`use_effect_with_deps`を利用しても実装できます．デストラクタ(クロージャ―)へ`gloo_events::EventListener`をキャプチャしてその中でドロップさせます．デストラクタが明示されていたり，関数内で閉じているためこちらの方が直感的かもしれません．
```rust
// スクロールの禁止
use_effect_with_deps(
	move |is_show_modal| {
		let listeners = if **is_show_modal {
			let document = gloo_utils::document();
			let options = EventListenerOptions {
				passive: false,
				..Default::default()
			};
			Some(vec![
				EventListener::new_with_options(&document, "wheel", options, move |e| {
					e.prevent_default();
				}),
				EventListener::new_with_options(&document, "touchmove", options, move |e| {
					e.prevent_default();
				}),
			])
		} else {
			None
		};

		move || drop(listeners) // デストラクタ
	},
	is_show_modal.clone(),
);
```