軽量とかシンプルとか言われてるHyperapp(v2), Dexie.js, v8n, Bulmaを取り合えず使ってみた

# この記事は

Web系の軽量とかシンプルとか言われてるフレームワークやライブラリを取り合えず使ってみただけ
初学者なので、使い方の間違いがあるかも

# 使うフレームワークとかの簡単な説明

## Hyperapp

https://github.com/jorgebucaran/hyperapp

JavaScriptフレームワーク
オープンソースライセンスは、MIT License
軽量らしい（ファビコンよりも）

## Dexie.js

https://dexie.org/

IndexedDBのラッパー
オープンソースライセンスは、Apache License 2.0
簡単で速いらしい

## v8n

https://imbrn.github.io/v8n/

バリデーションライブラリ
オープンソースライセンスは、MIT License
シンプルらしい

## Bulma 

https://bulma.io/

CSSフレームワーク
オープンソースライセンスは、MIT License
JavaScriptなしで使えるらしい

# サンプル

## 概要

ユーザー情報（名前、年齢）を管理するページ

## コード

<details><summary>サンプルコード（割と長いので折り畳み）</summary><div>

```html:サンプルコード
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <title>Hyperapp(v2), Dexie.js, v8n, Bulma を使用したサンプル</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bulma v0.9.3 | MIT License | https://unpkg.com/bulma/LICENSE -->
  <link rel="stylesheet" href="https://unpkg.com/bulma@0.9.3/css/bulma.min.css">
  <!-- v8n v1.3.3 | MIT License | https://unpkg.com/v8n@1.3.3/LICENSE -->
  <script src="https://unpkg.com/v8n@1.3.3/dist/v8n.min.js"></script>
  <!-- Dexie.js v3.0.3 | Apache License 2.0 | https://unpkg.com/dexie@3.0.3/NOTICE -->
  <script src="https://unpkg.com/dexie@3.0.3/dist/dexie.min.js"></script>
  <script type="module">
    /* Hyperapp v2.0.19 | MIT License | https://unpkg.com/hyperapp@2.0.19/LICENSE.md */
    import { h, memo, text, app } from "https://unpkg.com/hyperapp@2.0.19";
    /* @hyperapp/time v1.0.0 | MIT License | https://unpkg.com/hyperapp@2.0.19/LICENSE.md */
    import { every } from "https://unpkg.com/@hyperapp/time@1.0.0/index.js"

    const db = new Dexie("user_database");

    db.version(1).stores({
      users: '++id,name,age',
    });

    /**
     * ディープコピー
     **/
    const deepCopy = (obj) => JSON.parse(JSON.stringify(obj));

    /**
     * 即座に実行(effect constructor)
     **/
    const immediately = (effectRunner, action, props) => {
      return [effectRunner, { action, props }];
    };

    /**
     * 初期ステート
     **/
    const initialState = {
      id: 0,
      beforeName: "",
      name: "",
      isCorrectName: false,
      nameIsError: false,
      nameRemarks: "",
      beforeAge: "",
      age: "",
      isCorrectAge: false,
      ageIsError: false,
      ageRemarks: "",
      users: [
        {
          id: 0,
          name: "",
          age: "",
          isSelected: false,
        },
      ],
    };

    /**
     * 一覧リフレッシュ
     **/
    const refreshUsers = (state) => [
      state,
      immediately(loadUsersRunner)
    ];

    /**
     * 入力項目クリア
     **/
    const clearInput = (state) => [
      {
        ...state,
        id: 0,
        beforeName: "",
        name: "",
        isCorrectName: false,
        nameIsError: false,
        nameRemarks: "",
        beforeAge: "",
        age: "",
        isCorrectAge: false,
        ageIsError: false,
        ageRemarks: "",
      },
      immediately(loadUsersRunner)
    ];

    /**
     * Users読込(effect runner)
     **/
    const loadUsersRunner = (dispatch) => {
      // DBのUsersを配列で全件取得
      db.users.toArray(users => {
        // ソート
        users.sort((a, b) => {
          // 年齢の昇順
          if (Number(a.age) > Number(b.age)) {
            return 1;
          } else if (Number(a.age) < Number(b.age)) {
            return -1;
          }

          // 年齢が同じ場合、名前の昇順
          if (a.name > b.name) {
            return 1;
          } else if (a.name < b.name) {
            return -1;
          }

          return 0;
        });

        // users.isSelectedを追加（DBのUsersには存在しない）
        for (const user of users) {
          user.isSelected = false;
        }

        dispatch(setUsersList, users);
      });
    };

    /**
     * ユーザー一覧セット
     **/
    const setUsersList = (state, props) => [{
      ...state,
      users: props,
    }];

    /**
     * ユーザー一覧選択
     **/
    const selectUsersList = (state, props) => [
      {
        ...state,
        id: props.id,
        beforeName: props.name,
        name: props.name,
        isCorrectName: true,
        nameIsError: false,
        nameRemarks: "",
        beforeAge: props.age,
        age: props.age,
        isCorrectAge: true,
        ageIsError: false,
        ageRemarks: "",
      },
      immediately(selectUsersRunner, setUsersList, { users: deepCopy(state.users), id: props.id })
    ];

    /**
     * Users選択(effect runner)
     **/
    const selectUsersRunner = (dispatch, { action, props }) => {
      // users.isSelectedの設定
      for (const user of props.users) {
        user.isSelected = user.id == props.id;
      }

      dispatch(action, props.users);
    };

    /**
     * 名前セット
     **/
    const setName = (state, event) => [{
      ...state,
      name: event.target.value,
      isCorrectName: v8n().string().minLength(1).test(event.target.value.trim()),
      nameIsError: false,
      nameRemarks: "",
    }];

    /**
     * 年齢セット
     **/
    const setAge = (state, event) => [{
      ...state,
      age: event.target.value,
      isCorrectAge: v8n().numeric().greaterThanOrEqual(0).lessThanOrEqual(199).test(event.target.value),
      ageIsError: false,
      ageRemarks: "",
    }];

    /**
     * 登録
     **/
    const rgistration = (state) => [
      {
        ...state,
        nameRemarks: state.isCorrectName ? "" : "名前が正しくありません。",
        nameIsError: !state.isCorrectName,
        ageRemarks: state.isCorrectAge ? "" : "年齢が正しくありません。",
        ageIsError: !state.isCorrectAge,
      },
      state.isCorrectName && state.isCorrectAge && (
        state.id == 0 ?
          immediately(addUsersRunner, clearInput, { name: state.name, age: state.age }) :
          immediately(updateUsersRunner, clearInput, { id: state.id, name: state.name, age: state.age, beforeName: state.beforeName, beforeAge: state.beforeAge }))
    ];

    /**
     * 削除
     **/
    const deletion = (state, action) => [
      state,
      immediately(deleteUsersRunner, action, { id: state.id, name: state.name, age: state.age, beforeName: state.beforeName, beforeAge: state.beforeAge }),
    ];
  
    /**
     * データベース削除
     **/
    const dbDeletion = (state) => [
      {
        ...state,
        id: 0,
        beforeName: "",
        name: "",
        isCorrectName: false,
        nameIsError: false,
        nameRemarks: "",
        beforeAge: "",
        age: "",
        isCorrectAge: false,
        ageIsError: false,
        ageRemarks: "",
        users: [],
      },
      immediately(deleteDbRunner)
    ];

    /**
     * User追加(effect runner)
     **/
    const addUsersRunner = (dispatch, { action, props }) => {
      const name = props.name.trim();
      const age = Number(props.age);

      // DBに追加
      db.users.put({ name: name, age: age }).then((result) => {
        alert(name + "(" + age + ")が登録されました。");

        dispatch(action, props);
      });
    };

    /**
     * Users更新(effect runner)
     **/
    const updateUsersRunner = (dispatch, { action, props }) => {
      const name = props.name.trim();
      const age = Number(props.age);

      // DBを更新
      db.users.put({ id: props.id, name: name, age: age }).then((result) => {
        alert(props.beforeName + "(" + props.beforeAge + ")から" + props.name + "(" + age + ")へ更新されました。");

        dispatch(action, props);
      });
    };

    /**
     * Users削除(effect runner)
     **/
    const deleteUsersRunner = (dispatch, { action, props }) => {
      // DBから削除
      db.users.delete(props.id).then((result) => {
        alert(props.beforeName + "(" + props.beforeAge + ")は削除されました。");

        dispatch(action, props);
      });
    };

    /**
     * DB削除(effect runner)
     **/
    const deleteDbRunner = (dispatch) => {
      // DBから削除
      db.delete().then(() => {
        alert("データベースは削除されました。\nこのページを閉じてください。");
      })
    };

    /**
     * ログを書く
     **/
    const logging = (baseDispatch) => {
      return (action, props) => {
        let functionName;

        // ファンクション名を検索
        const searchFunction = (x0) => {
          if (Array.isArray(x0)) {
            x0.forEach(x1 => {
              searchFunction(x1)
            });
          } else if (typeof x0 === 'function') {
            functionName = x0.name;
          }
        }
        searchFunction(action);

        console.log(functionName, { action, props });

        baseDispatch(action, props);
      }
    };

    /**
     * 登録ボタンビュー
     **/
    const rgistrationButtonView = (props) => {
      return h("button", { class: "button is-primary", onclick: rgistration }, props.id == 0 ? text("登録") : text("更新"))
    }

    app({
      init: [
        initialState,
        immediately(loadUsersRunner),
      ],

      view: (state) => h("div", {}, [
        h("section", { class: "hero is-link" }, [
          h("div", { class: "hero-body" }, [
            h("div", { class: "container" }, [
              h("h1", { class: "title" }, text("ユーザー情報")),
              h("p", { class: "subtitle" }, text("Hyperapp(v2), Dexie.js, v8n, Bulma を使用したサンプル"))
            ])
          ])
        ]),
        h("section", { class: "section has-background-primary" }, [
          h("div", { class: "container" }, [
            h("div", { class: "content box" }, [
              h("div", { class: "field" }, [
                h("label", { class: "label" }, text("一覧（年齢順）")),
                h("ul", {}, state.users.map(user =>
                  h("a", {}, [
                    h("li", { class: { "has-text-weight-bold": user.isSelected }, onclick: [selectUsersList, user] }, text(user.name + "(" + user.age + ")")),
                  ]),
                )),
              ]),
              h("div", { class: "field" }, [
                h("label", { class: "label" }, text("名前")),
                h("span", { class: "help is-danger" }, text(state.nameRemarks)),
                h("input", { class: ["input", { "is-danger": state.nameIsError }], type: "text", oninput: setName, value: state.name }),
                h("p", { class: "help is-info" }, text("必須")),
              ]),
              h("div", { class: "field" }, [
                h("label", { class: "label" }, text("年齢")),
                h("span", { class: "help is-danger" }, text(state.ageRemarks)),
                h("input", { class: ["input", { "is-danger": state.ageIsError }], type: "text", oninput: setAge, value: state.age }),
                h("p", { class: "help is-info" }, text("必須（0～199）")),
              ]),
              h("div", { class: "field is-grouped" }, [
                h("div", { class: "control" }, [
                  memo(rgistrationButtonView, { id: state.id }),
                ]),
                h("p", { class: "control" }, [
                  state.id != 0 && h("button", { class: "button is-danger", onclick: [deletion, clearInput] }, text("削除")),
                ]),
                h("p", { class: "control" }, [
                  h("button", { class: "button", onclick: clearInput }, text("クリア")),
                ]),
              ]),
            ]),
          ]),
        ]),
        h("footer", { class: "footer" }, [
          h("div", { class: "container" }, [
            h("button", { class: "button is-danger is-outlined is-small", onclick: dbDeletion }, text("データベース削除（終了）")),
          ])
        ]),
      ]),

      // // 有効にすれば1秒毎に一覧が自動更新
      // subscriptions: (state) => [
      //   every(1000, refreshUsers) 
      // ],

      // // 有効にすればdispatchが呼ばれる度、ログが書かれる
      // dispatch: logging,

      node: document.getElementById("app"),
    });
  </script>
</head>

<body>
  <main id="app"></main>
</body>

</html>
```
</div></details>

