LoginSignup
1
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-02-19

この記事は

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

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

Hyperapp

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

Dexie.js

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

v8n

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

Bulma

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

サンプル

概要

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

コード

サンプルコード(割と長いので折り畳み)
サンプルコード
<!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>

実行環境

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

表示時

image.png

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

image.png

一覧選択時

image.png

エラー時(抜粋)

image.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の相性は良いと思う。

1
2
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
2