JavaScript超入門⑧ カウンターアプリを完成させよう
HTML、CSS、変数、関数、if文、DOM、クリックイベントを組み合わせ、数字を増減できるWebアプリを作ります。
ここまで学んだ変数、if、関数、DOM、イベントを、1つの小さなアプリへまとめましょう。今回は「+1」「-1」「リセット」で数字を動かすカウンターです。
数字が変わるだけでも、自分のボタンに反応するとちょっと楽しいですよね。ただ、見た目が動いたかだけでなく、データの変更と画面への反映は別だと分かることを目標にします。
1. 先にアプリのルールを決める
基本版は次の仕様です。
- 最初の数字は0
- +1で1増やし、-1で1減らす
- リセットで数字を0へ戻す
- 負の数は赤で表示する
- 再読み込みすると0へ戻る
仕様は、どの操作でどう動くかという約束です。今回、保存機能は作らないので、再読み込みで戻るのは不具合ではありません。
ブラウザと、HTMLを保存できるエディターがあれば試せます。外部ライブラリやアカウント登録は必要ありません。
2. まず、1ファイルの基本版を動かす
次をcounter.htmlとしてUTF-8で保存し、ブラウザで開きます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>カウンター</title>
<style>
body {
font-family: sans-serif;
text-align: center;
margin: 0;
padding: 32px 16px;
}
#value {
font-size: 48px;
margin: 16px 0;
}
button {
min-height: 44px;
padding: 10px 18px;
margin: 4px;
cursor: pointer;
}
button:focus-visible {
outline: 3px solid #17694a;
outline-offset: 3px;
}
</style>
</head>
<body>
<h1>カウンター</h1>
<p id="value" aria-live="polite">0</p>
<p id="status">0です</p>
<button id="decrease" type="button">-1</button>
<button id="reset" type="button">リセット</button>
<button id="increase" type="button">+1</button>
<script>
let count = 0;
const value = document.querySelector("#value");
const status = document.querySelector("#status");
function updateDisplay() {
value.textContent = String(count);
if (count < 0) {
value.style.color = "#b42318";
status.textContent = "0より小さい数です";
} else {
value.style.color = "#171b18";
status.textContent = count === 0 ? "0です" : "0より大きい数です";
}
}
document.querySelector("#increase").addEventListener("click", () => {
count++;
updateDisplay();
});
document.querySelector("#decrease").addEventListener("click", () => {
count--;
updateDisplay();
});
document.querySelector("#reset").addEventListener("click", () => {
count = 0;
updateDisplay();
});
updateDisplay();
</script>
</body>
</html>
+1を2回、-1を1回押すと1になります。リセットで0です。次に-1を押すと-1になり、色と説明文が変わります。
3. HTML・CSS・JavaScriptを切り分ける
| 部分 | 担当していること |
|---|---|
| HTMLのpやbutton | 数字や操作の置き場所 |
| styleの中 | 大きさ、余白、フォーカスの見た目 |
| scriptの中 | 数を覚える、操作で変える、表示する |
フォーカスは、キーボードで今操作する対象が選ばれている状態です。Tabキーでボタンを移動すると枠が出るようにしています。
値の変化は色だけでなく数字と文章でも伝えています。aria-liveは、更新を支援技術へ知らせるための指定です。色が分かりにくい場合や読み上げでも、情報を追いやすくします。
4. countがデータ、textContentが画面
let count = 0;
countは現在の数を覚える変数です。クリックで再代入するためletを使います。これをアプリの状態と呼びます。
count++と書いても、HTMLのpが自動で更新されるわけではありません。updateDisplay()で現在の状態をページへ反映します。
| 操作 | countの変化 | 表示更新後 |
|---|---|---|
| 読み込み | 0 | 0 |
| +1 | 0 → 1 | 1 |
| +1 | 1 → 2 | 2 |
| -1 | 2 → 1 | 1 |
| リセット | 1 → 0 | 0 |
「データは増えているのに画面が変わらない」なら、更新関数を呼び忘れていないかを見ます。逆にtextContentだけを変えると、countと表示が食い違います。
5. 表示を関数へ分ける理由
updateDisplayは、数と色と説明をまとめて更新します。
3つのボタンに同じ表示処理をコピーするより、1か所にまとめた方が変更漏れを減らせます。例えば色を変えたいときも、この関数だけを見れば済みます。
String(count)は数値を文字列へ変える操作です。textContentは数を代入しても文字列へ変換しますが、今回の例では変換を明示しています。HTMLとして解釈しない文字の表示なので、innerHTMLは使いません。MDNのtextContentで挙動を確認できます。
?と:は短い条件分岐
count === 0 ? "0です" : "0より大きい数です"は条件演算子です。「条件がtrueなら左の値、falseなら右の値」を選びます。
難しければ、普通のif / elseで書いても同じです。新しい書き方を全部使う必要はありません。
6. ボタンには、後で呼ぶ関数を登録する
addEventListener("click", () => { ... })は、クリックされたときの処理の登録です。登録した時点でcountを増やすわけではありません。
クリックしたら「状態を変える → 表示を更新する」の順で動きます。詳しい流れは前回のDOMとイベントでも確認できます。
最初に最後のupdateDisplay();を呼んでいるのは、初期表示も同じルールで作るためです。後で初期値を5へ変えた場合にも、HTMLの0を書き直し忘れてずれるのを防ぎます。
7. 改造の練習:±5・上下限・操作回数を付ける
次の4つを加えてください。
- +5と-5のボタン
- 10以上になったら緑色
- 最小値-10、最大値10
- ボタンを押した回数を別に表示
今回は上下限へ達していても、ボタンを押せば操作回数は1増えます。リセットも1回に数え、数字だけを0へ戻します。先に決めておくと、あとで「回数が変では?」と迷わずに済みます。
模範解答
基本版のheadは残し、body全体を次へ置き換えます。元のscriptを残すとイベントが重複するため、bodyは追加ではなく置き換えです。
<body>
<h1>カウンター</h1>
<p id="value" aria-live="polite">0</p>
<p id="status">0です</p>
<p>範囲: -10から10</p>
<p>操作回数: <span id="clickCount">0</span>回</p>
<button data-change="-5" type="button">-5</button>
<button data-change="-1" type="button">-1</button>
<button id="reset" type="button">リセット</button>
<button data-change="1" type="button">+1</button>
<button data-change="5" type="button">+5</button>
<script>
const MIN = -10;
const MAX = 10;
let count = 0;
let clicks = 0;
const value = document.querySelector("#value");
const status = document.querySelector("#status");
const clickCount = document.querySelector("#clickCount");
function updateDisplay() {
value.textContent = String(count);
clickCount.textContent = String(clicks);
if (count >= 10) {
value.style.color = "#17694a";
status.textContent = "上限の10です";
} else if (count < 0) {
value.style.color = "#b42318";
status.textContent = "0より小さい数です";
} else {
value.style.color = "#171b18";
status.textContent = count === 0 ? "0です" : "0より大きい数です";
}
}
function applyChange(change) {
const next = count + change;
count = Math.max(MIN, Math.min(MAX, next));
clicks++;
updateDisplay();
}
for (const button of document.querySelectorAll("[data-change]")) {
button.addEventListener("click", () => {
const change = Number(button.dataset.change);
applyChange(change);
});
}
document.querySelector("#reset").addEventListener("click", () => {
count = 0;
clicks++;
updateDisplay();
});
updateDisplay();
</script>
</body>
8. 改良版を、追加した仕組みごとに説明する
data-changeは、ボタンに付けた増減量
data-change="5"は、HTMLへ独自のデータを置くdata-*属性です。
JavaScriptではbutton.dataset.changeで読みます。ここで得るのは文字列の"5"なので、Numberで数値5へ変えています。
今回の値は自分で固定した-5、-1、1、5だけです。利用者が自由に入れる入力として使う場合は、Number.isFiniteや範囲の確認も追加します。
querySelectorAllは、全部の対象を選ぶ
querySelectorは最初の1要素、querySelectorAllは一致する要素の一覧を返します。
"[data-change]"はその属性がある要素を選ぶ指定です。増減の4ボタンをfor…ofで回し、同じ処理を登録します。リセットは増減ではないので、別に登録しています。
Math.minとMath.maxで範囲へ収める
Math.min(MAX, next)はMAXとnextの小さい方、Math.max(MIN, ...)はMINと結果の大きい方を選びます。
例えばcountが8で+5なら、nextは13です。
Math.min(10, 13)で10になるMath.max(-10, 10)で10のまま- countを10にして表示する
countが-8で-5なら、nextは-13、minの後も-13、maxで-10になります。これが範囲内へ収めるクランプという処理です。
操作回数と数字を別に持つ
countは今の数、clicksは操作した回数です。10でさらに+1を押した場合、countは10のままでもclicksは増えます。
1つの変数へ2つの意味を持たせず、それぞれの役割を分けると、リセットの意味も明確になります。
9. 期待する動作を表で確かめる
| 操作の順 | 数字 | 操作回数 |
|---|---|---|
| 読み込み | 0 | 0 |
| +5 | 5 | 1 |
| +5 | 10 | 2 |
| さらに+1 | 10 | 3 |
| リセット | 0 | 4 |
| -5を3回 | -10 | 7 |
この表は、改良版の期待結果です。手元のブラウザで同じ順に押して確認してください。
ほかに、Tabでボタンへ移動してEnterやSpaceでも試します。数が二重に増えたら、旧scriptが残っていないか、同じイベントを2回登録していないかを調べます。
保存機能はないので、ページ再読み込みでは数字も操作回数も0へ戻ります。これは画面の「リセット」とは違う動作です。
シリーズのまとめ
今回つながったのは、変数で状態を覚えること、関数へ処理を分けること、DOMで画面を扱うこと、イベントで操作を受け取ることです。
文法を全部使い切る必要はありません。配列やオブジェクトも、必要な場面で使えば十分です。まずは1つ変更し、期待する動きが変わったかを確認する流れを繰り返してみましょう。