はじめに
ナイトウ(@engineer_naito)と申します。
Vueを最近勉強しています。
Vueを勉強しているのですが、それ以前にTypeScript, JavaScriptの基礎知識が不足していることに気づきました。
そこでまずはフレームワークを利用せずに、
- HTML
- CSS(JavaScript)
- JavaScript(Vanilla)
でTODOアプリを作成してみることにしました。
コード
- JSFiddle
単純なTODOアプリです。
- タスクを入力して「登録」ボタンを押すとタスクが画面に表示される(箇条書き)
- 「削除」ボタンを押すとそのタスクが画面から消える
- チェックボックスを押すとタスクに取り消し線が入る
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="index.js"></script>
<title>vanilla todo</title>
</head>
<body>
<h1>Todos</h1>
<form id="todoForm">
<input id="task-input" />
<button type="submit">登録</button>
</form>
<ul id="todos"></ul>
</body>
</html>
const handleFormSubmit = (event) => {
event.preventDefault();
const inputElement = document.getElementById("task-input");
const inputValue = inputElement.value.trim();
if (!inputValue) {
alert("入力してください。");
return;
}
const todosElement = document.getElementById("todos");
const todoListItem = createTodoListItem(inputValue);
todosElement.appendChild(todoListItem);
inputElement.value = "";
};
const createTodoListItem = (todoContent) => {
const todoListItem = document.createElement("li");
const todoDone = document.createElement("input");
todoDone.type = "checkbox";
todoDone.onchange = () => {
todoListItem.classList.toggle("checked");
};
const todoLabel = document.createElement("label");
todoLabel.innerText = todoContent;
const removeTodoButton = document.createElement("button");
removeTodoButton.innerText = "削除";
removeTodoButton.onclick = () => {
todoListItem.remove();
};
todoListItem.append(todoDone, todoLabel, removeTodoButton);
return todoListItem;
};
document.getElementById("todoForm").addEventListener("submit", handleFormSubmit);
こちらのコードの是非はとりあえず置いておいて(コメントなどで指摘などよろしくお願いします。)、
JSFiddleに突っ込んで動作確認してみます。
うん、見た目はさておき意図した挙動になっています。
やった。
ここまでのコードを見て、このコードには明確な問題点があると気付いた方はいるでしょうか。
JSFiddleでは問題なく動作しているのでぼくは全く何も心配いらないと思っていました。
このあと先ほどのコードの問題点が発覚します。
ローカルでの動作確認
さてJSFiddleでの動作確認が済んだので、先ほどのコードをローカルで確認してみました。
ローカルで確認したところ、JSFiddleでは問題なかったのですがローカルだと動かないのです。
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')]
というエラーがコンソールに出てしまっています。
これはなんでしょうか。
エラーメッセージを見るに index.js
の
document.getElementById("todoForm").addEventListener("submit", handleFormSubmit);
に問題があるように見えますが、、、
Uncaught TypeError: Cannot read properties of null
エラーメッセージの内容を読むと、nullのプロパティを参照できないようなことを言っています。
それはそうです。
つまり、 index.js
の
document.getElementById("todoForm").addEventListener("submit", handleFormSubmit);
における document.getElementById("todoForm")
がnullだというエラーになっています。
これはどういうことでしょうか。
index.html
には <form id="todoForm">
があります。
それにも関わらず document.getElementById("todoForm")
がnullであると言われています。
JSではこのHTML内の <form id="todoForm">
が見えていないようです。
ぼくはどこを間違えているのでしょうか、何が原因なんでしょうか。
HTMLの解析
HTMLはコードを上から順に解析されるようです。
(すみません、根拠となる公式リファレンスを見つけることができませんでした。)
index.html
も上から順に解析されていき、
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="index.js"></script>
この行まで到達した際にJSファイルが読まれて実行されるはずです。
つまり、この時点ではまだHTMLファイルのbodyタグが解析されておらず、そのタグ内にある <form id="todoForm">
は見えていないのです。
これが今回遭遇したエラーの原因でした。
JSFiddleでは問題なく動作した理由がわかりません。
どなたかご存知の方いれば教えてください。
対策と回避方法
bodyタグの末尾にscriptタグを移動
これはHTMLが上から順に読まれることを考えると一番自然な解決方法に思えます。
scriptタグをこの位置に持ってくることで、bodyタグの要素が読み込まれたあとにJSファイルが実行されるため、 document.getElementById("todoForm")
は正しく取得することができます。
scriptタグにdefer属性(またはasync属性)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="index.js" defer></script>
<title>vanilla todo</title>
</head>
とするか、
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="index.js" async></script>
<title>vanilla todo</title>
</head>
とすることで解決することができました。
deferは遅延を意味し、asyncは非同期を意味しています。
ぼんやりなぜ解決することができたのかわかります。
scriptタグの属性(defer, async)
defer
JSの解析が完了し終わったあと、JSの実行をHTMLの解析が終わるまで遅らせています。
async
JSの解析が終わり次第、すぐにJSの実行を行います(HTMLの解析を待たずに)。
まとめ
scriptタグはheadタグ内に記載するイメージでしたが、そのまま書いても正しく動作しませんでした。
- bodyタグの末尾
- defer属性
-
async属性(追記(2024.03.02))
のいずれかで今回は解決することができました。
今の知識ではどの方法が望ましいのか、ケースバイケースなのかわかりません。
指摘やアドバイスなどあればいただきたいです。
最後に
今まで雰囲気でHTMLを書いていたのですが、手でゼロイチから書くと意外と詰まることが多くて勉強してばかりです。
環境構築の知見が乏しく、どうしてもJSFiddleみたいなplaygroundに頼ることばかりです。
今後はその辺の知識も身につけていきたいです。
最後まで読んでいただきありがとうございました!
追記(2024.03.02)
コメントをいただいたのでこちらで追記いたします。
-
DOMContentLoaded
イベントを利用する
https://qiita.com/kosuke-naito/items/c131b8be9d777d4cc70c#comment-5a9cb7e120590411b5ed
document.addEventListener('DOMContentLoaded', () => {
document.getElementById("todoForm").addEventListener("submit", handleFormSubmit);
});
とすればよさそう?
-
async
属性では正しい動作が保証されない
https://qiita.com/kosuke-naito/items/c131b8be9d777d4cc70c#comment-356cf0b4e1b4a2d72c04
もしサーバーの応答速度が速かった場合、レンダリングエンジンが #todoForm 要素までたどり着く前に index.js のダウンロードが完了し実行されてしまいます。
とのことで、私の実行した際にはこのケースにたまたまあてはまらなかったのでうまくいっていたようです。
環境によって挙動が変わってしまうのは望ましく無いので今回は async
属性を用いるのは不適当でした。