How to use findIndex() to find an index of selected item in JS?

Viewed 421

I'm making a todo list using JSONPlaceholder. Everything works fine (at least at the moment), but when I delete the selected <li>, the next <li> gets deleted.

I know it's because I did it by .splice(id - 1, 1) in deleteTodo. I know I should get the right index of the item which has the ID of the selected item instead of selectedItem, but I don't know how to get it. Probably, I'll have to use findIndex() but I can't think of the function to give as the parameter.

Any help will be appreciated.

todoList = {
    todos: [],
    removeClick: function (icon) {
        let selectedIndex = icon.parentElement.parentElement.dataset.index;
        todoList.deleteTodo(selectedIndex);
    },
    deleteTodo: function (index) {
        var id = index;
        $.ajax({
            url: `https://jsonplaceholder.typicode.com/todos/${id}`,
            type: 'delete', 
            success: function () {
                todoList.todos.splice(id - 1, 1);
                todoList.displayTodo();
            }
        })
    },
    displayTodo: function () {
        $ul.innerHTML = "";
        let innerContents = '';
        for (let i = 0; i < this.todos.length; i++) {
            let todo = this.todos[i];
            let className = 'completed'
            let checked = 'checked'
            if (todo.completed === false) {
                className = "";
                checked = "";
            }
            innerContents += `
                <li data-index="${todo.id}">
                    <div class="checkboxDiv">
                        <input type="checkbox" class="checkbox" data-key="checkbox"
                               ${checked} />
                    </div>
                    <div class="contents ${className}" data-key="contents">
                        ${todo.title}
                    </div>
                    <div class="icondiv">
                        <i class="far fa-edit" data-key="icon_edit"></i>
                        <i class="far fa-trash-alt" data-key="icon_trash"></i>
                    </div>
                </li>
            `;
        }
        $ul.innerHTML = innerContents;
    }
};
todoList.getData(); 
$ul.addEventListener('click', (e) => {
    switch (e.target.dataset.key) {
        case "icon_trash":
            todoList.removeClick(e.target);
            break;
    }
});
2 Answers

Solution:

Note: Below the code snippet, I explained the issue with your code

You can simply filter the data with deleted item

todoList.todos = todoList.todos.filter(({id: todoID}) => todoID !== id); 

or

todoList.todos.splice(todoList.todos.findIndex(({id: todoID}) => todoID == id), 1);

Don't do id - 1 because it is not array index, it is todo's id value

Add click event to trash icon itself

<i class="fa fa-trash" data-key="icon_trash" onclick="todoList.removeClick(this)"></i></div>

