Uncaught TypeError: Cannot read property 'dataset' of null になる理由

お世話になっております。前回はありがとうございました。
2回目の質問になります。
今回の質問はコードが長いのでためらいましたが、どうしても自力では理解できず質問させていただきました。

現在TodoMVCというTodoアプリのサンプルサイトを参考に勉強しています。
https://todomvc.com/examples/vanillajs/
https://github.com/tastejs/todomvc/tree/gh-pages/examples/vanilla-es6/src
質問の内容はエラーになる理由なのですが、エラー自体は解決できています。
しかし、そのコードでなぜエラーが解決できるのかがわからず質問させていただきました。
エラーの内容はこちらです。

Uncaught TypeError: Cannot read property 'dataset' of null
    at _itemId (view.js:7)
    at HTMLInputElement.<anonymous> (view.js:247)
    at HTMLUListElement.dispatch(helpers.js: 44)

問題の箇所はview.jsにあるbindEditItemSavebindEditItemCancelの★の部分です。
これらが参照しているview.js_itemId関数ではelementの親要素(またはさらにその親要素)のdatasetを取得しているのに
その子要素であるelement(編集中のinput)にdatasetを指定しないと上記のエラーになってしまいます。
この理由が理解できずに苦戦しています。
このエラーの理由をご教授いただけますでしょうか。

考えたこと

バブリングとキャプチャリングが関係しているのかと推察して
stopPropagationを使ってみましたがダメでした。

Todoアイテムを追加し編集中の状態のHTMLです

<section style="display: block;" class="main">
      <input id="toggle-all" class="toggle-all" type="checkbox">
      <label for="toggle-all">Mark all as complete</label>
    <ul class="todo-list">
      <li data-id="1613277309944" class="editing"> /* このdatasetを取得しているはず */
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>テスト</label>
          <button class="destroy"></button>
        </div>
        <input class="edit"> /* 編集中のinput */
      </li>
    </ul>
      <footer class="footer">
        <span class="todo-count">1 item left</span>
        <ul class="filters">
          <li>
            <a href="#/" class="selected">All</a>
          </li>
          <li>
            <a href="#/active">Active</a>
          </li>
          <li>
            <a href="#/completed">Completed</a>
          </li>
        </ul>
        <button class="clear-completed" style="display: none;">Clear completed</button>
      </footer>
    </section>

helpers.js

export function $on(target, type, callback, capture) {
    target.addEventListener(type, callback, !!capture);
}

/**
 * セレクタに一致するすべての要素のイベントにハンドラを付けます。
 */
export function $delegate(target, selector, type, handler, capture) {
    const dispatch = (event) => {
        const targetElement = event.target;
        const potentialElements = target.querySelectorAll(selector);
        let i = potentialElements.length;
        while (i--) {
            if (potentialElements[i] == targetElement) {
                handler.call(targetElement, event);
            }
        }
    }
    $on(target, type, dispatch, !!capture);
}

view.js

// TodoアイテムのIDを10進数の整数値にパースする関数
// ★親要素のdatasetを取得しているはずなのに子要素のdatasetを作らないとエラーになる
const _itemId = element => {
  return parseInt(element.parentNode.dataset.id || element.parentNode.parentNode.dataset.id, 10);
};

export default class View {

  /**
   * Todoリストの編集中アイテムがフォーカスを失った時にhandlerを実行
   */
  bindEditItemSave(handler) {
    $delegate(this.$todoList, 'li .edit', 'blur', ({ target }) => {
      if (!target.dataset.iscanceled) { // ② ★これが無いとエラーになってしまう
        handler(_itemId(target), target.value.trim());
      }
    }, true);
    $delegate(this.$todoList, 'li .edit', 'keypress', ({ target, keyCode }) => {
      if (keyCode === ENTER_KEY) {
        target.blur();
      }
    });
  }

  /**
   * Todoリストの編集中アイテムでESCキーが押された時にフォーカスを外しhandlerを実行
   */
  bindEditItemCancel(handler) {
    $delegate(this.$todoList, 'li .edit', 'keyup', ({ target, keyCode }) => {
      if (keyCode == ESCAPE_KEY) {
        target.dataset.iscanceled = true; // ①★これが無いとエラーになってしまう
        target.blur();
        handler(_itemId(target));
      }
    });
  }
}

controller.js

export default class Controller {
  constructor(store, view) {
    this.store = store;
    this.view = view;
    view.bindEditItemSave(this.editItemSave.bind(this));
    view.bindEditItemCancel(this.editItemCancel.bind(this));
  }
    /**
     * エディットでアイテムを保存します。
     */
    editItemSave(id, title) {
        if (title.length) {
            this.store.update({ id, title }, () => {
            this.view.editItemDone(id, title);
            });
        } else {
            this.removeItem(id);
            }
    }

    /**
     * アイテム編集モードをキャンセルします。
     */
    editItemCancel(id) {
        this.store.find({ id }, data => {
            const title = data[0].title;
            this.view.editItemDone(id, title);
        });
    }
}

たしぎ@プログラミング

大手ブラック企業脱出→プログラミング独学中/主にプログラミング学習についてツイートしてます。WordPress/PHP/JavaScript/競技プログラミングも少し/サッカーとアニメも好きです #30DAYSトライアル

コメント