tiiti
@tiiti

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

javascriptの配列とfilerについて教えていただきたいです。

配列への正しいfilterの掛け方が分からず苦戦しております。

javascriptを使用し簡単なTODOリストを作成しております。
その中でfilterを使ったタスクの削除ボタンの実装がうまくいきません
(以前他のアプローチを他の方にご教示いただいたのですが、自分で試していた方法でもゴールまで持っていけないかと思い投稿させていただきました)

該当するソースコード

  const deletebutton = document.createElement("button");
  deletebutton.innerText = "削除";
  deletebutton.style.backgroundColor = "lightgray";
  deletebutton.addEventListener("click", () => {
    const resultdelete = tasks.filter((aaa) => aaa !== task);
    showTaskList(resultdelete);
    console.log("大元のタスク?", tasks);
    console.log("選択したタスク", task);
    console.log("resultdelete", resultdelete);

    // if (radiowork.checked) {
    //   const newresult = resultdelete.filter((task) => task.status === "作業中");
    //   console.log(newresult);
    //   showTaskList(newresult);
    // }
  });
  return deletebutton;
};

まず、記載コードで大元のタスクの配列(tasks)から削除ボタンを押したタスクのみを削除(ある意味非表示)にしそこからさらにfilterにかけ、作業中・完了・すべてとstatusに合わせて表示、非表示を切り替えようと考えているのですが、そもそも大元の配列tasksからタスクを削除できていないようです。
tasksをログに出力しても削除ボタンを押したタスクは残ったままでした。
自分でもこんがらがってしまっている部分があり拙い文章で大変申し訳ございません。
何かご助言を頂けますと幸いです。

下記全体

const input = document.getElementById("addbutton");
const tbody = document.getElementById("tbody");
const radiowork = document.getElementById("radio-work");
const radiocomplete = document.getElementById("radio-complete");
const radioall = document.getElementById("radio-all");

const tasks = [];
const createStatusButton = (task) => {
  const button = document.createElement("button");
  button.innerText = task.status;
  button.style.backgroundColor = "lightgray";
  button.addEventListener("click", () => {
    const buttonStatus = button.innerText;
    // console.log("変更前", task);

    if (buttonStatus === "作業中" && radiowork.checked) {
      button.textContent = "完了";
      task.status = "完了";
      const result = tasks.filter((task) => task.status === "作業中");
      showTaskList(result);
    } else if (buttonStatus === "完了" && radiocomplete.checked) {
      button.textContent = "作業中";
      task.status = "作業中";
      const result2 = tasks.filter((task) => task.status === "完了");
      showTaskList(result2);
    } else if (radioall.checked) {
      if (task.status === "作業中") {
        button.textContent = "完了";
        task.status = "完了";
      } else {
        button.textContent = "作業中";
        task.status = "作業中";
      }
      showTaskList(tasks);
    }
    // console.log("変更後", task);
  });

  return button;
};

const createdeleteButton = (task) => {
  const deletebutton = document.createElement("button");
  deletebutton.innerText = "削除";
  deletebutton.style.backgroundColor = "lightgray";
  deletebutton.addEventListener("click", () => {
    const resultdelete = tasks.filter((aaa) => aaa !== task);
    showTaskList(resultdelete);
    console.log("大元のタスク?", tasks);
    console.log("選択したタスク", task);
    console.log("resultdelete", resultdelete);

    // if (radiowork.checked) {
    //   const newresult = resultdelete.filter((task) => task.status === "作業中");
    //   console.log(newresult);
    //   showTaskList(newresult);
    // }
  });
  return deletebutton;
};

const showTaskList = (filteredTasks) => {
  console.log("filteredTasks", tasks);
  tbody.innerHTML = "";
  filteredTasks.forEach((task) => {
    const tr = document.createElement("tr");
    const td1 = document.createElement("td");
    td1.innerHTML = `${task.id}`;
    const td2 = document.createElement("td");
    td2.innerHTML = `${task.comment}`;
    const td3 = document.createElement("td");
    const td4 = document.createElement("td");
    tbody.appendChild(tr);
    tr.appendChild(td1);
    tr.appendChild(td2);
    tr.appendChild(td3);
    tr.appendChild(td4);
    td3.appendChild(createStatusButton(task));
    td4.appendChild(createdeleteButton(task));
  });
};

const tasksStatus = document.getElementById("statusradio");
const text = document.getElementById("tasks");

input.addEventListener("click", () => {
  const text = document.getElementById("tasks");
  if (text.value === "") {
    alert("タスクを入力してください");
  } else {
    tasks.push({
      id: tasks.length + 1,
      comment: text.value,
      status: "作業中",
    });
    if (radiowork.checked) {
      const result = tasks.filter((task) => task.status === "作業中");
      showTaskList(result);
    } else if (radiocomplete.checked) {
      const result2 = tasks.filter((task) => task.status === "完了");
      showTaskList(result2);
    } else if (radioall.checked) {
      showTaskList(tasks);
    }
    text.value = "";
  }
});

