はじめに
JSでTODOアプリをつくり、そのやり方を教材として書き起こしていた。教材の仕様に沿ってアロー関数をつかったらうまく動作しなくなった。なんだこれは。少しだけ考えた。
問題
問題が発生したのはTODOの内容をクリックして、既存のタスクを編集する箇所。今までのfunctionではOKだったが、アロー関数にするとなんだかおかしい。
コード
問題点を明らかにするためにその箇所だけを抜き出して別にコードを書いた。「テスト」をクリックするとアロー関数のやつが実行されて、「テスト2」だと普通の関数が実行される。
テストをクリックして、入力フォームのカーソルを外すと
「Uncaught TypeError: Cannot read property 'classList' of null」
というエラーが出てくる。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<link rel ="stylesheet" href="todo.css">
<title>イベントハンドラテスト</title>
</head>
<body>
<header>
<h1>TODOテスト</h1>
</header>
<div class="container">
<div class="todo-container">
<ul class="todo-list">
<li>
<span class="todo-content1">テスト</span>
</li>
<li>
<span class="todo-content2">テスト2</span>
</li>
</ul>
</div>
</div>
<script src = "test.js"></script>
</body>
</html>
const editTodo = (e) => {
let itemToEdit = e.target;
if(!itemToEdit.classList.contains('on')) {
itemToEdit.classList.add('on');
let contentBeforeEdit = itemToEdit.textContent;
itemToEdit.innerHTML = '<input type="text" class="editbox1" value="'+contentBeforeEdit+'" />';
const editContent1 = document.querySelector('.editbox1');
const saveTodoContent = (e) => {
let itemToSave = e.target;
itemToSave.parentNode.classList.remove('on');
let txtvalue = itemToSave.value;
if (txtvalue ==''){
txtvalue = itemToSave.defaultValue;
}
itemToSave.parentNode.innerHTML = txtvalue;
}
editContent1.addEventListener('blur',saveTodoContent);
}
}
function editTodo2(e) {
if(!this.classList.contains("on")) {
this.classList.add("on");
let contentBeforeEdit = this.textContent
this.innerHTML = '<input type="text" class="editbox2" value="'+contentBeforeEdit+'" />'
const editContent2 = document.querySelector(".editbox2")
let saveTodoContent = function(){
this.parentNode.classList.remove('on')
let txtvalue = this.value
if (txtvalue ==""){
txtvalue = this.defaultValue
}
this.parentNode.innerHTML = txtvalue
}
editContent2.addEventListener("blur",saveTodoContent)
}
}
//Select DOM
const todoContent = document.querySelector('.todo-content1');
todoContent.addEventListener('click', editTodo);
const todoContent2 = document.querySelector('.todo-content2');
todoContent2.addEventListener('click', editTodo2);
問題を追う
カーソルを外した時に「classListがnullですよ」というエラーが発生する。ちょうどこの行である。
const saveTodoContent = (e) => {
//
itemToSave.parentNode.classList.remove('on');
//
Chromeのデベロッパーモードで少しずつ検証していった。そうするとカーソルを外す時に実行されるイベントsaveTodoContent
が2回実行されていることがわかった。そして2回目の実行時にエラーが発生していた。なぜだ。
だいたいはthisが原因だった。
原因
この機能では項目の状態を知る必要がある。つまり「タスクが現在入力モードなのか否か」ということを知りたいのだ。コード上ではclasslistにonというclassを付与したり外したりしてそれを操作している。
今まではその検知にthisを用いていたが、今回アロー関数に書き換えたことによってthisが使えなくなり、別の記法で書き換えた。そこがよくなかったのだ。
ここである。
function editTodo2(e) {
if(!this.classList.contains("on")) {
// .. クリックときに編集モードでなければ以下を実行
const editTodo = (e) => {
let itemToEdit = e.target;
if(!itemToEdit.classList.contains('on')) {
// .. クリックときに編集モードでなければ以下を実行
this/e.target
thisとe.targetはなにが違うのか。thisは場面によってさまざまに容態を変化させるが、ここでは以下の記事を参考にするならば「関数を呼び出している元のオブジェクト」である。
e.targetとはクリックしたときの物体である。テキスト文字であったり、入力フォームであったりする。
thisの中身はタスクが編集モードか否かでも変わらない。しかしe.targetは中身が変わる。ためしにconsole.logで読んでみよう。
const editTodo = (e) => {
let itemToEdit = e.target;
console.log(e.target);
console.log(this);
//
function editTodo2(e) {
console.log(e.target);
console.log(this);
上4行はアロー関数のほう。下はいままでの関数だ。たしかになんか違っていた。
どうしたか
アロー関数にはイベントが実行される条件を増やした。
const editTodo = (e) => {
let itemToEdit = e.target;
if(!itemToEdit.classList.contains('todo-content1')){
return;
}
if(!itemToEdit.classList.contains('on')) {
//....
これで問題は解決した。
おわりに
文章がまとまらないままこれを書き出してしまった。それでもどこかに残しておかないと、後々自分が困ってしまう。
もしかしたら追記するかもしれないししないかもしれない。