## 実行環境

https://wonderful-mud-000567400.azurestaticapps.net/

## 実行時のスクリーンショット

### 表示時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/e771a71d-acb7-d1c4-0332-2c41dd438752.png)

ユーザー情報は、IndexedDBのuser_databaseのusersへ保存

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/4a7218f5-15c1-db17-f909-09c62c5ab73c.png)

### 一覧選択時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/e7e3b7ec-3abd-05ee-093e-3594012b9f67.png)

### エラー時（抜粋）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/3893f70e-12e5-b6da-83ef-421702f47ac5.png)

# フレームワークとか使ってみた感想

## Hyperapp

取り合えず日本語の資料が少ないし、あっても古いことが多い。
（middlewareプロパティが、dispatchに名前が変わっていた時は唖然とした）
学習する人は、チュートリアルをやってAPIリファレンスを読むのオススメ

Tutorial
https://github.com/jorgebucaran/hyperapp/blob/main/docs/tutorial.md

API Rreference
https://github.com/jorgebucaran/hyperapp/blob/main/docs/reference.md

## Dexie.js

IndexedDBを使いたい人は、取り合えず使ってみると良いと思う。
使い方は簡単だけど、hyperappと相性は良くも悪くもなかった。

## v8n

チェック結果をbooleanで返してくれれば十分！みたいな人にオススメ
hyperappの相性は良いと思う。

## Bulma 

僕みたいなデザインセンスの欠片も無い人が、それっぽいデザインにするのには良さそう。
hyperappの相性は良いと思う。