radiowork.addEventListener("change", (task) => {
  console.log(tasks); // ラジオボタン(作業中)がクリックされた時のタスクを表示
  const result = tasks.filter((task) => task.status === "作業中");
  console.log("result", result); // フィルターをかけた後のタスクのみをコンソールに出力
  showTaskList(result);
});

// ラジオボタン(完了)がクリックされた時
radiocomplete.addEventListener("change", () => {
  const result2 = tasks.filter((task) => task.status === "完了");
  console.log("result", result2);
  showTaskList(result2);
});

// ラジオボタン(完了)がクリックされた時
radioall.addEventListener("change", () => {
  showTaskList(tasks);
});

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <title>JS課題3</title>
  </head>
  <body>
    <h1 class="text-4xl font-semibold font-serif">ToDoリスト</h1>
    <div class="mt-8" id="statusradio">
      <input
        type="radio"
        name="taskradio"
        id="radio-all"
        value="all"
        checked
      />すべて
      <input type="radio" name="taskradio" id="radio-work" value="work" />作業中
      <input
        type="radio"
        name="taskradio"
        id="radio-complete"
        value="complete"
      />完了
    </div>

    <table>
      <thead>
        <tr class="text-xl font-extrabold font-serif">
          <th class="px-1">ID</th>
          <th class="px-1">コメント</th>
          <th class="px-1">状態</th>
        </tr>
      </thead>

      <tbody id="tbody" name="tbody"></tbody>
    </table>

    <p class="text-2xl font-black font-serif pt-4">新規タスクの追加</p>
    <div class="pt-4">
      <input
        type="text"
        id="tasks"
        class="border border-gray-500"
        name="tasks"
      />
      <input type="button" id="addbutton" value="追加" class="rounded" />
    </div>

    <script src="index.js"></script>
  </body>
</html>

0

2Answer

==== 追記 ====
こちらの回答はVercleneさんのご指摘の通り、今回の質問のケースに当てはまりませんでした。
見当違いの回答になってしまい申し訳ございません。
= = = = = = = = =

taskはオブジェクトです。
オブジェクトの比較は===(!==)でできません。

const resultdelete = tasks.filter((aaa) => aaa !== task);

下記のように、中身が同じ値でも、taskAtaskBは別物なのでfalseになります。

const taskA = {
  id: 1,
  comment: "comment",
  status: "作業中",
};

const taskB = {
  id: 1,
  comment: "comment",
  status: "作業中",
};

console.log(taskA === taskB); // => false

オブジェクト比較の一例

もっと詳しくはこちら

なので、配列へのfilterの掛け方が問題ではなく、
filterの中で行なっているオブジェクトの比較が問題です。

今回の場合、idのようなオブジェクトの一意性を担保できるプロパティがあると、「idが同じなら同じタスク」と考えることができます。

const resultdelete = tasks.filter((aaa) => aaa.id !== task.id);

オブジェクトの全項目で比較したい時は、専用の関数をあらかじめ作成する手もあります。
先ほどのtaskAtaskBisSameTask関数で比較すると、trueという結果になります。

const isSameTask = (taskA, taskB) => {
    // 項目ごとで値をチェックして、異なる値だったらfalse
    if (taskA.id !== taskB.id) return false;
    if (taskA.comment !== taskB.comment) return false;
    if (taskA.status !== taskB.status) return false;
    // 最後までreturnされていなかったらtrue
    return true;
}

console.log(isSameTask(taskA, taskB)); // => true
const resultdelete = tasks.filter((aaa) => !isSameTask(aaa, task));

最後に、色々試している途中だからだと思いますが、生存期間が短い変数とは言え、aaaという命名はよろしくないです。。。

1Like