Below is the working code snippet( Lot of errors in your script, I don't know how that is working for you ;) )

var $ul = $('ul');

todoList = {

  todos: [],

  removeClick: function(element) {
    let id = $(element).parent().parent().data('index');
    todoList.deleteTodo(id);
  },


  deleteTodo: function(id) {
    $.ajax({
      url: `https://jsonplaceholder.typicode.com/todos/${id}`, //
      type: 'delete',
      success: function() {
        // todoList.todos = todoList.todos.filter(({id: todoID}) => todoID !== id);
        todoList.todos.splice(todoList.todos.findIndex(({id: todoID}) => todoID == id), 1);
        todoList.displayTodo();
      }
    })
  },

  getTodos: function() {
    $.ajax({
      url: `https://jsonplaceholder.typicode.com/todos/`, //
      type: 'get',
      success: function(data) {
        todoList.todos = data;
        todoList.displayTodo();
      }
    })
  },

  displayTodo: function() {
    $ul.innerHTML = "";
    let innerContents = '';
    for (let i = 0; i < todoList.todos.length; i++) {
      let todo = todoList.todos[i];
      let className = 'completed'
      let checked = 'checked'
      if (todo.completed === false) {
        className = "";
        checked = "";
      }
      innerContents += `<li data-index="${todo.id}"><div class="checkboxDiv">
                             <input type="checkbox" class="checkbox" data-key="checkbox" ${checked}/></div>
                             <div class="contents ${className}" data-key="contents">
                             ${todo.title}</div>
                             <div class="icondiv"><i class="fa fa-edit" data-key="icon_edit"></i>
                             <i class="fa fa-trash" data-key="icon_trash" onclick="todoList.removeClick(this)"></i></div>
                             </li>`

    }

    $ul.html(innerContents);

  },

}

todoList.getTodos();
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul></ul>

Issue: Consider below array of objects

[{id: 1, name: 'name 1'},{id: 2, name: 'name 2'},{id: 3, name: 'name 3'},{id: 4, name: 'name 4'}]

With your code todoList.todos.splice(id - 1, 1);

let's delete item whose id is 2 (name = 'name 2') means todoList.todos.splice(2 - 1(1), 1);. So it correctly delete the item from index 1 and new object will be

[{id: 1, name: 'name 1'},{id: 3, name: 'name 3'},{id: 4, name: 'name 4'}]

Now let's delete item whose id is 3 (name = 'name 3') means todoList.todos.splice(3 - 1(2), 1);.

Here is the problem with your code, now it will delete the item present at index 2 which is {id: 4, name: 'name 4'} and new object will be

[{id: 1, name: 'name 1'},{id: 3, name: 'name 3'}]

Explanation:

findIndex(({id: todoID}) => todoID == id)
  • I used ES6 Object destructuring feature.
  • Instead of accessing entire todo object in the findIndex, I am just only grabbing id property from todo object and renaming it to todoID
  • I renamed id to todoID to differentiate with id that we are getting as a parameter for the deleteTodo method
  • Atlast comparing todoID with id which is deleteTodo method's parameter
  • If comparision is success, then findIndex will return index of the todo object
  • So, Array.splice(<returned index>, 1) will remove that todo from the array

Without Object destructuring and arrow function, the code looks like below

todoList.todos.splice(todoList.todos.findIndex(function(todo) {
    return todo.id === id;
}), 1);

The problem is probably that the elements in the array shift, while their data-index attribute stays the same.

Say I have todos with ids [1,2,3], if I where to remove the todo with 1 this would correctly work. You do todos.splice(id - 1, 1). However the other elements now shift (2 is at index 0 and 3 is at index 1), so the same method of removing (with index id - 1) no longer works.

Instead use a Map or plain object to correlate the id with the todo. I'm not sure where you fill the todos array. But you can convert the current contents to a Map like so:

const todosById = new Map(todoList.todos.map(todo => [todo.id, todo]));

Here are the CRUD methods for a Map object:

todosById.set(todo.id, todo);   // create/update
const todo = todosById.get(id); // read
todosById.delete(id);           // delete

The index based for-loop has to go, instead use forEach or for...of:

todosById.forEach((todo, id) => {
  // ...
  innerContents += `html`;
});

for (const [id, todo] of todosById) {
  // ...
  innerContents += `html`;
}

Here is an example of a simplified version:

const todos = [
  {id: 1, title: "make homework", completed: true },
  {id: 2, title: "cook diner"   , completed: false},
  {id: 3, title: "walk the dog" , completed: true },
];

const $ul = document.getElementById("todo-list");

const todoList = {
    todos: new Map(todos.map(todo => [todo.id, todo])),
    set: function (id, properties) {
      id = parseInt(id, 10);
      if (!this.todos.has(id)) this.todos.set(id, {id});
      Object.assign(this.todos.get(id), properties);
      this.display();
    },
    delete: function (id) {
      this.todos.delete(parseInt(id, 10));
      this.display();
    },
    display: function () {
      while ($ul.lastChild) $ul.removeChild($ul.lastChild);
      let innerContents = '';
      this.todos.forEach(todo => {
        innerContents += `
          <li class="todo" data-id="${todo.id}">
            <input class="completed" type="checkbox" ${todo.completed ? "checked" : ""} />
            ${todo.title}
            <i class="fa fa-trash"></i>
          </li>
        `;
      });
      $ul.innerHTML = innerContents;
    }
};
todoList.display();

$ul.addEventListener("click", ({target}) => {
  let todo = target;
  while (!todo.matches(".todo[data-id]")) todo = todo.parentElement;

  if (target.matches(".fa-trash")) {
    todoList.delete(todo.dataset.id);
  } else if (target.matches(".fa-edit")) {
    // ...
  } else if (target.matches(".completed")) {
    todoList.set(todo.dataset.id, {completed: target.checked});
  }
});
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">

<ul id="todo-list"></ul>

Related