この記事は
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>
実行環境
実行時のスクリーンショット
表示時
ユーザー情報は、IndexedDBのuser_databaseのusersへ保存
一覧選択時
エラー時(抜粋)
フレームワークとか使ってみた感想
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の相性は良いと思う。