Comments

  1. ちなみに、tasksに削除済みのタスクを残すかどうか考えておいた方がいいです。
    以前私が提案した方法ですと、削除されたタスクのstatusを変更し、削除されてもtasksに残ります。
    例えば今回tasksにfilter済みの配列(resultdelete)を代入することになった場合、
    削除されたタスクはtasksから消えます。
    どのやり方にするのは自由ですが、タスクを残さない場合、
    タスク追加時にidが重複するリスクがあります。

    id: tasks.length + 1,

    下記のような操作が考えられるためです。
    1. [{id: 1}, {id: 2}] => 追加(id: 2 + 1)
    2. [{id: 1}, {id: 2}, {id: 3}] => 削除(id: 2)
    3. [{id: 1}, {id: 3}] => 追加(id: 2 + 1)
    4. [{id: 1}, {id: 3}, {id: 3}]
        ↑id:3が重複
  2. @tiiti

    Questioner

    前回に引き続き誠にありがとうございます!
    aaa.id !== task.idもaaa!== taskも同じじゃないか?
    と浅はかな考えで記載してしまっておりました、、
    aaaの命名も以後気をつけます。。

    また削除済みのタスクを残すかについてもご記載いただきありがとうございます!
    おそらく自身では気づけなかったと思いますので注意いたします!!
  3. @tiiti

    Questioner

    @tiitiさん、、、時間差の再質問を失礼したします。。。
    仰っていただいたIDの重複に少し関係する事なのですが、

    const resultdelete = tasks.splice(task.id - 1, 1);
    showTaskList(tasks);

    上記にて削除ボタンのクリックイベントでtasksからタスクを削除する際、
    1回目は選択したタスクを削除できる
    2回目は選択したタスクの一つ下のタスクが削除される
    上記事象が発生いたします。
    タスクを削除した後、IDをつめて振りなおせれば解決するのではと考えています。
    (削除後タスクを追加してもIDが重複しなくなる)
    そのような実装は可能なのでしょうか??
  4. まず、最初の回答にも追記しましたが、
    本文の内容は見当違いでした。申し訳ございません。

    追加にいただいた質問の回答は下記にて記載します。
    splice()の第一引数は配列の先頭位置です。つまりindexです。
    下記のコードはid 1~6 から 1,2 を削除しようとした結果、2,4,5,6が残るコードです。

    ------------------------------
    let deletedId;
    let resultdelete;
    const tasks = [1, 2, 3, 4, 5, 6];

    // id: 1を削除しようとする
    deletedId = 1;
    resultdelete = tasks.splice(deletedId - 1, 1); // Array [1]

    // id: 2を削除しようとする
    deletedId = 2;
    resultdelete = tasks.splice(deletedId - 1, 1); // Array [3]

    console.log(tasks); // Array [2, 4, 5, 6]
    ------------------------------

    一回目の配列: [1, 2, 3, 4, 5, 6]
    一回目の対象index: 1 - 1 = 0
    一回目の対象id: 1

    二回目の配列: [2, 3, 4, 5, 6]
    二回目の対象index: 2 - 1 = 1
    二回目の対象id: 3
    ------------------------------
    このように、質問者から見て、「2回目は選択したタスクの一つ下のタスクが削除される」事象が起きています。
    「タスクを削除した後、IDをつめて振りなおせれば解決するのではと考えています。」
    ↑のような実装、やろうと思えば可能ですが、おすすめ致しません。
    つめて振りなおす前提であれば、最初からIDの概念をなくし、indexで操作する前提で作るのはいかがでしょうか。
    一意性を担保できないならIDがない方がいいと個人的に思っています。

    また、今回は素のJavaScript(PureJS/VanillaJS)を使っていて大きな影響は出ないと思います。
    将来学習が進み、データベースとかフレームワークとかも使い始めた場合、
    質問者様のおっしゃったパターンではいろいろ問題が出てくると思います。

    結論として、実装は可能です。
    私個人としておすすめできない方法ですが、目的は「質問者自身の考えた方法でゴールまで持っていきたい」ならば、試してみる価値はあると思います。
    もし検討の余地があれば、IDを振りなおすではなく、IDをなくして、最初からindexのみで操作する前提にした方が無駄がないと思います。
  5. @tiiti

    Questioner

    ご返信が遅れてしまい申し訳ございません。
    ご丁寧にご回答いただき誠にありがとうございます!
    現実的な事までご教示いただきありがとうございます。
    とてもためになりました!
    おすすめできない理由も念頭にアウトプットの練習がてら少しやってみます。
    やはり以前ご回答いただいた実装方法がベストだったと改めて感じました!

filter()は配列をシャローコピーします.

元の配列からfilter()で要素を削除するには,元の配列をfilter()した結果で書き換えてください.

0Like

Comments

  1. @tiiti

    Questioner

    ご回答いただきありがとうございます。

    例えば、下記の場合
    const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
    const result = words.filter(word => word.length > 6);

    filterをかけた後、resultを使って実装していく
    といったイメージで正しいでしょうか?
  2. resultを使って何を実装するおつもりかは知りませんが,少なくともdeleteButtonのコールバックで呼び出される処理が元のtasksに影響していないことを指摘しています.
    (注:上の例でもconsole.log(words)すれば内容が書き換わっていないことがお分かりいただけると思います.)

    何かしら適切にtasks自体を書き換える必要があります.

    @aotoriii さんのオブジェクト比較云々についてですが,createDeleteButtonに元のtaskオブジェクトを渡しているので現状のコードで動いてしまいます.
    taskのidを識別子として使用するつもりなら,そのあたりも考慮に入れてください.
  3. @tiiti

    Questioner

    元々tasks自体の書き換えに苦戦し、質問を投稿させていただいたのですが、
    大元の配列(tasks)の書き換えは可能なのでしょうか??
  4. 何も考えなければtasks = tasks.filter()で済む話です.
    ただし意図しないタイミングで呼ばれないように注意する必要があります.
    tasksを使用した重い処理が走る可能性がある場合,その間tasksが書き換わらないようにする等考慮してください.

Your answer might help someone💌