この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの6日目です。
Structiveについて詳しくはこちらより
前回のおさらい
5日目では、Proxyと構造パスを使った仮想DOMなしのリアクティビティを学びました。今日は、Week 1の締めくくりとして、実際に手を動かしてカウンターアプリを作ります。
今日作るもの
シンプルなカウンターアプリを、段階的に機能を追加しながら作っていきます:
- 基本のカウンター - 増減ボタン
- ステップ機能 - 増減の幅を変更
- リセット機能 - 初期値に戻す
- 履歴表示 - 変更履歴を記録
セットアップ
まず、基本的なHTMLファイルを用意します:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Counter App</title>
<script type="importmap">
{
"imports": {
"@components/app-main": "./counter.st.html"
}
}
</script>
<script type="module" src="path/to/framework.js"></script>
</head>
<body>
<app-main></app-main>
</body>
</html>
Step 1: 基本のカウンター
最もシンプルなカウンターから始めましょう。
counter.st.html
<template>
<div class="counter">
<h1>カウンター</h1>
<div class="display">
<span class="count">{{ count }}</span>
</div>
<div class="buttons">
<button data-bind="onclick:decrement">-</button>
<button data-bind="onclick:increment">+</button>
</div>
</div>
</template>
<style>
.counter {
max-width: 400px;
margin: 2em auto;
padding: 2em;
border: 2px solid #333;
border-radius: 8px;
text-align: center;
}
.display {
margin: 2em 0;
}
.count {
font-size: 3em;
font-weight: bold;
color: #007bff;
}
.buttons button {
font-size: 1.5em;
padding: 0.5em 1.5em;
margin: 0 0.5em;
cursor: pointer;
}
</style>
<script type="module">
export default class {
count = 0;
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
}
</script>
コードの解説
状態の定義:
count = 0;
- カウンターの初期値は0
- クラスのプロパティとして宣言するだけ
UI(テンプレート):
<span class="count">{{ count }}</span>
-
{{ count }}で状態の値を表示 - 状態が変わると自動的に更新される
イベントハンドラ:
<button data-bind="onclick:increment">+</button>
-
data-bind="onclick:increment"でクリックイベントをバインド -
incrementメソッドが呼ばれる
メソッド:
increment() {
this.count += 1;
}
- 状態を直接変更するだけ
- Proxyが変更を検知してUIを更新
動作確認
このコードで以下が実現されています:
- ✅
+ボタンでカウントが増える - ✅
-ボタンでカウントが減る - ✅ 数値がリアルタイムで表示される
Step 2: ステップ機能の追加
増減の幅を変更できるようにします。
counter.st.html(更新版)
<template>
<div class="counter">
<h1>カウンター</h1>
<!-- ステップの設定 -->
<div class="step-control">
<label>
ステップ:
<input
type="number"
data-bind="valueAsNumber:step"
min="1"
>
</label>
</div>
<div class="display">
<span class="count">{{ count }}</span>
</div>
<div class="buttons">
<button data-bind="onclick:decrement">- {{ step }}</button>
<button data-bind="onclick:increment">+ {{ step }}</button>
</div>
</div>
</template>
<style>
/* 既存のスタイル + 追加 */
.step-control {
margin-bottom: 1em;
}
.step-control input {
width: 60px;
padding: 0.3em;
font-size: 1em;
}
</style>
<script type="module">
export default class {
count = 0;
step = 1; // 追加
increment() {
this.count += this.step; // stepを使う
}
decrement() {
this.count -= this.step; // stepを使う
}
}
</script>
追加された機能
新しい状態:
step = 1;
- 増減の幅を管理
双方向バインディング:
<input type="number" data-bind="valueAsNumber:step" min="1">
-
valueAsNumberで数値として自動変換 - ユーザーが入力すると
stepが更新される -
stepが変わるとinputの値も更新される
動的なボタンラベル:
<button data-bind="onclick:increment">+ {{ step }}</button>
- ボタンに現在のステップ値を表示
-
stepが変わるとボタンのラベルも自動更新
Step 3: リセット機能の追加
カウントを初期値に戻す機能を追加します。
counter.st.html(更新版)
<template>
<div class="counter">
<h1>カウンター</h1>
<div class="step-control">
<label>
ステップ:
<input
type="number"
data-bind="valueAsNumber:step"
min="1"
>
</label>
</div>
<div class="display">
<span class="count">{{ count }}</span>
</div>
<div class="buttons">
<button data-bind="onclick:decrement">- {{ step }}</button>
<button data-bind="onclick:reset">リセット</button>
<button data-bind="onclick:increment">+ {{ step }}</button>
</div>
</div>
</template>
<script type="module">
const INITIAL_COUNT = 0;
export default class {
count = INITIAL_COUNT;
step = 1;
increment() {
this.count += this.step;
}
decrement() {
this.count -= this.step;
}
reset() {
this.count = INITIAL_COUNT;
}
}
</script>
ポイント
定数の活用:
const INITIAL_COUNT = 0;
- 初期値を定数化
- リセット時に同じ値に戻せる
リセットメソッド:
reset() {
this.count = INITIAL_COUNT;
}
- 状態を初期値に戻すだけ
- UIは自動的に更新される
Step 4: 履歴表示の追加
変更履歴を記録して表示します。
counter.st.html(完成版)
<template>
<div class="counter">
<h1>カウンター</h1>
<div class="step-control">
<label>
ステップ:
<input
type="number"
data-bind="valueAsNumber:step"
min="1"
>
</label>
</div>
<div class="display">
<span class="count">{{ count }}</span>
</div>
<div class="buttons">
<button data-bind="onclick:decrement">- {{ step }}</button>
<button data-bind="onclick:reset">リセット</button>
<button data-bind="onclick:increment">+ {{ step }}</button>
</div>
<!-- 履歴表示 -->
<div class="history">
<h2>履歴</h2>
<ul>
{{ for:history }}
<li>{{ history.*.message }}</li>
{{ endfor: }}
</ul>
<button data-bind="onclick:clearHistory">履歴をクリア</button>
</div>
</div>
</template>
<style>
/* 既存のスタイル + 追加 */
.history {
margin-top: 2em;
padding-top: 2em;
border-top: 1px solid #ccc;
}
.history ul {
list-style: none;
padding: 0;
max-height: 200px;
overflow-y: auto;
text-align: left;
}
.history li {
padding: 0.5em;
border-bottom: 1px solid #eee;
}
</style>
<script type="module">
const INITIAL_COUNT = 0;
export default class {
count = INITIAL_COUNT;
step = 1;
history = [];
increment() {
this.count += this.step;
this.addHistory(`+${this.step} → ${this.count}`);
}
decrement() {
this.count -= this.step;
this.addHistory(`-${this.step} → ${this.count}`);
}
reset() {
this.count = INITIAL_COUNT;
this.addHistory(`リセット → ${this.count}`);
}
addHistory(message) {
const timestamp = new Date().toLocaleTimeString();
this.history = this.history.concat({
message: `[${timestamp}] ${message}`
});
}
clearHistory() {
this.history = [];
}
}
</script>
新機能の解説
履歴の状態:
history = [];
- 配列で履歴を管理
履歴への追加:
addHistory(message) {
const timestamp = new Date().toLocaleTimeString();
this.history = this.history.concat({
message: `[${timestamp}] ${message}`
});
}
-
concatで新しい配列を作成(重要!) - Day 5で学んだ通り、配列全体を置き換えることでProxyが検知
forループでの表示:
{{ for:history }}
<li>{{ history.*.message }}</li>
{{ endfor: }}
-
history配列の各要素を表示 - ワイルドカード
*で各アイテムにアクセス
各メソッドから呼び出し:
increment() {
this.count += this.step;
this.addHistory(`+${this.step} → ${this.count}`); // 履歴を記録
}
完成したアプリの動作
これで以下の機能を持つカウンターアプリが完成しました:
- ✅ 基本機能: カウントの増減
- ✅ ステップ調整: 増減幅の変更
- ✅ リセット: 初期値に戻す
- ✅ 履歴表示: 操作の記録
- ✅ 履歴クリア: 履歴の削除
コードの特徴
このアプリを作るのに必要だったのは:
状態(3つのプロパティ):
count = INITIAL_COUNT;
step = 1;
history = [];
メソッド(5つ):
increment()
decrement()
reset()
addHistory()
clearHistory()
ボイラープレートはゼロ:
- useState不要
- setter関数不要
- useEffect不要
- イベントハンドラのラッパー関数不要
構造パスの利点の実感
このアプリを作って、以下を実感できたはずです:
1. シンプルなコード
// これだけでUIが更新される
this.count += this.step;
2. 明確な依存関係
<!-- このUIは count に依存していることが一目瞭然 -->
<span class="count">{{ count }}</span>
3. 双方向バインディングの自然さ
<!-- 自動的に双方向にバインドされる -->
<input type="number" data-bind="valueAsNumber:step">
4. 配列の扱いやすさ
// concat で新しい配列を作るだけ
this.history = this.history.concat(newItem);
まとめ
今日は、実際に手を動かしてカウンターアプリを作りました:
学んだこと:
- SFCの基本構造(template、style、script)
- 状態とUIのバインディング
- イベントハンドラの定義
- 双方向バインディング
- 配列の扱い方(concat)
- forループでのリスト表示
構造パスの実践的な利点:
- コード量が少ない
- 理解しやすい
- メンテナンスしやすい
- 機能追加が簡単
次回予告:
明日は、今週のまとめをやります。
次回: Day 7「今週のまとめ」
カウンターアプリを作ってみて、感想や質問があればコメントでぜひ!