VanJSでお問い合わせフォームを作ってみた

前回、公式のチュートリアルを触ってみましたが、今回は自分でお問い合わせフォームを作ってみたいと思います。
テキストインプット、ラジオボタン、セレクト、チェックボックス、テキストエリアの主要な入力フォームで作ってみます。

https://qiita.com/mori_goq/items/364d63c861c796b6ac4b

## 完成イメージ

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1125523/22ab3793-1c46-9891-3b47-f5793ac8577c.png)


## 作ってみる

### 環境準備

```tree
├── index.html
└── main.js
└── van-1.1.0.nomodule.min.js
```

今回は公式ドキュメントから`.min.js`ファイルをダウンロードして、ローカルでVanJSを読み込みます。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

</body>
    <script type="text/javascript" src="van-1.1.0.nomodule.min.js"></script>
    <script src="main.js"></script>
</html>
```

### お問い合わせフォームを作る

```javascript
const {
  table,
  tbody,
  tr,
  th,
  td,
  form,
  div,
  input,
  select,
  option,
  label,
  textarea,
  button,
} = van.tags

const Form = () => {
  const name = van.state('')
  const sex = van.state('1')
  const prefecture = van.state('1')
  const mediaList = van.state([])
  const message = van.state('')

  const onsubmit = (e) => {
    e.preventDefault()

    console.log('送信', {
      name: name.val,
      sex: sex.val,
      prefecture: prefecture.val,
      mediaList: mediaList.val,
      message: message.val,
    })
  }

  return form(
    { onsubmit },
    table(
      tbody(
        tr(
          th('お名前'),
          td(
            // テキストインプット
            input({
              type: 'text',
              value: name,
              oninput: (e) => (name.val = e.target.value),
            }),
          ),
        ),
        tr(
          th('性別'),
          td(
            // ラジオボタン
            [
              { value: '1', label: '男' },
              { value: '2', label: '女' },
            ].map((item) =>
              div(
                input({
                  id: `sex${item.value}`,
                  type: 'radio',
                  value: item.value,
                  checked: () => item.value === sex.val,
                  onchange: (e) => (sex.val = e.target.value),
                }),
                label({ for: `sex${item.value}` }, item.label),
              ),
            ),
          ),
        ),
        tr(
          th('都道府県'),
          td(
            // セレクトボックス
            select(
              {
                value: prefecture,
                onchange: (e) => (prefecture.val = e.target.value),
              },
              // セレクトボックスのオプション
              [
                { value: '1', label: '北海道' },
                { value: '2', label: '青森県' },
                { value: '3', label: '岩手県' },
              ].map((item) => option({ value: item.value }, item.label)),
            ),
          ),
        ),
        tr(
          th('当社を何で知りましたか？'),
          td(
            // チェックボックス
            [
              { value: '1', label: 'テレビ' },
              { value: '2', label: '新聞' },
              { value: '3', label: 'SNS' },
            ].map((item) =>
              div(
                input({
                  id: `media${item.value}`,
                  type: 'checkbox',
                  value: item.value,
                  checked: () => mediaList.val.includes(item.value),
                  onchange: (e) => {
                    const { value, checked } = e.target

                    mediaList.val = checked
                      ? [...mediaList.val, value]
                      : mediaList.val.filter((media) => media !== value)
                  },
                }),
                label({ for: `media${item.value}` }, item.label),
              ),
            ),
          ),
        ),
        tr(
          th('お問い合わせ内容'),
          td(
            // テキストエリア
            textarea({
              rows: 10,
              value: message,
              oninput: (e) => (message.val = e.target.value),
            }),
          ),
        ),
      ),
    ),
    button({ type: 'submit' }, '送信'),
  )
}

van.add(document.body, Form())
```

## 注意点

`van.state()`で宣言した変数は、関数で書かないと動的に変わりません。例えばラジオボタンの`checked`は`checked: item.value === sex.val`だと、`sex.val`の値が動的に変わらないため、`checked: () => item.value === sex.val`のように記述する必要があります。

```javascript
// ラジオボタン
[
  { value: '1', label: '男' },
  { value: '2', label: '女' },
].map((item) =>
  div(
    input({
      id: `sex${item.value}`,
      type: 'radio',
      value: item.value,
      // ❗関数で書く必要がある
      checked: () => item.value === sex.val,
      onchange: (e) => (sex.val = e.target.value),
    }),
    label({ for: `sex${item.value}` }, item.label),
  ),
),
```

ただし、`value: name`のように`.val`を書いてなければ関数で書く必要はありません。

```javascript
// テキストインプット
input({
  type: 'text',
  // ✅これは関数で書かなくてもOK
  value: name,
  oninput: (e) => (name.val = e.target.value),
}),
```

逆に、`value: name.val`だと、値が動的に変わらないため、例えば送信後にテキストの値を空にしようと、`name.val = ''`と書いても`input`の値は空になりません。

```javascript
// テキストインプット
input({
  type: 'text',
  // ❌これだと動的に値が変わらない
  value: name.val,
  oninput: (e) => (name.val = e.target.value),
}),
```

## まとめ

VanJSでお問い合わせフォームができました。感想ですが、JSXの感覚で書けるのでとても書きやすいです。Reactに慣れている方であれば学習コストも低いと思うのでおすすめです。

## 最後に

### GoQSystemでは一緒に働いてくれる仲間を募集中です！

ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site?qiita=14ed49b89cd648d5b756